Programa¸c˜
ao Funcional – Aulas 12 & 13
Sandra Alves
DCC/FCUP
2015/16
O Jogo da Vida
• Um aut´omato celular inventado pelo matem´atico John H. Conway. • O jogo desenrola-se numa grelha bi-dimensional.
• Cada posi¸c˜ao est´a vazia ou tem uma c´elula. • A col´onia de c´elulas evolui por gera¸c˜oes.
• Determinamos uma nova gera¸c˜ao pelas seguintes regras:
1. morrem as c´elulas com menos do que 2 ou mais do que 3 vizinhos; 2. sobrevivem c´elulas com 2 ou 3 vizinhos;
3. nasce uma nova c´elula em cada posi¸c˜ao vazia com exactamente 3 vizinhos. http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
Objetivo
Um programa que:
• simula a passagem de n gera¸c˜oes;
• mostra a sucess˜ao de gera¸c˜oes no terminal.
Baseado na solu¸c˜ao do livro Programming in Haskell de Graham Hutton (cap´ıtulo 9). Representa¸c˜ao do jogo
Vamos representar a col´onia de c´elulas por uma lista de coordenadas:
type Pos = (Int,Int) -- coluna, linha
type Cells = [Pos] -- coordenadas das c´elulas
Exemplo: um glider. glider :: Cells
Para facilitar a visualiza¸c˜ao: • largura e altura limitadas;
width, height :: Int width = 80
height = 24
• lados esquerdo/direito e de topo/baixo s˜ao ligados.
Algumas fun¸c˜oes auxiliares
-- testar se uma posi¸c˜ao est´a viva ou morta -- usa ‘elem’ (do prel´udio-padr˜ao)
isAlive, isEmpty :: Cells -> Pos -> Bool isAlive ps p = elem p ps
isEmpty ps p = not (isAlive ps p) -- obter as 8 posi¸c˜oes vizinhas
neighbs :: Pos -> [Pos]
neighbs (x,y) = map wrap [(x-1,y-1), (x,y-1), (x+1,y-1), (x-1,y),
(x+1,y) , (x-1,y+1), (x,y+1) , (x+1,y+1)] -- garantir que uma posi¸c˜ao est´a dentro do tabuleiro
wrap :: Pos -> Pos
wrap (x,y) = ((x-1) ‘mod‘ width + 1, (y-1) ‘mod‘ height + 1) -- contar c´elulas vivas entre as vizinhas
liveneighbs :: Cells -> Pos -> Int
liveneighbs ps = length . filter (isAlive ps) . neighbs Transi¸c˜ao entre gera¸c˜oes
A nova gera¸c˜ao depende apenas da gera¸c˜ao atual. Assim, vamos definir uma fun¸c˜ao de transi¸c˜ao entre gera¸c˜oes.
As novas celulas s˜ao as sobreviventes mais os nascimentos: nextgen :: Cells -> Cells
nextgen ps = survivors ps ++ births ps
Falta definir duas fun¸c˜oes auxiliares: survivors, births :: Cells -> Cells -- sobreviventes duma gera¸c˜ao
survivors :: Cells -> Cells survivors ps
= [p | p<-ps, elem (liveneighbs ps p) [2,3]] -- nascimentos duma gera¸c˜ao
-- ‘nub’ remove repetidos duma lista births :: Cells -> Cells births ps
= [p | p<-nub (concat (map neighbs ps)), isEmpty ps p,
liveneighbs ps p == 3] Visualiza¸c˜ao
Uma fun¸c˜ao para fazer a anima¸c˜ao de n gera¸c˜oes da col´onia partindo duma configura¸c˜ao inicial. life :: Cells -> Int -> IO ()
life ps n | n>0 = do { cls ; printCells ps ; wait 500 ; life (nextgen ps) (n-1) } | otherwise = return ()
Esta fun¸c˜ao n˜ao devolve um resultado ´util — o objetivo ´e fazer anima¸c˜ao no terminal. Fun¸c˜oes auxiliares de IO:
cls :: IO () -- limpar o terminal printCells :: Cells -> IO () -- mostrar a col´onia
wait :: Int -> IO () -- esperar (ms)
Usamos:
• fun¸c˜ao usleep para esperar (standard POSIX);
• sequˆencias ANSI de controlo do terminal (http://en.wikipedia.org/wiki/ANSI_escape_code) para limpar e posicionar o texto.
Sum´ario
• Uma implementa¸c˜ao simples do jogo do vida de Conway.
• Separa¸c˜ao entre computa¸c˜ao e intera¸c˜ao patente nos tipos de fun¸c˜oes; por ex:
liveneighbs :: Cells -> Pos -> Int -- computa¸c˜ao nextgen :: Cells -> Cells -- computa¸c˜ao printCells :: Cells -> IO () -- visualiza¸c˜ao life :: Cells -> Int -> IO () -- intera¸c˜ao • Facilita a compreens˜ao e extens˜ao do programa.
Extens˜oes
• Ler a configura¸c˜ao inicial da entrada padr˜ao. • Contar o n´umero de c´elulas ao longo das gera¸c˜oes. • Detetar casos especiais:
– todas as c´elulas mortas; – repeti¸c˜oes (naturezas mortas); – ciclos de periodo fixo.
• Melhorar a visualiza¸c˜ao: s´ımbolos Unicode, cores, etc. • Configura¸c˜ao inicial aleat´oria (usando randomRIO)
import System.Random
main = do x<-randomRIO (1,10) -- entre 1 e 10 print x
A biblioteca Gloss
• Para fazer desenhos, anima¸c˜oes, simula¸c˜oes e jogos 2D; • Simples: pensada para ensino de programa¸c˜ao;
• Implementada usando OpenGL e GLUT (mas n˜ao ´e preciso aprender nada disto) • S´ıtio oficial: http://gloss.ouroborus.net/
Primeiro exemplo import Graphics.Gloss
main = display window white ex1
window = InWindow "Gloss" (800,600) (0,0) ex1 = circleSolid 100
Compilar e executar: $ ghc exemplo.hs $ ./exemplo
(Esc ou fechar a janela para sair.)
O exemplo em detalhe import Graphics.Gloss main :: IO ()
main = display window white ex1 — desenhar em fundo branco window :: Display
window = InWindow "Gloss" (800,600) (0,0) — numa janela com 800x600 pixels
ex1 :: Picture
ex1 = circleSolid 100
— um c´ırculo cheio com 100 pixels de raio Figuras
As figuras geom´etricas s˜ao valores de tipo Picture.
A biblioteca gloss exporta muitas fun¸c˜oes para construir figuras; alguns exemplos: circleSolid :: Float -> Picture — c´ırculo dado o raio
rectangleSolid :: Float -> Float -> Picture — rectangulo dada largura, altura line :: Path -> Picture — linha poligonal
polygon :: Path -> Picture — pol´ıgono cheio type Path = [Point] — percurso type Point = (Float,Float) — coordenada x,y
Cores
Podemos mudar a cˆor de uma figura: color :: Color -> Picture -> Picture
As cores usuais est˜ao pr´e-definidas na biblioteca: red, green, blue, yellow, cyan, magenta, ... Sobrepor figuras
Podemos sobrepor v´arias figuras numa s´o: pictures :: [Picture] -> Picture
ex2 = pictures [color red (circleSolid 100),
color white (rectangleSolid 100 50)]
Transla¸c˜oes e rota¸c˜oes
Por omiss˜ao as figuras s˜ao desenhadas na origem (coordenadas (0, 0)). Para desenhar noutro ponto basta fazer uma transla¸c˜ao:
translate :: Float -> Float -> Picture -> Picture — transla¸c˜ao por dx, dy
Tamb´em podemos fazer rota¸c˜oes por um ˆangulo (em graus): rotate :: Float -> Picture -> Picture
ex3 = pictures [translate 100 100 (circleSolid 50), rotate 45 (rectangleSolid 100 50)]
Ampliar ou reduzir
Podemos tamb´em ampliar ou reduzir figuras. scale :: Float -> Float -> Picture -> Picture
— mudar a escala dados factores x, y ex4 = pictures [circleSolid 50,
translate 0 100
(scale 1 0.5 (circleSolid 50))]
Simula¸c˜oes
Tamb´em podemos usar o Gloss para fazer anima¸c˜ao de simula¸c˜oes discretas ao longo do tempo. A fun¸c˜ao simulate da biblioteca faz a anima¸c˜ao; precisamos de lhe passar:
1. um modelo inicial ;
2. uma fun¸c˜ao para converter um modelo numa figura;
3. uma fun¸c˜ao para avan¸car o tempo do modelo por um intervalo ∆t. Exemplo
Simular o movimento de uma bola:
• movimento linear e uniforme (velocidade constante); • sem atrito nem gravidade;
• colis˜oes com os limites duma “caixa” virtual (janela). Modelo
— posi¸c˜ao e velocidade
type Ball = (Point, Vector) — definidos na biblioteca Gloss type Point = (Float,Float) type Vector = (Float,Float)
Fun¸c˜ao de desenho
drawBall :: Ball -> Picture drawBall ((x,y),(dx,dy))
= translate x y (color red (circleSolid ballRadius)) — raio da bola em pixels (constante)
ballRadius :: Float ballRadius = 10
Atualizar posi¸c˜ao e detetar colis˜oes Calculamos a nova posi¸c˜ao:
x0 = x + ∆t × dx y0 = y + ∆t × dy Se uma das coordenadas ultrapassar os limites:
• limitamos a coordenada;
• invertemos a componente correspondente do vector velocidade.
updateBall :: ViewPort -> Float -> Ball -> Ball
updateBall _ dt ((x,y),(dx,dy)) = ((x’,y’), (dx’,dy’)) where (x’,dx’) = clip x dx (maxX-ballRadius)
(y’,dy’) = clip y dy (maxY-ballRadius) clip h dh max
| h’ > max = (max, -dh) | h’ < -max= (-max, -dh)
| otherwise = (h’, dh) where h’ = h + dt*dh — limites da “caixa” virtual
maxX, maxY :: Float maxX = 300
maxY = 300
Programa principal main = do
ball <- randomBall
simulate window black fps ball drawBall updateBall — n´umero de atualiza¸c˜oes por segundo (”frames per second”) fps :: Int
fps = 60
window = InWindow "Gloss Ball" ... — inicializar parˆametros aleatoriamente randomBall :: IO Ball
randomBall = ... Simula¸c˜oes e jogos
Para uma simula¸c˜ao especificamos uma fun¸c˜ao de atualiza¸c˜ao do “estado do mundo” com a passagem de ∆t segundos:
simulate :: ...
-> model — estado inicial -> (model -> Picture) – fun¸c˜ao de desenho -> (ViewPort -> Float -> model -> model)
— fun¸c˜ao de atualiza¸c˜ao -> IO ()
Num jogo, al´em de simular a passagem de tempo, necessitamos de reagir a eventos causados pelo jogador: • pressionar/largar teclas;
• mover o cursor do rato;
• pressionar/largar bot˜oes do rato; • etc.
A fun¸c˜ao play que permite tratar estes eventos. play :: ...
-> world – estado inicial -> (world -> Picture) — desenhar -> (Event -> world -> world) — reagir a eventos -> (Float -> world -> world) — atualiza¸c˜ao -> IO ()
Asteroids
• Um dos primeiros jogos v´ıdeo de arcada (1979)
• O jogador controla uma nave espacial num “mundo” 2D
• Deve disparar sobre os aster´oides e evitar ser atingido pelos fragmentos Vamos usar o Gloss para implementar um jogo deste g´enero.
Objetos em jogo Trˆes tipos:
1. a nave do jogador;
2. os aster´oides (v´arios tamanhos); 3. os lasers (disparados pela nave).
Representamos o mundo do jogo por uma lista de objetos: type World = [Object]
Cada objeto cont´em informa¸c˜ao de forma e de movimento: type Object = (Shape, Movement)
Formas
Representamos as trˆes formas de objetos por um novo tipo com trˆes construtores: data Shape = Asteroid Float — aster´oide (tamanho)
| Laser Float — laser (tempo restante) | Ship — nave do jogador
Os asteroides tˆem um parˆametro que especifica o seu tamanho Os lasers tˆem como parˆametro o tempo restante antes de “decairem”
Desenhar objetos
drawObj :: Object -> Picture
drawObj (shape, ((x,y), _, ang, _))
= translate x y (rotate ang (drawShape shape)) drawShape :: Shape -> Picture
drawShape Ship = color green ship drawShape (Laser _) = color yellow laser drawShape (Asteroid size)
= color red (scale size size asteroid) ship, laser, asteroid :: Picture — figuras b´asicas ...
Desenhar o mundo
Recorde que o “mundo” ´e uma lista de objetos: type World = [Object]
Basta desenhar todos os objetos e combinar as figuras: drawWorld :: World -> Picture
drawWorld objs = pictures (map drawObj objs) Reagir a eventos
Pressionar teclas ← ou →: iniciar rota¸c˜ao da nave Levantar teclas ← ou →: parar rota¸c˜ao da nave Pressionar tecla ↑: acelerar a nave
Pressionar barra de espa¸cos: disparar laser A fun¸c˜ao que reage a eventos ´e:
react :: Event -> World -> World Invariantes:
• O “mundo” ´e uma lista de objetos. • Esta lista nunca ´e vazia.
• O primeiro objeto ´e sempre a nave do jogador.
— iniciar/terminar rota¸c˜ao `a esquerda
react (EventKey (SpecialKey KeyLeft) keystate _ _) (ship:objs)
= (ship’:objs)
where (Ship, (pos, vel, ang, angV)) = ship
angV’ = if keystate==Down then (-180) else 0 ship’ = (Ship, (pos, vel, ang, angV’))
Movimento dos objetos
O movimento de cada objeto ´e caracterizado por: • posi¸c˜ao (x, y)
• velocidade linear (dx, dy) • orienta¸c˜ao ang
• velocidade de rota¸c˜ao angV
Representamos em Haskell por um tuplo: type Movement = (Point, — posi¸c˜ao
Vector, — velocidade linear Float, — orienta¸c˜ao (graus)
Float) — velocidade angular (graus/s) Atualiza¸c˜ao da posi¸c˜ao
• Calcular nova posi¸c˜ao e orienta¸c˜ao ap´os ∆t.
• Movimento limitado a uma “caixa”: −maxW idth ≤ x ≤ maxW idth −maxHeight ≤ y ≤ maxHeight • Se um objeto sair da janela deve re-entrar pelo lado oposto (“wrap around ”)
• Velocidade linear e de rota¸c˜ao s˜ao constantes (at´e o objeto ser destruido) move :: Float -> Movement -> Movement
move dt ((x,y), (dx,dy), ang, angV) = ((x’,y’), (dx,dy), ang’, angV)
where x’ = wrap (x+dt*dx) maxWidth y’ = wrap (y+dt*dy) maxHeight ang’ = ang + dt*angV
wrap h max | h > max = h-2*max | h < -max= h+2*max | otherwise = h
Dete¸c˜ao de colis˜oes
Num jogo de a¸c˜ao ´e ´util detetar colis˜oes entre objetos: 1. entre os lasers e aster´oides;
2. entre a nave e os aster´oides.
Colis˜ao de um ponto
Vamos aproximar o aster´oide por uma “bola” de centro (x0, y0).
Um ponto (x, y) colidiu com o aster´oide se est´a dentro da bola: (x − x0)2+ (y − y0)2≤ r2
— testar uma colis˜ao entre um laser e um aster´oide hits :: Object -> Object -> Bool
hits (Laser _, ((x,y), _, _, _)) (Asteroid sz, ((x’,y’), _, _, _)) = (x-x’)**2 + (y-y’)**2 <= (sz*10)**2 hits _ _ = False
Processar colis˜oes
Para cada aster´oide que ´e atingido por algum laser : 1. partir em fragmentos mais pequenos;
2. remover fragementos demasiados pequenos.
Sobrevivem ap´os cada passo de simula¸c˜ao: 1. a nave do jogador;
2. os fragmentos resultantes de todas as colis˜oes; 3. os aster´oides e lasers n˜ao envolvidos em colis˜oes. collisions :: [Object] -> [Object]
collisions (ship:objs) = ship:(frags ++ objs’ ++ objs’’) where rocks = filter isAsteroid objs
lasers = filter isLaser objs
frags = concat [fragment rock | rock<-rocks, any (‘hits‘rock) lasers] objs’ = [obj | obj<-rocks,
not (any (‘hits‘obj) lasers)] objs’’= [obj | obj<-lasers,
not (any (obj‘hits‘) rocks)]
fragment :: Object -> [Object] – fragmentar um aster´oide ...
Fun¸c˜ao de atualiza¸c˜ao
Dado o intervalo de tempo ∆t: 1. atualizar a posi¸c˜ao cada objeto; 2. remover lasers que tenham “decaido”; 3. processar colis˜oes.
Exprimimos como a composi¸c˜ao de trˆes fun¸c˜oes: updateWorld :: Float -> World -> World
updateWorld dt = collisions . decay dt . map (moveObj dt)
decay :: Float -> World -> World ... %— ver c´odigo
C´odigo (implementa¸c˜ao de Prof. Pedro Vasconcelos:)