Skip to content

Latest commit

 

History

History
1922 lines (1483 loc) · 62.4 KB

02.md

File metadata and controls

1922 lines (1483 loc) · 62.4 KB

Matéria de LP2

Nota 1: Nos exercícios com imagens, as imagens devem ser guardadas em modo Git LFS na pasta solucoes/02/ e devem ter como nome o número do exercício.

Nota 2: Nos exercícios em que faça sentido submeter uma solução ou projeto .NET Console App completo, podes colocar o mesmo numa pasta com o número do exercício + underscore + nº da solução, por exemplo solucoes/02/005_sol1.

Nota 3: Nos exercícios em Unity deves seguir a mesma lógica que no caso anterior, mas deves também incluir nessa mesma pasta os ficheiros .gitignore e .gitattributes disponíveis em https://github.com/VideojogosLusofona/unity_git.

Problemas

1 - Considera o seguinte código:

class Program
{
    static void Main(string[] args)
    {
        int[] arrayOfInts = new int[] { 1, 2, 3, 4 };

        foreach (int i in arrayOfInts)
        {
            Console.WriteLine(i);
        }
    }
}
  1. Converte o código apresentado para que faça uso explícito do enumerador devolvido pelo array.
  2. Qual é ou quais são as vantagens do uso de foreach sobre o uso explícito de enumeradores?
  3. Qual é a interface que a classe array implementa que a obriga a ter um método para devolver um enumerador? Esta implementação é direta ou através de outras interfaces intermédias? Se se verificar este último caso, indica essa hierarquia de interfaces.
  4. Qual é o design pattern que propõe esta forma de aceder sequencialmente aos elementos de um objeto agregado/coleção, independentemente da forma como o mesmo foi implementado.

Soluções


2 - Considera o seguinte código inicial para um jogo de real-time strategy (RTS):

public struct Vector2
{
    public float X { get; }
    public float Y { get; }

    public Vector2(float x, float y)
    {
        X = x;
        Y = y;
    }
}
public interface IUnit
{
    string UnitName { get; }
    Vector2 Position { get; }
    float Health { get; }

    void Move(Vector2 newPosition);
}
public class Unit : IUnit
{
    public string UnitName { get; private set; }
    public Vector2 Position { get; private set; }
    public float Health { get; private set; }

    public Unit(string name, Vector2 position, float health)
    {
        UnitName = name;
        Position = position;
        Health = health;
    }

    public void Move(Vector2 newPosition)
    {
        Position = newPosition;
    }

    public override string ToString()
    {
        return $"{UnitName} at ({Position.X:f1}, {Position.Y:f1}) " +
            $"with {Health:f1} HP";
    }
}

Chegaste a um ponto no desenvolvimento do teu jogo em que pretendes ter batalhões ou grupos de unidades que funcionem de forma semelhante às unidades individuais, nomeadamente que implementem a interface IUnit. Queres ainda permitir a existência de sub-batalhões, ou seja, sub-grupos dentro dos grupos de unidades. O funcionamento das propriedades e dos métodos de um grupo de unidades deve ser o seguinte:

  • Name - Um grupo de unidades deve ter o nome Group of x units, em que x representa o número de unidades no grupo (incluíndo unidades em sub-grupos).
  • Position - Deve representar o centroide (média da posição x e média da posição y) das posições das unidades no grupo. A posição de um sub-grupo deve contar apenas como uma unidade.
  • Health - Média da Health das unidades individuais. A Health de um sub-grupo deve contar apenas como uma unidade.
  • Move() - A posição dada é o novo centroide do grupo. Todas as unidades individuais devem manter as posições relativas entre si. Para o efeito deve ser determinado o vetor de movimento entre os dois centroides (= newPos - oldPos), e este vector deve ser adicionado à posição atual de cada unidade no grupo, como ilustrado na seguinte imagem:

image

Autoria dos ícones na imagem: Eucalyp (Flaticon)

  1. Qual o design pattern ideal para resolveres este problema?
  2. Aplica e adapta o design pattern em questão para resolveres o problema, minimizando dentro do possível alterações ao código existente. Nota que os design patterns apenas oferecem soluções gerais, pelo que não tens de seguir exatamente o respetivo template, sobretudo se isso fizer sentido e melhorar a tua solução final.
  3. Cria uma classe Program com o método Main() para testares a solução desenvolvida em modo consola e sem interação com o utilizador.

Soluções


3 - Adapta o código da solução anterior para uso num novo projeto 2D em Unity. O projeto deve ser muito simples (sem animações, som, etc), e permitir a seleção de uma unidade com o botão esquerdo do rato (eliminando a seleção anterior) e o agrupamento de várias unidades com a combinação Ctrl+LeftMouseButton. Clicar numa zona sem unidades tem o efeito de mover a(s) unidade(s) selecionada(s) para essa localização. As várias ações no jogo devem ser descritas na consola do Unity com o método Debug.Log(), fazendo uso do(s) override(s) de ToString() da(s) classe(s) que representa(m) unidade(s).

Descreve ainda quais as principais alterações que tiveste de fazer no código original para o mesmo ser adaptado ao Unity.

Nota: Para resolução deste exercício não deves usar a hierarquia de game objects do Unity para agrupar as unidades. Deves manter, dentro do possível, a estrutura original do código do exercício anterior.

Soluções


4 - Resolve o problema anterior usando a hierarquia de game objects do Unity para agrupar e mover as unidades em grupo.

Soluções


5 - Considera as co-rotinas do Unity e responde às seguintes questões:

  1. Apresenta o código de uma co-rotina que verifique, de 5 em 5 segundos, se a variável de instância health é inferior a 25.0f, e em caso afirmativo, imprima "Low health" na consola.
  2. Considera a variável de instância healthCheck do tipo IEnumerator, cujo valor é obtido com a invocação da co-rotina da alínea anterior. Apresenta uma linha de código para dar início à co-rotina, e outra linha de código para parar a co-rotina.

Soluções


6 - Como podes fazer com que uma co-rotina seja invocada com um período de tempo aleatório entre um dado mínimo e máximo? Apresenta um exemplo em código.

Nota: Este exercício tem múltiplas formas de ser resolvido, sendo apropriado para várias soluções.

Soluções


7 - Modifica a seguinte linha do seguinte código de modo a que respeite o dependency inversion principle, tendo em conta que a coleção em questão terá de suportar a adição, remoção e contagem de itens:

List<string> containerOfStrings = new List<string>();

Justifica a tua resposta.

Soluções


8 - Considera a seguinte classe:

[Awesome] public class BestEverClass { }

Responde às seguintes questões:

  1. Cria um atributo chamado AwesomeAttribute, sem parâmetros, aplicável apenas a classes.
  2. Na classe Program, método Main():
    1. Cria um array de object com os seguintes conteúdos:
      • Índice 0: instância de string com a frase "String de teste".
      • Índice 1: instância de Random.
      • Índice 2: instãncia de BestEverClass.
    2. Percorre o array com um foreach, obtendo para cada objeto o seu Type, e analisando através de reflexão, se o tipo em questão tem ou não o atributo AwesomeAttribute, mostrando essa informação no ecrã. Para o caso do array em questão, deverá aparecer no ecrã o seguinte:
Type 'string' is not awesome :(
Type 'Random' is not awesome :(
Type 'BestEverClass' is awesome :)

Nota: Exercício de dificuldade elevada que requer ir um pouco além da matéria lecionada nas aulas.

Soluções


9 - Cria um programa em C# que aceite como único argumento da linha de comandos um nome de ficheiro e imprima os conteúdos desse ficheiro no ecrã (semelhante à instrução cat do Linux/Bash/Powershell).

O programa deve tratar separadamente todas as exceções que podem ocorrer quando o ficheiro é aberto para leitura, apresentando uma mensagem de erro adequada e simples em cada caso.

Soluções


10 - Considera a seguinte struct:

public struct TilePosition
{
    public static int XMax { get; internal set; }
    public static int YMax { get; internal set; }
    public int X { get; }
    public int Y { get; }
    public TilePosition(int x, int y)
    {
        // Código do construtor
    }
}

Responde às seguintes questões:

  1. Adiciona o código ao construtor, de modo as propriedades X e Y sejam inicializadas com valores entre 0 e XMax ou YMax, conforme o caso. Se os parâmetros x e y estiverem fora do respetivo intervalo, deve ser lançada a exceção InvalidTilePositionException, que aceita como argumentos dois bools, que indiquem respetivamente se os parâmetros x e y são inválidos.
  2. Cria a classe InvalidTilePositionException, fazendo override da propriedade Message de modo a que indique quais os argumentos inválidos (x, y ou ambos).
  3. Na classe Program, método Main(), testa as várias possibilidades (parâmetros válidos, um parâmetro inválido, ambos os parâmetros inválidos), capturando devidamente a exceção.

Soluções


11 - Considera um jogo tile-based, em que cada tile tem as seguintes propriedades:

  • DefensePoints - Bónus de defesa (DEF)
  • EssentialPoints - Bónus de extração de bens essenciais (ESS)
  • LuxuryPoints - Bónus de extração de bens de luxo (LUX)

Inicialmente existem os seguintes tipos base de tile com as características indicadas:

  • Grassland: DEF = 0, ESS = 3, LUX = 0
  • Desert: DEF = -1, ESS = 0, LUX = 1
  • Jungle: DEF = 1, ESS = 1, LUX = 1
  • Mountain: DEF = 3, ESS = 1, LUX = 1

Além disso, estes tiles podem ainda ter especializações particulares que podem alterar as suas características conforme indicado:

  • Vineyard: ESS+1, LUX+1
  • Sheep: ESS+3
  • Gold: LUX+4
  • Fortress: DEF+3, LUX-1

É possível cada tile ter zero, uma ou mais especializações. Propõe uma solução para este problema, composta pelos seguintes elementos:

  • Diagrama UML completo.
  • Código C#, no qual se inclui uma classe Program para testar o código.

Soluções


12 - O teu jogo é shooter de sucesso, e a classe base de todas as armas é IWeapon, cuja interface está definida da seguinte forma:

interface IWeapon
{
    // This property is true if weapon is in alternate firing mode, false
    // otherwise
    bool IsAlternate { get; }

    // Reload the weapon
    void Reload();

    // Shoot the weapon, return true if any rounds left to shoot in current
    // firing mode, false otherwise
    bool Shoot();

    // Switch between main and alternate firing modes
    void SwitchFireMode();
}

Os teus investidores exigem um DLC que inclua melee weapons, ou seja, armas para combate corpo a corpo. Os investidores tomaram a liberdade de adquirir um pacote de assets para esse fim, e querem integra-lo no jogo. A interface das melee weapons é a seguinte:

interface IMelee
{
    // Attacks return true if they hit opponent
    bool AttackFromAbove();
    bool AttackFromBelow();
    bool AttackFromTheLeft();
    bool AttackFromTheRight();
}

Como podes resolver este problema? Escreve código para esse fim e justifica as tuas opções.

Soluções


13 - Cria uma facade para o sistema de input do Unity com um único método que devolve um Vector2. Esta facade deve contemplar input de teclado, rato e controlador, e o Vector2 devolvido deve representar a direção do movimento num plano 2D.

Para testar a facade, cria uma scene contendo um elemento UI.Text que mostre o valor do vetor em cada momento

Soluções


14 - Que princípio de programação orientada a objetos está a ser violado pelo seguinte método? Como podes corrigir a situação?

public static float Average(List<int> values)
{
    float sum = 0f;
    foreach (int n in values)
    {
        sum += n;
    }
    return sum / values.Count;
}

Soluções


15 - No código utilizado na questão anterior está a ser utilizado um pattern que está incorporado de forma nativa na linguagem C#. Que pattern se trata e em que consiste exatamente?

Soluções


16 - Considera a seguinte interface:

public interface IWorld
{
    Vector3 Dimensions { get; }
    IEnumerable<Player> AllPlayers { get; }
    int NumPlayers { get; }
    int NumNPCs { get; }
    int NumTraps { get; }
    float Score { get; }
    GameState State { get; }
    bool IsGameRunning { get; }
    bool IsNaturalDisasterOcurring { get; }
    float GameSpeed { get; set; }

    void AddPlayer(IPlayer player);
    void RemovePlayer(IPlayer player);
    void AddNPC(INPC npc);
    void RemoveNPC(INPC npc);
    void AddTrap(ITrap trap);
    void RemoveTrap(ITrap trap);
    void SetTexture(Vector3 position, Texture texture);
    Texture GetTexture(Vector3 position);
    void StartGame();
    void EndGame();
    void PauseGame();
    void StartNaturalDisaster(IDisaster disaster, float duration);
    void StopAllNaturalDisasters();
    void SaveGame(string fileName);
    void LoadGame(string fileName);

    event EventHandler GameStarted;
    event EventHandler GameFinished;
    event Action<IPlayer> NPCAdded;
    event ActionI<Player> NPCRemoved;
}

Que princípios de design estão a ser violados? Em concreto, quais são as consequências e problemas a que estão sujeitas as classes que implementam esta interface? Como poderias resolver o problema (ao nível das interfaces)?

Soluções


17 - Usando as interfaces nativas do C# para implementação do Observer pattern, nomeadamente IObserver<T> e IObservable<T>, implementa:

  1. Um sujeito/observável que leia teclas do teclado (suprimindo a tecla lida de modo a que não apareça no ecrã) e notifique os observadores cada vez que uma tecla é premida.
  2. Um observador que indique no ecrã a tecla lida (e.g. Detetada tecla 'R').
  3. Um observador que guarde o carácter associado à tecla num ficheiro (em modo append).

O programa deve terminar quando for pressionada a tecla Escape.

Soluções


18 - Resolve o exercício anterior com recurso a eventos.

Soluções


19 - Considera a seguinte classe:

using UnityEngine;

public static class VectorOperations
{
    // Normalized direction between two game objects
    public static Vector2 Direction(Vector2 from, Vector2 to)
    {
        return (to - from) / (to - from).magnitude;
    }

    // Distance between two game objects
    public static float Distance(Vector2 obj1, Vector2 obj2)
    {
        return (obj1 - obj2).magnitude;
    }

    // Convert angle in degrees into normalized vector
    public static Vector2 Deg2Vec(float angle)
    {
        float angleRad = angle * Mathf.Deg2Rad;
        return new Vector2(Mathf.Cos(angleRad), Mathf.Sin(angleRad));
    }

    // Determine angle of vector in degrees
    public static float Vec2Deg(Vector2 vector)
    {
        return Mathf.Atan2(vector.y, vector.x) * Mathf.Rad2Deg;
    }
}

Responde às seguintes questões:

  1. Simplifica os métodos usando notação lambda (=>).
  2. Declara, para cada método, um delegate personalizado compatível.
  3. Indica, para cada método, um delegate pré-definido do C# que seja compatível.
  4. Assumindo que estás num método noutra classe, escreve quatro linhas de código nas quais declaras quatro variáveis do tipo delegate pré-definido que indicaste na alínea anterior, atribuindo-lhes o respetivo método compatível.
  5. Repete a alínea anterior mas considerando os delegates personalizados que declaraste na alínea 2 (e não os delegates pré-definidos do C#).

Soluções


20 - Qual é ou quais são as diferenças entre delegates e eventos no C#?

Soluções


21 - Que design pattern é explicitamente implementado pelos eventos do C#? Explica o teu raciocínio.

Soluções


22 - Considera a seguinte classe C# para uso no Unity:

public class PlayerStats : MonoBehaviour {

    private Player player;

    private void Awake()
    {
        player = GameObject.FindWithTag("Player").GetComponent<Player>();
    }

    private void UpdatePowerUpStats(float powerUpMagnitude)
    {
        // Código que atualiza as estatísticas de power-ups do player
    }
}

Responde às seguintes questões:

  1. Indica um delegate pré-definido do C# compatível com o método UpdatePowerUpStats.
  2. Considera que a classe Player tem um evento nativo do C# chamado PickedUpAPowerUp. Completa a classe PlayerStats de modo a que o método UpdatePowerUpStats seja notificado desse evento quando a instância de PlayerStats estiver ativa.
  3. Responde à questão anterior considerando que o evento PickedUpAPowerUp é do tipo UnityEvent<float>.

Soluções


23 - Cria os seguintes métodos de extensão para a classe string:

  1. Método que conta o número de palavras numa string. Podes usar o método Split() para dividir a string em várias palavras, mas certifica-te que todos os caracteres de whitespace (i.e. espaços, tabs, new lines, etc) são usados como separadores de palavras (estuda a documentação do método de modo para perceberes como). Deves ainda ignorar palavras com comprimento zero.
  2. Método que conte o número de frases numa string. Podes assumir que uma frase termina com um ponto final. Frases de comprimento zero ou só com whitespace não devem ser contadas.
  3. Método que conte o número de parágrafos numa string. Podes assumir que um parágrafo termina com uma nova linha (carácter \n). Parágrafos com comprimento zero ou só com whitespace não devem ser contados.

Para testares os métodos, cria um programa que conte as palavras, frases e parágrafos num ficheiro de texto, cujo nome deve ser passado como argumento na linha de comandos. Neste programa deves apanhar e tratar todas as exceções que possam ser lançadas pelo código de leitura do ficheiro.

Soluções


24 - Cria um programa que apresente as seguintes estatísticas sobre um projeto em C#:

  1. Número de linhas de código
  2. Número de linhas em branco (apenas whitespace)
  3. Número de linhas que são comentários (i.e. linhas que começam com //, ignorando comentários do estilo /* ... */)

Para o efeito deves criar três métodos de extensão apropriados com os nomes IsLineOfCode(), IsBlank() e IsComment(), que devolvem um booleano indicando se a linha (string) é do tipo em questão.

O programa deve receber como argumento da linha de comando uma pasta ou um nome de ficheiro. Se for indicada uma pasta, o programa deve indicar a estatística global para todos os ficheiros .cs na pasta e respetivas sub-pastas. Caso seja indicado um ficheiro, o programa deve certificar-se que o mesmo tem extensão .cs e mostrar as estatísticas apenas para esse ficheiro.

Este programa deve ser estruturado de acordo com as melhores práticas de tratamento de exceções, design de classes e design patterns.

Apresenta também o diagrama UML simples (sem campos e métodos) da solução.

Soluções


25 - Simplifica ao máximo a seguinte struct usando lambdas e eventualmente operadores ternários:

public struct GameMap
{
    private float topScore;
    private int gamesPlayed;
    private int gamesWon;

    public string Name { get; }
    public string Filename { get; }
    public float SuccessRate
    {
        get {
            if (gamesPlayed == 0)
                return 0f;
            else
                return gamesWon / (float) gamesPlayed;
        }
    }
    public float TopScore {
        get
        {
            return topScore;
        }
        set
        {
            if (value > topScore)
            {
                topScore = value;
            }
        }
    }

    public GameMap(string name, string filename)
    {
        Name = name;
        Filename = filename;
        gamesPlayed = 0;
        gamesWon = 0;
        topScore = 0;
    }

    public void GamePlayed(bool won)
    {
        gamesPlayed++;
        if (won)
        {
            gamesWon++;
        }
    }
}

Soluções


26 - Simplifica ao máximo o seguinte código usando lambdas e eventualmente operadores ternários:

public struct Bullet
{
    private float calibre;
    public float Calibre
    {
        get { return calibre; }
        set { if (value < 0.1f) calibre = 0.1f; else calibre = value; }
    }
}
public class Weapon
{
    public float Value { get; }
    public Weapon(float value) { Value = value; }
}
public class Gun : Weapon
{
    private Bullet[] bullets;
    public Gun(float value, int numBullets, float calibre) : base(value)
    {
        bullets = new Bullet[numBullets];
        for (int i = 0; i < numBullets; i++)
        {
            bullets[i] = new Bullet() { Calibre = calibre };
        }
    }
}

Soluções


27 - Simplifica ao máximo o seguinte código usando lambdas e eventualmente operadores ternários:

public struct Passenger
{
    private double weight;
    public double Weight
    {
        get { return weight; }
        set { if (value < 5) weight = 5; else weight = value; }
    }
}
public class Vehicle
{
    public double Value { get; }
    public Vehicle(double value) { Value = value; }
}
public class Car : Vehicle
{
    private Passenger[] passengers;
    public Car(double value, int numPassengers, float avgWeight) : base(value)
    {
        Random r = new Random();
        passengers = new Passenger[numPassengers];
        for (int i = 0; i < numPassengers; i++)
        {
            passengers[i] = new Passenger()
            {
                Weight = avgWeight + r.Next(-10, 10)
            };
        }
    }
}

Soluções


28 - Considera o seguinte código:

public class EventProducer
{
    public string Str { get; private set; }
    public int N { get; private set; }

    protected virtual void OnProducedEvent()
    {
        ProducedEvent?.Invoke(Str, N);
    }
    public event Action<string, int> ProducedEvent;
}

O código anterior funciona perfeitamente, mas tens um patrão chato que exige que uses delegates propositadamente criados para uso em eventos. Assim sendo, substitui o delegate pré-definido Action<T1,T2> pelo delegate pré-definido EventHandler<TEventArgs>, alterando o código em conformidade. Poderá ser necessário criar uma classe adicional.

Soluções


29 - O ficheiro 029.tsv contém dados, separados por tabs, sobre todos os exoplanetas conhecidos (a 30-11-2018). Cada linha contém a seguinte informação:

  • ID do planeta
  • Nome do sistema estelar a que o planeta pertence
  • Letra do planeta dentro do seu sistema estelar
  • Nome do planeta
  • Método de deteção do planeta
  • Período orbital em dias
  • Distância em parsecs
  • Temperatura em Kelvins
  • Telescópio responsável pela descoberta

As linhas que começam com o carácter # são comentários e devem ser ignoradas.

Cria um projeto no Visual Studio que respeite os seguintes requisitos:

  1. Enumeração que contenha os diferentes métodos de descoberta de exoplanetas.
  2. Struct imutável Planet, que contém toda a informação sobre um planeta sob a forma de propriedades só de leitura e um construtor para inicializar essas mesmas propriedades. Cada propriedade deve ser do tipo apropriado ao dado que reporta.
  3. A struct Planet deve ter overrides de GetHashCode() e Equals() de modo a que a igualdade entre planetas dependa apenas do respetivo ID.
  4. Classe PlanetLoader, com um único método LoadPlanets(). Este método aceita um parâmetro o nome de ficheiro que contém os dados sobre os planetas, devolvendo um IEnumerable<Planet> com todos os planetas lidos. Tanto a classe como o método devem ser static.
  5. Classe Program, com método Main(), no qual é invocado o método LoadPlanets(), e posteriormente mostrada no ecrã a seguinte informação, obtida com o uso de Lambdas e LINQ (sintaxe fluente ou expressões query, sem preferência):
    • Número de exoplanetas existentes.
    • Método mais comum de deteção.
    • Máximo de exoplanetas em torno da mesma estrela.
    • Média do período orbital.
    • Planeta mais longínquo (e a respetiva distância).
    • Média da temperatura dos planetas cuja estrela tem pelo menos dois planetas.
    • Telescópio com menos descobertas.
  6. Todas as possíveis exceções devem ser tratadas ao nível da classe Program.
  7. Todas as classes e respetivos membros devem estar documentados com comentários XML.

Soluções


30 - Desenha o diagrama UML completo do problema anterior (não requer resolução do problema).

Soluções


31 - Considera o seguinte método:

public bool HasWeapons(int npcId)
{
    return npcs[npcId].Bag.Any(item => item.Category == ItemCategory.Weapon);
}

Responde às seguintes questões:

  1. Qual é a tua interpretação sobre o que o código está a fazer?
  2. Qual é o tipo da propriedade Category?
  3. Que princípio ou princípios de design estão a ser violados neste código? Que problemas poderão ocorrer se algum dos objetos manipulados for null?
  4. Simplifica o código usando operadores para tratamento de nulls e expressões Lambda.

Soluções


32 - Considera a seguinte enumeração:

enum Monster { Troll, Ogre, Elf, Demon, Vampire, Werewolf, Minion }

Responde às seguintes questões:

  1. Declara uma lista de Monster na qual seja possível também introduzir nulls.
  2. Assume que a variável monst é do tipo Monster. Escreve uma linha de código onde atribuis à variável monst o valor do primeiro elemento da lista da alínea anterior, tendo em conta que se este valor for null, o valor efetivamente a atribuir será Minion.
  3. Escreve o código de um método que receba a lista da primeira alínea e devolva um inteiro indicando quantos nulls existem na lista. O método deve ser o mais compacto possível, fazendo uso Lambdas e LINQ.

Soluções


33 - Considera o tipo int e responde às seguintes questões:

  1. Escreve o código dos seguintes métodos de extensão para int, bem como da classe que os contém:
    • bool IsEven(): Devolve true se inteiro for par, false caso contrário.
    • bool IsZeroOrPositive(): Devolve true se inteiro for zero ou positivo, false caso contrário.
    • bool IsDivisorOf(int otherInt): Devolve true se inteiro for divisor do inteiro passado como argumento, false caso contrário.
  2. Considera o inteiro 8. Como invocarias o método IsDivisorOf(15) nesse valor? E qual seria o resultado?
  3. Considera a variável setOfInts do tipo IEnumerable<int>. Usando expressões Lambda e LINQ, escreve as seguintes expressões:
    • Expressão que resulte no número (quantidade) de inteiros pares existentes em setOfInts.
    • Expressão que resulte num IEnumerable<int> só com divisores de 60.
  4. Qual teria de ser o tipo da variável setOfIntsOrNulls de modo a que possa também conter nulls?
  5. Considerando que a variável setOfIntsorNulls é do tipo que indicaste na alínea anterior, escreve uma expressão, usando expressões Lambda e LINQ, que indique se o enumerável contém ou não nulls.
  6. Escreve o código para percorrer todos os elementos de setOfIntsorNulls e efetuar a operação XOR (^) entre todos os seus valores, guardando o resultado na variável xored, do tipo int. Para o efeito podes assumir que esta variável já existe e foi inicializada a zero. Os nulls devem ser considerados como tendo o valor zero para efeitos de XOR. Usa, sempre que possível, operadores para tratamento de nulls.

Soluções


34 - Considera a seguinte classe C# para uso no Unity:

public class Player : MonoBehaviour {
    // ... outras partes do código ...
    private void OnTriggerEnter(Collider other) {
        if (other.tag == "PowerUp") {
            // Evento lançado aqui
        }
    }
    // ... outras partes do código ...
    public event Action<float> PickedUpAPowerUp;
}

Responde às seguintes questões:

  1. Cria um método cujo único propósito seja lançar o evento PickedUpAPowerUp, seguindo as boas práticas para o efeito. Este método é invocado, por exemplo, na linha de código que diz // Evento lançado aqui (não é preciso mostrar essa invocação).
  2. Identifica o uso de delegates no código apresentado, indicando se os mesmos (caso existam) são ou não nativos do C#.

Soluções


35 - Considera a variável lstStr do tipo List<string>. O tipo List<T> tem vários métodos cujo nome começa com Find, os quais têm como objetivo encontrar elementos na lista que obedeçam a um dado predicado. Este predicado fornece a estratégia de procura na forma de um delegate. Responde às seguintes questões:

  1. Em que design pattern foi inspirada esta forma de procurar elementos numa lista?
  2. Usando o método FindAll(), escreve uma expressão que resulte numa lista contendo apenas as strings que contenham a sub-string "LP2", partindo da lista original lstStr.

Soluções


36 - Resolve a 2ª alínea do exercício anterior usando LINQ.

Soluções


37 - Estás a desenvolver um RPG e tens NPCs com três categorias base que os definem:

  • Movimento
  • Combate
  • Interação (com o jogador)

Podem existir milhares de NPCs diferentes. Para atingir essa diversidade, cada NPC individual pode utilizar diferentes e complexas estratégias que definem as suas três categorias base. Para efeitos deste exercício fica uma lista de alguns dos NPCs que vão aparecer no jogo final, bem como as estratégias que os definem em cada categoria base:

  • B.J. King: movimento solto, combate confinado, interação agressiva
  • J.P. Morgan: movimento solto, combate melee, interação afável
  • M. Faraday: movimento rígido, combate à distância, interação rabugenta
  • J. Austen: movimento gracioso, não combate, interação inteligente
  • A. Frank: movimento gracioso, combate inteligente, interação afável
  • M. Thatcher: movimento rígido, combate confinado, interação agressiva
  • J. Reno: movimento rígido, combate confinado, interação inteligente
  • J. Goodall: movimento solto, combate melee, interação inteligente
  • A. Rand: movimento gracioso, combate confinado, interação rabugenta
  • F. Kahlo: movimento gracioso, não combate, interação rabugenta
  • M.L. King: movimento rígido, combate inteligente, interação inteligente
  • D. Gea: movimento solto, combate à distância, interação afável
  • A. Murray: movimento gracioso, combate confinado, interação agressiva
  • L. Bird: movimento solto, combate à distância, interação rabugenta
  • R. Polanski: movimento rígido, combate inteligente, interação inteligente
  • A. Lavigne: movimento gracioso, não combate, interação afável
  • K. Hammett: movimento solto, combate melee, interação agressiva

Responde às seguintes questões:

  1. Indica, justificando, um design pattern apropriado para implementar este sistema.
  2. Apresenta um diagrama UML com a informação mínima e essencial que demonstre como o design pattern indicado na alínea anterior pode ser utilizado para implementar este sistema.
  3. Apresenta uma implementação muito simples em C# deste sistema baseada na resposta dada nas alíneas anteriores. As "estratégias complexas" devem ser confinadas a simples Console.WriteLine().

Sugestão: Submeter soluções parciais, uma alínea de cada vez, e esperar por feedback antes de responder à alínea seguinte.

Soluções


38 - Estás a desenvolver um jogo em que os passos seguidos por uma arma ao realizar um ataque são sempre os mesmos, nomeadamente:

  1. Decrementar munições (dependendo de quantas foram gastas).
  2. Determinar probabilidade de sucesso.
  3. Determinar estrago infligido no inimigo.
  4. Determinar tempo de cooldown.

Existem diferentes tipos de arma, que apesar de seguirem estes quatro passos durante um ataque, poderão personalizá-los de formas distintas. É expectável que o tempo de cooldown para a maior parte das armas seja zero. Numa primeira versão o jogo terá duas armas, melee e pistol. A arma melee tem tempo de cooldown igual a zero.

Responde às seguintes questões:

  1. Que design pattern te parece mais apropriado para resolver este problema?
  2. Apresenta o diagrama UML de uma possível solução, contendo a informação mínima e necessária que realce o pattern utilizado.
  3. Apresenta uma implementação mínima da solução proposta na alínea anterior, que faça uso de Console.WriteLine() para exemplificar os passos do algoritmo de combate seguido por cada arma.

Soluções


39 - Resolve o problema anterior usando delegates. Em que condições é possível usar delegates para este efeito e qual a vantagem de os usar neste caso.

Soluções


40 - Ainda na sequência dos dois problemas anteriores, considera que as armas podem realizar os passos (ações) indicados anteriormente sem nenhuma obrigatoriedade ou ordem específica, sendo possível inclusive repetir ações. Existem ainda duas ações adicionais possíveis, em concreto:

  1. Determinar estrago na arma.
  2. Determinar estrago infligido no próprio jogador, devido por exemplo a sobreutilização ou backfire da arma.

Responde às seguintes questões:

  1. Que design pattern te parece mais apropriado para resolver este problema?
  2. Apresenta o diagrama UML de uma possível solução, contendo a informação mínima e necessária que realce o pattern utilizado.
  3. Apresenta uma implementação mínima da solução proposta na alínea anterior, que faça uso de Console.WriteLine() para exemplificar os passos do algoritmo de combate seguido por cada arma.

Soluções


41 - Que princípios gerais de programação por objetos é que estão a ser violados no seguinte diagrama UML? Justifica a tua resposta.

UML

Soluções


42 - Que princípios gerais de programação por objetos é que estão a ser violados no seguinte diagrama UML? Justifica a tua resposta.

UML

Soluções


43 - Na questão anterior, em que circunstâncias é que o Decorator pattern poderia oferecer uma boa solução?

Soluções


44 - Na questão 42, em que circunstâncias é que o Strategy pattern poderia oferecer uma boa solução?

Soluções


45 - Compara a documentação dos delegates pré-definidos Action e ThreadStart. Porque razão não é possível utilizar o delegate Action em threads, tendo em conta que ambos os delegates devolvem void e não aceitam parâmetros.

Soluções


46 - Considera a seguinte enumeração:

public enum Quadrilateral { Square, Rectangle, Trapezoid, Rhombus, Kite }

Escreve um programa com duas threads. Uma das threads coloca aleatoriamente e com igual probabilidade itens desta enumeração numa pilha (stack) thread-safe, com intervalos de colocação aleatórios entre 0 a 8 segundos. A outra thread, que pode ser a thread principal, retira todos os itens em intervalos regulares de 10 segundos.

Após a colocação de um item, existe uma probabilidade de 5% da primeira thread decidir terminar o programa. Para o efeito, esta thread coloca null na pilha e finaliza a sua execução. A thread principal, ao capturar este null durante o desempilhamento, também finaliza a sua execução. Atenção que as enumerações são tipos de valor e não aceitam diretamente o valor null.

A visualização do programa deve ser feita em modo de consola, com a consola dividida em duas colunas:

  1. Os itens colocados na pilha pela primeira thread são indicados na primeira coluna, até um máximo de 30 itens. Após a colocação de 30 itens, a coluna deve ser limpa e a indicação dos itens deve recomeçar no início da coluna.
  2. Quando a thread principal retira os itens da pilha, deve indicar os itens retirados na segunda coluna. Antes de começar novo desempilhamento, a thread deve limpar a coluna.

Este programa deve ser estruturado de acordo com as melhores práticas de tratamento de exceções, design de classes e design patterns. Quaisquer variáveis de tipos não thread-safe partilhadas entre threads devem estar guardadas por locks.

Soluções


47 - Implementa um jogo em modo consola, do estilo Space Invaders, no qual o jogador controla uma defesa anti-aérea ^ que dispara um tiro | com frequência máxima de um tiro por segundo. O tiro viaja à velocidade de 1 carácter de consola por cada 150 milissegundos. Deve também existir um alvo O, que é destruído quando é atingido por um tiro, reaparecendo um novo alvo após 1 segundo.

O jogo deve ser implementado com as seguintes threads, que comunicam entre si através de instâncias da fila thread-safe BlockingCollection<T>:

  1. A Thread de input lê do teclado e envia as teclas lidas para uma fila thread-safe. São reconhecidas apenas as teclas Espaço, Seta direita, Seta esquerda e Escape. As restantes teclas são ignoradas. A thread deve terminar a sua execução quando detetar a tecla_Escape_, enviando a mesma para a fila antes de terminar.
  2. A Thread Jogador controla o jogador ^, obtendo as teclas da fila. O jogador pode andar para a esquerda e para a direita e disparar tiros | quando deteta a tecla Espaço. O jogador não pode disparar mais do que um tiro por segundo, devendo ignorar a tecla Espaço durante esse período. Para disparar o tiro, a thread Jogador lança uma nova thread por cada tiro disparado, passando-lhe a posição atual do jogador. A thread Jogador não tem de esperar (fazer join) das threads de disparo, e termina quando detetar a tecla Escape na fila thread-safe.
  3. Cada Thread de disparo é responsável pela renderização e deteção de colisão de um tiro. Cada tiro sobe na vertical à velocidade de 1 caráter de consola por 150 milissegundos. Cada uma destas threads termina quando o respetivo tiro desaparece do ecrã ou quando o mesmo colide com o alvo O. Neste último caso, antes de terminar, a thread deve: a) retirar o alvo destruído do ecrã; b) esperar 1 segundo; e, c) colocar aleatoriamente um novo alvo no ecrã.

Este programa deve ser estruturado de acordo com as melhores práticas de tratamento de exceções, design de classes e design patterns. Quaisquer variáveis de tipos não thread-safe partilhadas entre threads devem estar guardadas por locks.

Soluções


48 - Cria as seguintes classes:

  • Classe AddManager, com uma propriedade só de leitura de nome Total do tipo int (suportada numa variável de instância privada de nome total), e com um método AddToTotal(), que aceita um inteiro e adiciona-o à variável total. Esta classe deve ser um singleton com inicialização lazy, e tanto a sua inicialização como a modificação da variável total (dentro do método AddToTotal()) devem ser thread-safe.
  • Classe Adder, com o método LetsAdd(), que cria e lança uma thread que invoca 1000 vezes o método AddToTotal() da instância solitária de AddManager, passando-lhe inteiros aleatórios entre 0 e 100. A classe tem também a propriedade auto-implementada Partial, na qual a thread guarda o total parcial relativo aos inteiros que somou. O método LetsAdd() retorna a instância da thread criada e lançada.
  • Classe Program, com o método Main(), no qual: a) são criadas 20 instâncias de Adder; b) é invocado o método LetsAdd() em cada delas, sendo mantidas referência às threads devolvidas; c) é feita uma espera (com join) em todas as threads devolvidas; d) é realizada e mostrada no ecrã a soma do Partial de todas as instâncias de Adder; e) é mostrado no ecrã o valor Total da instância solitária de AddManager; e, f) é indicado no ecrã se as somas são iguais ou não.

Nota que, para o programa estar correto, as somas devem ser sempre iguais.

Soluções


49 - Desenha o diagrama UML completo do problema anterior (não é necessário resolução do mesmo).

Soluções


50 - Considera o método com assinatura void DoStuff(). Assumindo que estás noutro método da mesma classe, escreve código para:

  1. Declarar 100 threads que executem o método DoStuff().
  2. Iniciar as 100 threads de modo a que executem em paralelo.
  3. Esperar que a execução dessas mesmas threads termine.

Soluções


51 - Considera a seguinte classe:

class ParallelCalculation
{
    private double result;
    public void AddToResult(double toAdd)
    {
        result += toAdd;
    }
    // Other code
    public event Action Bingo;
}
  1. Modifica a classe de modo a que as operações realizadas pelo método AddToResult() sejam thread-safe.
  2. Considera que pc é uma instância de ParallelCalculation, e que tens um método com assinatura void BingoHandler(). Escreve uma linha de código na qual subscrevas o método BingoHandler() no evento Bingo.
  3. Escreve o código do método OnBingo() da classe ParallelCalculation, usando as melhores práticas de visibilidade, extensão e tratamento de nulls.

Soluções


52 - Considera a seguinte classe:

public class AnnotatedDouble
{
    public double DoubleValue { get; set; }
    public string Annotation { get; set; }

    public override string ToString() => $"{DoubleValue:f2} ({Annotation})";
}

Responde às seguintes questões:

  1. Adiciona uma conversão definida pelo utilizador para converter AnnotatedDouble em double (usando o valor da propriedade DoubleValue).
  2. Adiciona uma conversão definida pelo utilizador para converter AnnotatedDouble em string (usando o valor da propriedade Annotation).
  3. Adiciona uma conversão definida pelo utilizador para converter double em AnnotatedDouble, na qual a propriedade DoubleValue toma o valor do double e a propriedade Annotation é inicializada com uma string vazia "".
  4. Adiciona uma conversão definida pelo utilizador para converter uma string em AnnotatedDouble, na qual o campo Annotation toma o valor dessa string, e o campo double é inicializado a zero.
  5. Apresenta algumas linhas de código que exemplifiquem as conversões definidas nas alíneas anteriores.

Soluções


53 - Considera a classe AnnotatedDouble da questão anterior. Faz overload dos seguintes operadores de modo a que realizem as operações indicadas:

  • Operador + (adição): soma valores na propriedade DoubleValue; concatena strings na propriedade Annotation.
  • Operador - (subtração): subtrai valores na propriedade DoubleValue; remove caracteres da propriedade Annotation do primeiro operando que estejam presentes na propriedade Annotation do segundo operando.
  • Operador - (negação): aplica negação à propriedade DoubleValue, inverte string na propriedade Annotation (e.g. "ola" passa a ser "alo").

Soluções


54 - Relativamente aos operadores implementados na alínea anterior, qual é o output do seguinte código:

ad1 = new AnnotatedDouble() { DoubleValue = -4.2, Annotation = "Negative" };
ad2 = new AnnotatedDouble() { DoubleValue = 9.653, Annotation = "Highest" };
Console.WriteLine(ad1 + ad2);
Console.WriteLine(ad2 + ad1);
Console.WriteLine(ad1 - ad2);
Console.WriteLine(-ad2);

Nota: Não é necessário resolver o problema anterior para responder a esta questão.

Soluções


55 - Indica o principal problema da sobreutilização de overloading de operadores.

Soluções


56 - Responde à questão 52 sem usar conversões definidas pelo utilizador, ou seja, criando métodos To***() e From***(). Quais as vantagens e desvantagens desta abordagem comparada com a abordagem utilizada na questão anterior.

Soluções


57 - Considera o seguinte tipo:

public struct Weird { }

Responde às seguintes questões:

  1. Implementa um indexador só de leitura no tipo Weird que aceite como índice uma variável do tipo object e devolva uma string contendo o resultado da invocação de ToString() na instância de object convertido em maiúsculas. Deves usar notação Lambda para simplificar a resolução do problema.
  2. Considera o seguinte código:
Weird weirdVar;
string str = weirdVar["Hello world!"];
Console.WriteLine(str);
  • O que vai ser impresso no ecrã?
  • Porque é que não foi preciso instanciar weirdVar antes de usarmos o indexador (linha 2)? Teria sido necessário instanciar weirdVar se o tipo Weird fosse uma classe?
  • Na sequência do código apresentado, o que seria impresso no ecrã pela seguinte linha de código: Console.WriteLine(weirdVar[100]);

Soluções


58 - Porque é que não é boa ideia implementar um indexador na forma pedida na questão anterior?

Soluções


59 - Cria uma pequena simulação na qual três agentes se movem aleatoriamente num mundo de dimensão 40x20 toroidal com vizinhança de Moore. Os agentes devem ter as seguintes características:

Agente Carácter Intervalo mov.
1 # 0.2 segundos
2 $ 0.5 segundos
3 @ 1 segundo

A coluna "Intervalo mov." representa o intervalo entre cada movimento de um agente.

A simulação deve ser implementada usando bons princípios de design de classes, e o game loop deve ter update fixo e renderização variável.

Nota: A mesma posição do mundo pode conter mais do que um agente, pelo que a solução deve contemplar este requisito.

Soluções


60 - Os Autómatos Celulares Elementares (ACEs) são sistemas muito simples mas que produzem padrões muito complexos. Estes sistemas podem ser representados com um array de booleanos. Por exemplo, um ACE de tamanho 5 pode ter o seguinte aspeto gráfico:

.*..*

Neste exemplo o caráter . corresponde a false e o caráter * a true. Os ACEs podem ter novas gerações, que dependem da geração anterior e de uma regra de geração. Esta regra define como cada célula da nova geração é criada a partir das suas progenitoras. Mais especificamente, cada célula da nova geração depende da sua progenitora direta bem como das suas duas vizinhas. E como sabemos o estado das vizinhas nas extremidades do array? Não há problema, pois podemos considerar o mundo como toroidal. Por exemplo, o vizinho esquerdo da célula na posição 0 do array é a célula na última posição desse mesmo array.

Existem 256 regras, desde a regra 0 até à regra 255. Para entendermos como a regra funciona, é necessário converter o valor decimal da mesma em binário. Por exemplo, na regra 110, o valor decimal 110 corresponde ao valor binário 01101110. Após esta conversão, a regra 110 funciona da seguinte forma para determinar as gerações seguintes:

Possíveis estados dos progenitores 111 110 101 100 011 010 001 000
Novo estado da célula central segundo a regra 110 0 1 1 0 1 1 1 0

Para o exemplo apresentado, .*..*, a próxima geração será **.**, de acordo com a seguinte análise:

  • Célula 0 tem como progenitores *.* (101), logo o seu novo valor vai ser * (1). Devido ao mundo ser toroidal, o progenitor esquerdo é a última célula do array.
  • Célula 1 tem como progenitores .*. (010), logo o seu valor vai ser * (1).
  • Célula 2 tem como progenitores *.. (100), logo o seu valor vai ser . (0).
  • Célula 3 tem como progenitores ..* (001), logo o seu valor vai ser * (1).
  • Célula 4 tem como progenitores .*. (010), logo o seu valor vai ser * (1). Devido ao mundo ser toroidal, o progenitor direito é a primeira célula do array.

Se deixarmos este ACE correr durante 5 gerações, vai ser impresso no ecrã o seguinte padrão:

.*..*
**.**
.***.
**.*.
*****

Desenvolve um programa que solicite ao utilizador a regra a utilizar (0 a 255), o tamanho do mundo (3 a 100) e o nº de gerações a produzir (3 a 1000), e mostre no ecrã a evolução do respetivo ACE.

A simulação deve ser implementada usando bons princípios de design de classes, fazendo obrigatoriamente uso de um game loop (basta ter passo fixo com sincronização) e de um double buffer (um array para a geração atual e outro para a próxima geração).

Soluções


61 - Implementa a simulação Langton's ant fazendo uso de bons princípios de design de classes e design patterns apropriados ao problema. Na resposta deves indicar que princípios e patterns tiveste o cuidado de utilizar.

Soluções


62 - Descreve, por palavras tuas, três dos principais problemas no uso de singletons. Qual é a principal vantagem no uso deste design pattern?

Soluções


63 - O Factory method pattern é um caso específico de outro pattern. Indica qual e explica por palavras tuas as semelhanças e diferenças.

Soluções


64 - Considera a classe NPC, com três campos:

  • Strength
  • Dexterity
  • Charisma

Existem duas classes concretas de NPC, nomeadamente Orc e Human. Os primeiros têm ainda o campo Endurance e os últimos o campo Wisdom. Podem existir vários NPCs num nível, sendo a especificação de níveis feita num ficheiro de texto semelhante ao seguinte:

NPC: Human
Strength: 78
Dexterity: 51
Charisma: 67
Wisdom: 33

NPC: Human
Strength: 44
Dexterity: 40
Charisma: 75
Wisdom: 71

NPC: Orc
Strength: 91
Dexterity: 83
Charisma: 29
Endurance: 49

Cria um programa que leia um ficheiro semelhante, cujo nome deve ser indicado como 1º argumento na linha de comandos, e instancie os NPCs especificados no ficheiro usando uma simple factory. Os vários NPCs instanciados podem ser indicados no ecrã para fins de debug (sugestão: criem e usem um método ToString() para a classe NPC e respetivas classes derivadas). O programa deve fazer uso de bons princípios de design de classes, design patterns apropriados ao problema, devendo ainda verificar, usando exceções, possíveis situações de erro (ficheiro não especificado, ficheiro não existe, ficheiro com conteúdos inválidos/inesperados, etc).

Soluções


65 - Considera os seguintes tipos:

public enum ScenarioAlphabet { X, Y, A, B }
public class ScenarioObject
{
    public float Weight { get; private set; }
    public string Name { get; private set; }

    private Texture texture; // Texture é um tipo de valor
    private List<string> attributes;
    private ScenarioAlphabet[] secretPassphrase;

    /* ...more code... */

    public ScenarioObject Clone()
    {
        // Código do método
    }
}

Estão a desenvolver um jogo no qual vão ter um número limitado de diferentes tipos de objetos nos cenários. Os diferentes objetos são inicialmente instanciados e guardados num dicionário de tipo Dictionary<string, ScenarioObject>. No jogo propriamente dito, todos os objetos adicionais são criados a partir deste conjunto inicial de objetos. Por exemplo:

Dictionary<string, ScenarioObject> prefabs;

// ...

ScenarioObject aTree = prefabs["Tree"].Clone();

Responde às seguintes questões:

  1. Qual é o design pattern que está aqui a ser utilizado?
  2. Apresenta o código do método Clone() da classe ScenarioObject.

Soluções


66 - Ainda relativamente aos tipos ScenarioAlphabet e ScenarioObject apresentados na alínea anterior, adiciona um método à classe ScenarioObject para conversão explícita de instâncias da mesma em string, tal que a string gerada represente os conteúdos do array secretPassphrase. Por exemplo, se os conteúdos deste array forem { ScenarioAlphabet.A, ScenarioAlphabet.X, ScenarioAlphabet.X }, a string equivalente à instância original de ScenarioObject será "AXX".

Soluções


67 - No problema anterior, porque razão faz mais sentido a conversão ser explícita e não implícita?

E se quisermos converter strings em ScenarioObject, a conversão deve ser explícita ou implícita? Porquê? Com base nesta resposta, adiciona um método à classe ScenarioObject para conversão de strings em ScenarioObject usando a seguinte lógica:

  • Propriedade Weight igual a 1.0f.
  • Propriedade Name igual à string a ser convertida.
  • Variável texture deve ter o seu valor por omissão.
  • Variável attributes deve ser uma lista vazia.
  • Variável secretPassphrase deve ser determinada usando a lógica inversa à que foi utilizada na alínea anterior. Caso a string contenha caracteres que não X, Y, A e B, os mesmos devem ser ignorados.

Soluções


68 - Apresenta duas ou mais linhas de código que exemplifiquem as conversões definidas nas duas alíneas anteriores.

Soluções


69 - Considera o seguinte código:

public abstract class GameCharacter
{
    public int Health { get; protected set; }
    public int Mana { get; protected set; }

    public abstract void Attack();
    public abstract void CastSpell();
}

public class Warrior : GameCharacter
{
    public override void Attack()
    {
        // Specialized attack logic for warriors
    }

    public override void CastSpell()
    {
        // Warriors cannot cast spells, so this method throws an exception
        throw new InvalidOperationException("Warriors cannot cast spells");
    }
}

public class Mage : GameCharacter
{
    public override void Attack()
    {
        // Mages are not good at physical attacks, so this method throws an exception
        throw new InvalidOperationException("Mages cannot use physical attacks");
    }

    public override void CastSpell()
    {
        // Specialized spell casting logic for mages
    }
}

Qual o princípio SOLID que está a ser violado por este código e como poderias resolver o problema?

Soluções


70 - Considera o seguinte código:

public interface IGameCharacter
{
    void Attack();
    void CastSpell();
    void UseItem();
}

public class Warrior : IGameCharacter
{
    public void Attack()
    {
        // Specialized attack logic for warriors
    }

    public void CastSpell()
    {
        // Warriors cannot cast spells, so this method does nothing
    }

    public void UseItem()
    {
        // Specialized item usage logic for warriors
    }
}

public class Mage : IGameCharacter
{
    public void Attack()
    {
        // Mages are not good at physical attacks, so this method does nothing
    }

    public void CastSpell()
    {
        // Specialized spell casting logic for mages
    }

    public void UseItem()
    {
        // Mages do not use items, so this method does nothing
    }
}

Qual o princípio SOLID que está a ser violado por este código e como poderias resolver o problema?

Soluções


71 - Considera um jogo de estratégia em que as nações/fações podem ter diferentes algoritmos de inteligência artificial que controlam os seus seguintes aspetos:

  • Governação: democracia, comunismo, fascismo
  • Militar: agressivo, passivo, defensivo
  • Trocas comerciais: ganancioso, aberto, fechado

Sugere um design pattern apropriado. Justifica a tua resposta e apresenta um diagrama UML de classes (simples) com a tua proposta de solução.

Soluções


72 - O seguinte código é um esqueleto de um Object pool para GameObjects do Unity:

public class ObjectPool : MonoBehaviour
{
    [SerializeField]
    [Range(10, 100)]
    private int poolSize;

    [SerializeField]
    private GameObject prefab;

    private Stack<GameObject> pool;

    private void Start()
    {
        // Initialize pool
    }

    public GameObject GetFromPool()
    {
        // Remove object from pool, set active, return it
    }

    public void ReturnToPool(GameObject gameObject)
    {
        // Deactivate object and return it to the pool
    }
}

Completa os métodos Start(), GetFromPool() e ReturnToPool(), assumindo que os objetos reutilizáveis são todos criados à partida. Caso sejam pedidos mais objetos do que aqueles inicialmente disponíveis, deve ser lançada uma InvalidOperationException.

Soluções


73 - Relativamente ao Object Pool design pattern, descreve por palavras tuas:

  1. As circunstâncias em que é apropriado usá-lo.
  2. O principal benefício de usar o pattern quando as circunstâncias são apropriadas.
  3. O princípio (ou princípios) que é (são) quebrado(s) se usarmos o pattern quando as circunstâncias não o justificam.

Soluções


74 - Considera o seguinte código:

public interface IMonster
{
    // Returns a deep copy of this monster
    IMonster Clone();
}
public interface IMonsterFactory
{
    // Create a new monster of the specified type
    IMonster Create(string monsterType);
}

Responde às seguintes questões:

  1. Apesar de termos ainda pouco código, que design patterns parecem estar envolvidos no mesmo? Justifica a tua resposta.
  2. Cria uma classe MonsterFactory concreta que implementa IMonsterFactory, e que faz uso de um dicionário de monstros prontos a criar (por exemplo, do tipo Dictionary<string, IMonster>). Deve ser lançada a exceção ArgumentException caso o monstro não exista no dicionário.
    • Nota: podes ignorar o construtor e assumir que o dicionário de monstros prontos a usar já foi criado.

Soluções


75 - Na continuação do problema anterior, considera a possível classe concreta Monster:

public class Monster : IMonster
{
    public string Name { get; }
    public float HP { get; private set; }
    public float Shield { get; private set; }
    public Image Mugshot { get; private set; }

    private List<Power> powers;
    private Dictionary<string, int> killsByEnemyName;

    // Returns a deep copy of this monster
    public IMonster Clone()
    {
        // Complete me!
    }
}

Assume que:

  • Image e Power são tipos de referência.
  • Os monstros com o mesmo nome têm sempre a mesma Mugshot durante o jogo.
  • Quando um novo monstro é criado, tem os mesmos Power do monstro original; no entanto, a lista de Power de cada monstro pode variar ao longo do tempo para cada monstro individual, sem afetar a lista de Power dos restantes monstros.
  • Uma instância de Power pode ser modificada ao longo do tempo; no entanto, alterar um Power de um monstro só altera esse Power para esse monstro específico.
  • A classe Power também tem um método Clone() que devolve uma cópia profunda da instância na qual é invocado.
  • O dicionário de kills por nome de inimigo é específico a cada monstro. Quando um novo monstro é criado, começa sem qualquer kill.

Responde às seguintes questões:

  1. Que campo de Monster, tipo de referência, é intrínseco?
  2. Identifica todos campos intrínsecos e extrínsecos da classe Monster e explica qual a diferença.
  3. Completa o método Clone() da classe Monster de modo a que devolva um novo monstro baseado na instância no qual é invocado.

Soluções