LegendaryBananaCat / lp2_2020_prec

Projeto de Recurso de Linguagens de Programação II 2020/2021

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Projeto de Recurso de Linguagens de Programação II 2020/2021

Introdução

Os grupos devem implementar o modelo de simulação Pedra, Papel e Tesoura na forma de duas aplicações C#, uma em consola, outra em Unity.

Este modelo explora um ecossistema de três espécies, pedras (cor azul), papéis (cor verde) e tesouras (cor vermelha), que competem por espaço no mundo de simulação. As interações entre as espécies são baseadas no jogo Pedra, Papel e Tesoura, ou seja, tesoura vence papel, papel vence pedra, e pedra vence tesoura. Os organismos competem com os seus vizinhos, movem-se no mundo e reproduzem-se. Estas interações resultam em padrões de espiral cujo tamanho e estabilidade dependem dos parâmetros da simulação.

O modelo está implementado como exemplo no software NetLogo (aberto e gratuito, também corre diretamente no browser), implementação esta que pode e deve ser usada como termo de comparação ao projeto desenvolvido. A Secção Descrição apresenta o modelo em detalhe.

O jogo deve ser implementado em consola (.NET Core 3.1) e em Unity. A lógica, regras e dados da simulação (o chamado modelo), devem ser completamente independentes tanto da interface de consola (WriteLines, ReadLines), como do Unity (ver Secção MVC pattern). Este modelo deve ser obrigatoriamente o mesmo em ambas as implementações, devendo ser partilhado de uma forma que não implique copiar os ficheiros de um lado para o outro. Existe forma de fazer isto ao nível do Git usando sub-módulos (ver Secção Sugestões para o uso de sub-módulos em Git).

Ambas as versões devem ser multi-plataforma, ou seja, devem funcionar em Linux e macOS, pelo que devem ser evitados métodos e classes que apenas funcionam em Windows.

Os grupos podem ter entre 1 a 3 elementos.

Descrição

O modelo de simulação

A simulação corre numa grelha com dimensões (x, y) toroidal com vizinhança de Von Neumann de raio 1. Toroidal significa que a grelha "dá a volta" na vertical e na horizontal, ou seja, na prática não tem paredes.

Cada célula da grelha pode estar ocupada por uma das três espécies ou estar vazia (neste caso assume a cor de fundo do terminal). A simulação funciona por turnos e em cada turno podem acontecer os seguintes eventos:

  1. Evento de Troca/Movimento:
    • Duas células vizinhas são aleatoriamente escolhidas.
    • O estado de cada uma das células passa a ser o estado da célula vizinha (ou seja, as células trocam de estado).
  2. Evento de Reprodução:
    • Duas células vizinhas são aleatoriamente escolhidas.
    • Se uma das células for vazia passa a ser ocupada por um elemento da espécie na célula vizinha.
    • Nada acontece se nenhuma das células ou ambas as células forem vazias.
  3. Evento de Seleção:
    • Duas células vizinhas são aleatoriamente escolhidas.
    • Os seus ocupantes competem um com o outro segundo as regras do Pedra, Papel e Tesoura.
    • A célula do vizinho perdedor torna-se vazia.
    • Caso alguma das células escolhidas seja vazia, nada acontece.

A simulação tem cinco parâmetros:

  • xdim - Número inteiro que representa a dimensão horizontal da grelha de simulação.
  • ydim - Número inteiro que representa a dimensão vertical da grelha de simulação.
  • swap_rate_exp, número real entre -1.0 e 1.0, que representa a taxa dos eventos de troca/movimento.
  • repr_rate_exp, número real entre -1.0 e 1.0, que representa a taxa dos eventos de reprodução.
  • selc_rate_exp, número real entre -1.0 e 1.0, que representa a taxa dos eventos de seleção.

O número exato de cada tipo de evento em cada turno é determinado aleatoriamente através de uma distribuição de Poisson, que expressa a probabilidade de uma série de eventos ocorrer num certo período de tempo. Uma vez que este é um projeto de programação, não é exigido aos alunos que compreendam a fundo esta distribuição, apenas que consigam implementar um método chamado Poisson() que aceite um valor real λ (média da distribuição) e devolva um número inteiro aleatório obtido através desta distribuição. A secção Gerar inteiros aleatórios a partir da distribuição de Poisson discute possíveis abordagens. Uma possível declaração deste método é a seguinte:

private int Poisson(double lambda);

O número exato de eventos em cada turno é então obtido através do método Poisson(), em que λ é dado por:

double lambda = (xdim * ydim / 3.0) * Math.Pow(10, rate_exp);

A variável rate_exp pode ser swap_rate_exp, repr_rate_exp ou selc_rate_exp, dependendo do tipo de evento em questão. Por exemplo, o número de trocas/movimentos num dado turno pode ser dado por:

// Obter o lambda λ, valor médio das trocas/movimentos
// Notar que este valor é constante ao longo da simulação
double lambdaSwap = (xdim * ydim / 3.0) * Math.Pow(10, swap_rate_exp);

// Obter o número de trocas/movimentos a efetuar no turno atual
int numSwaps = Poisson(lambdaSwap);

Uma vez obtido o número de cada tipo de eventos para o turno atual, os mesmos devem ser individualmente colocados numa lista. Essa lista deve ser então embaralhada (ver secção Embaralhar uma lista ou array), e finalmente percorrida, de modo a que cada evento seja executado. A ideia é que os eventos sejam executados numa ordem aleatória. Por exemplo, se num dado turno serão executadas 3 trocas, 4 reproduções e 2 seleções (valores obtidos aleatoriamente a partir da distribuição de Poisson), a lista com estes eventos deverá ter inicialmente o seguinte conteúdo:

Swap
Swap
Swap
Reproduction
Reproduction
Reproduction
Reproduction
Selection
Selection

Após o embaralhamento a ordem dos conteúdos é randomizada:

Reproduction
Swap
Reproduction
Selection
Reproduction
Selection
Swap
Swap
Reproduction

É nesta fase, após o embaralhamento, que a lista deve ser percorrida, e cada um dos eventos executado para o turno atual.

A visualização deve ser atualizada após todos os eventos de dado turno terem sido executados. A imagem/vídeo em baixo mostra uma implementação de consola em C# com dimensões 300 x 70 e todos os rates_exp colocados a zero.

Rock Paper Scissors

Resumindo, a simulação é executada de acordo com os seguintes passos:

  1. Criar o mundo de simulação, cada célula inicializada aleatoriamente (pedra, papel, tesoura, vazia).
  2. Determinar número de eventos de troca/movimento, reprodução e seleção, a partir da distribuição de Poisson tal como explicado em cima.
  3. Colocar esses eventos numa lista, um a um.
  4. Embaralhar a lista.
  5. Percorrer a lista e executar esses eventos, um a um.
  6. Limpar a lista.
  7. Atualizar visualização.
  8. Se utilizador tiver entretanto dado indicação para terminar a simulação, parar a execução da mesma. Caso contrário voltar para o ponto 2.

Requisitos da versão em consola

O programa de consola deve aceitar os parâmetros da simulação como argumentos da linhas de comandos, como indicado no seguinte exemplo (que assume que o projeto se chama ConsoleApp):

dotnet run -p ConsoleApp -- 100 40 -0.02 0.75 0.00

A opção -- serve para separar entre as opções do comando dotnet e as opções do programa a ser executado, neste caso a nossa simulação. As opções seguintes devem ser dadas por ordem e têm o seguinte significado:

  • 1ª opção: xdim, número inteiro maior ou igual que 2.
  • 2ª opção, ydim, número inteiro maior ou igual que 2.
  • 3ª opção, swap-rate-exp, número real entre -1.0 e 1.0.
  • 4ª opção, repr-rate-exp, número real entre -1.0 e 1.0.
  • 5ª opção, selc-rate-exp, número real entre -1.0 e 1.0.

Caso não seja dado o número correto de opções, ou caso alguma das opções não seja válida, o programa deve terminar com uma mensagem de erro apropriada. Podem existir problemas no parsing dos valores reais devido ao separador decimal ser uma vírgula e não um ponto na língua portuguesa (ver secção Parsing de números reais). Se as opções forem corretas, a simulação começa imediatamente.

A simulação entra em pausa se o utilizador pressionar a barra de espaços, e continua a sua execução se o utilizador carregar na barra de espaços novamente. A simulação termina quando o utilizador pressionar a tecla Escape. É possível verificar se alguma tecla for pressionada através da propriedade Console.KeyAvailable, evitando deste modo que o programa fique preso à espera de uma tecla. Em alternativa, podem implementar um game loop dinâmico tal como dado nas aulas, existindo uma thread separada a capturar as teclas pressionadas (esta solução será valorizada, pois está mais em linha com a matéria de LP2).

Soluções mais eficientes (que executem a simulação mais rapidamente) serão bonificadas na nota. Todas as otimizações implementadas devem ser mencionadas no relatório. Duas sugestões mutuamente exclusivas (i.e., não podem ser usadas em conjunto):

  • Na visualização atualizar apenas as células que foram modificadas num turno e não todas.
  • Atualizar o mundo de uma só vez (com um único Console.Write()), pré-criando uma string com todos os seus conteúdos (isto requer o uso de sequências de escape ANSI, indo além da formatação de cores disponível na classe Console). A forma mais eficiente de "ir construíndo" uma string é através de um StringBuilder, e não com concatenação.

Estas otimizações no projeto de consola só devem ser efetuadas após a simulação estar completamente funcional, e é perfeitamente possível ter nota máxima, ou perto disso, sem a implementação das mesmas.

A aplicação de consola deve funcionar em Windows, macOS e Linux. A melhor estratégia para garantir que assim seja é testar o jogo em Linux (e.g., numa máquina virtual). Algumas instruções incompatíveis com macOS e Linux são, por exemplo:

As instruções que só funcionam em Windows têm a seguinte indicação na sua documentação:

The current operating system is not Windows.

Requisitos da versão Unity

A versão Unity deve ter um UI que permita definir os 5 parâmetros da simulação, bem como botões para Play, Pause, Stop e Quit. Os 5 parâmetros só podem ser alterados quando a simulação estiver no estado Stop, e devem estar limitados ao intervalo {2, 3, ..., 500} para os parâmetros xdim e ydim, e ao intervalo [-1.0, 1.0] para os restantes parâmetros. A aplicação começa no estado Stop, sendo necessário o utilizador clicar no Play para dar início à simulação. Podem adicionar outros elementos ao UI, caso considerem-nos úteis, como por exemplo um slider para controlar a velocidade da simulação. Este UI deve pertencer à própria aplicação, e não ao editor do Unity.

Existem várias formas de criar a animação da simulação, das quais podemos destacar três:

  • Usar uma RawImage e atualizar os píxeis da respetiva textura. É provavelmente a opção mais simples.
  • Usar um TileMap.
  • Usar shaders. De longe a opção mais eficiente, mas provavelmente a mais complexa.

O uso de shaders será bonificado, mas apenas se tiverem sido bem implementados. É perfeitamente possível ter nota máxima usando apenas uma RawImage. Notem que a primeira otimização indicada para o projeto de consola também poderá ser válida para o projeto Unity.

Considerações e sugestões técnicas

MVC pattern

O Model-View-Controller (MVC) design pattern é o ponto de partida para a realização deste projeto. Alguns recursos que podem utilizar para compreender como funciona este pattern:

Sugestões para o uso de sub-módulos em Git

Para fazerem clone de um projeto com sub-módulos devem usar o seguinte comando, exemplificado para o projeto exemplo MVCExample:

git clone --recurse-submodules https://github.com/VideojogosLusofona/MVCExample.git

Caso se tenham esquecido de usar a opção --recurse-submodules, podem executar o seguinte comando na raiz do projeto que obtém os conteúdos dos sub-módulos:

git submodule update --init --recursive

Os sub-módulos estão inicialmente no estado HEAD detached, isto é, não estão em nenhum ramo. Para os sub-módulos ficarem no ramo pretendido, por exemplo o ramo common, basta fazer cd até à pasta de cada sub-módulo e fazer git checkout common (e depois git pull para obter as últimas alterações ou git add/commit/push para criarem commits específicos ao sub-módulo).

O projeto ColorShapeLinks usa esta estratégia.

Como alternativa a um ramo separado, podem usar para o vosso projeto um segundo repositório para conter o código comum.

Gerar inteiros aleatórios a partir da distribuição de Poisson

Um gerador de números aleatórios obtidos a partir da distribuição de Poisson recebe um valor real λ (média dos números aleatórios a devolver) e devolve um número inteiro que corresponde ao número (aleatório) de eventos.

A linguagem C# apenas oferece a classe Random, que produz números aleatórios a partir da distribuição uniforme. O Wikipédia sugere alguns algoritmos para obter valores a partir da distribuição de Poisson tendo como base a distribuição uniforme. No entanto, apenas o segundo, "algorithm poisson random number (Junhao, based on Knuth)", funciona bem com valores elevados de λ, necessários para este projeto. Na parte do algoritmo que diz "Generate uniform random number u in (0,1)", podem usar o método NextDouble() da classe Random para obter valores aleatórios uniformes entre 0 e 1. Todas as variáveis internas deste algoritmo devem ser double, exceto a variável k, que deve ser um int. O parâmetro STEP deve ser uma constante com o valor 500.

Embora seja preferível os alunos implementarem esta função a partir do pseudo-código disponível no Wikipédia, também se aceita o uso de código encontrado na Internet com esta funcionalidade. Nesse caso, deve ser feita referência à fonte.

Embaralhar uma lista ou array

O algoritmo Fisher–Yates é um método de embaralhamento (shuffling) tipicamente utilizado para embaralhar listas ou arrays.

Tal como no caso do gerador de números aleatórios de Poisson, é preferível serem os alunos a implementar este algoritmo a partir do pseudo-código disponível no Wikipédia. No entanto, também se aceita o uso código encontrado na Internet, com a devida referência à fonte.

Parsing de números reais

De modo a converter uma string num número real (neste caso, um double), usa-se tipicamente uma das seguintes abordagens:

// s é uma string, x é um double
x = Convert.ToDouble(s);   // Abordagem 1
x = double.Parse(s);       // Abordagem 2
double.TryParse(s, out x); // Abordagem 3 (preferida)

A última forma é a preferida, pois permite-nos verificar se a conversão foi inválida. No entanto pode ocorrer um problema caso o PC esteja configurado com a língua portuguesa, na qual o separador decimal é uma vírgula e não um ponto. Para evitar este problema, podemos indicar ao C# que pretendemos uma conversão independente da língua configurada no computador, assumindo o ponto como separador decimal:

// Requer using extra no início da classe
using System.Globalization;
//...
// s é uma string, x é um double
x = Convert.ToDouble(s, CultureInfo.InvariantCulture);               // Abordagem 1
x = double.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture); // Abordagem 2
double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out x); // Abordagem 3 (preferida)

Notar que a abordagem com TryParse() é normalmente usada com um if:

if (double.TryParse(...))
{
    // Conversão feita com sucesso
}
else
{
    // Conversão falhou
}

Sugestão sobre como começar a abordar este projeto

Antes de começarem a preocupar-se com a arquitetura do projeto, MVC e submódulos em Git, sugiro que implementem a simulação em consola e/ou Unity tendo como única preocupação ver a simulação a funcionar (não se esqueçam de usar Git logo de início, pois todos os commits contam). Sugiro também que comparem a vossa simulação com o output produzido pelo modelo em NetLogo, de modo a confirmarem que está tudo alinhado e que os resultados sao parecidos.

Quando tiverem confiança que a simulação está bem implementada, tentem separar todo o código específico da simulação (que não contenha código de consola, e.g. Console.QualquerCoisa(), nem código de Unity, e.g. using UnityEngine). Esse código é o modelo (o M em MVC), e deve ser colocado num projeto/pasta/namespace próprio, e posteriormente num ramo (ou repositório) próprio, tal como apresentado no tutorial. Nesta fase podem implementar a outra versão que ainda não implementaram, usando esse código comum do modelo, tal como descrito no tutorial. Pode ser necessário eventualmente fazer algum ajuste ao código comum. Devem garantir que tanto a versão de consola como a versão Unity têm sempre o código comum igual e atualizado.

Objetivos e critério de avaliação

O projeto tem um peso de 10 valores na componente prática de LP2. A nota final do projeto será atribuída segundo os seguintes objetivos:

  • O1 - Programa deve funcionar como especificado. Atenção aos detalhes, pois é fácil desviarem-se das especificações caso não leiam o enunciado com atenção.
  • O2 - Projeto e código bem organizados, nomeadamente:
    • O projeto deve estar devidamente organizado, fazendo uso de classes, structs e/ou enumerações, consoante seja mais apropriado. Cada tipo (i.e., classe, struct ou enumeração) deve ser colocado num ficheiro com o mesmo nome. Por exemplo, uma classe chamada Simulation deve ser colocada no ficheiro Simulation.cs.
    • A escolha da coleções e design patterns deve ser adequada a cada situação. Serão privilegiadas soluções que tenham em consideração bons princípios de design de classes, como é o caso dos princípios SOLID. Estes patterns e princípios devem ser balanceados com o princípio KISS, crucial no desenvolvimento de qualquer aplicação.
    • O código deve ser o mais eficiente possível sem comprometer a arquitetura.
    • O código deve estar devidamente comentado e indentado.
    • Não deve existir código "morto", que não faz nada, como por exemplo variáveis, propriedades ou métodos nunca usados.
    • Projeto compila e executa sem erros e/ou warnings.
  • O3 - Projeto adequadamente documentado com comentários de documentação XML. A documentação gerada em formato HTML em Doxygen ou DocFX, deve estar incluída no zip do projeto, mas não integrada no repositório Git.
  • O4 - Repositório Git deve refletir boa utilização do mesmo, nomeadamente:
    • Devem existir commits de todos os elementos do grupo, commits esses com mensagens que sigam as melhores práticas para o efeito (como indicado aqui, aqui, aqui e aqui).
    • Ficheiros binários não necessários, como por exemplo todos os que são criados nas pastas bin e obj, bem como os ficheiros de configuração do Visual Studio (na pasta .vs ou .vscode), não devem estar no repositório. Ou seja, devem ser ignorados ao nível do ficheiro .gitignore.
    • Assets binários necessários, como é o caso da imagem do diagrama UML, devem ser integrados no repositório em modo Git LFS.
  • O5 - Relatório em formato Markdown (ficheiro README.md), organizado da seguinte forma:
    • Título do projeto.
    • Autoria:
      • Nome dos autores (primeiro e último) e respetivos números de aluno.
      • Informação de quem fez o quê no projeto. Esta informação é obrigatória e deve refletir os commits feitos no Git.
      • Indicação do repositório Git utilizado. Esta indicação é opcional, pois podem preferir manter o repositório privado após a entrega.
    • Arquitetura da solução:
      • Descrição da solução, com breve explicação de como o código foi organizado, bem como dos algoritmos não triviais que tenham sido implementados.
      • Um diagrama UML de classes simples (i.e., sem indicação dos membros da classe) descrevendo a estrutura de classes.
    • Observações e resultados:
      • Indicar o que acontece ao colocar swap-rate-exp a 1.0 deixando as restantes rate-exp a zero.
      • Indicar o que acontece ao colocar swap-rate-exp a -1.0 deixando as restantes rate-exp a zero.
      • É possível encontrar algum conjunto de parâmetros que resulte na extinção de uma das espécies? Quando uma espécie se extingue, o que acontece às outras duas?
    • Referências, incluindo trocas de ideias com colegas, código aberto reutilizado (e.g., do StackOverflow) e bibliotecas de terceiros utilizadas. Devem ser o mais detalhados possível.
    • Nota: o relatório deve ser simples e breve, com informação mínima e suficiente para que seja possível ter uma boa ideia do que foi feito. Atenção aos erros ortográficos e à correta formatação Markdown, pois ambos serão tidos em conta na nota final.

O projeto tem um peso de 10 valores na nota final da disciplina e será avaliado de forma qualitativa. Isto significa que todos os objetivos têm de ser parcialmente ou totalmente cumpridos. A cada objetivo, O1 a O5, será atribuída uma nota entre 0 e 1. A nota do projeto será dada pela seguinte fórmula:

N = 10 x O1 x O2 x O3 x O4 x O5 x D x A

Em que D corresponde à nota da discussão e percentagem equitativa de realização do projeto, também entre 0 e 1. Isto significa que se os alunos ignorarem completamente um dos objetivos, não tenham feito nada no projeto ou não comparecerem na discussão, a nota final será zero.

O termo A representa uma penalização por atrasos na entrega.

Entrega

O projeto deve ser submetido no Moodle até às 23h00 de 14 de julho de 2021. O projeto entregue deve ter os seguintes conteúdos:

  • Todos os ficheiros do projeto incluídos no repositório git.
  • Pasta escondida .git com o repositório Git local do projeto.
  • Documentação HTML ou CHM gerada com Doxygen, DocFX ou ferramenta similar.
  • Ficheiro README.md contendo o relatório do projeto em formato Markdown.
  • Ficheiros de imagens, contendo o diagrama UML de classes e outras figuras que considerem úteis. Estes ficheiros, bem como ficheiros binários da versão Unity, devem ser incluídos no repositório em modo Git LFS.

Atenção: A submissão tem de ter menos de 20 Megabytes e ser efetuada mesmo através do Moodle. Não são aceites links para Google Drive, etc. Desta forma tenham muito cuidado com o .gitignore e o .gitattributes no início do projeto, e tenham em atenção que a versão Unity e de consola podem precisar de ficheiros de configuração Git diferentes (ver código do tutorial).

Honestidade académica

Nesta disciplina, espera-se que cada aluno siga os mais altos padrões de honestidade académica. Isto significa que cada ideia que não seja do aluno deve ser claramente indicada, com devida referência ao respectivo autor. O não cumprimento desta regra constitui plágio.

O plágio inclui a utilização de ideias, código ou conjuntos de soluções de outros alunos ou indivíduos, ou quaisquer outras fontes para além dos textos de apoio à disciplina, sem dar o respectivo crédito a essas fontes. Os alunos são encorajados a discutir os problemas com outros alunos e devem mencionar essa discussão quando submetem os projetos. Essa menção não influenciará a nota. Os alunos não deverão, no entanto, copiar códigos, documentação e relatórios de outros alunos, ou dar os seus próprios códigos, documentação e relatórios a outros em qualquer circunstância. De facto, não devem sequer deixar códigos, documentação e relatórios em computadores de uso partilhado.

Nesta disciplina, a desonestidade académica é considerada fraude, com todas as consequências legais que daí advêm. Qualquer fraude terá como consequência imediata a anulação dos projetos de todos os alunos envolvidos (incluindo os que possibilitaram a ocorrência). Qualquer suspeita de desonestidade académica será relatada aos órgãos superiores da escola para possível instauração de um processo disciplinar. Este poderá resultar em reprovação à disciplina, reprovação de ano ou mesmo suspensão temporária ou definitiva da ULHT.

Texto adaptado da disciplina de Algoritmos e Estruturas de Dados do Instituto Superior Técnico

Referências

Licenças

Este enunciado é disponibilizado através da licença CC BY-NC-SA 4.0.

Metadados

About

Projeto de Recurso de Linguagens de Programação II 2020/2021

License:Other