-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpacman.hs
More file actions
487 lines (426 loc) · 15.8 KB
/
pacman.hs
File metadata and controls
487 lines (426 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}
{- Pac-Man game
An uploaded version can be reached at:
http://daniel-diaz.github.io/projects/processing/pacman.html
-}
import Graphics.Web.Processing.Mid
import Graphics.Web.Processing.Mid.CustomVar
import Graphics.Web.Processing.Html
import Control.Applicative
import GHC.Generics (Generic)
import Data.Monoid ((<>))
-- | Tile types:
--
-- * 0 - Wall
-- * 1 - Dot
-- * 2 - Energizer
-- * 3 - Empty
-- * 4 - Pac-Man only wall
--
type Tile = Proc_Int
isWall :: Tile -> Proc_Bool
isWall t = t #== 0 #|| t #== 4
type Map = [Tile]
intFromChar :: Char -> Int
intFromChar c = read [c]
main :: IO ()
main = do
map <- (fmap (fmap intFromChar) . lines) <$> readFile "pacman.map"
let rows = length map
cols = length $ head map
map' = concat map
dots = length $ filter (==1) map'
writeHtml "processing.js" "pacman.pde" "Pac-Man!" "pacman.html" $ pacmanScript rows cols dots map'
------------------------------
-- CONFIGURATIONS
framesPerSecond :: Num a => a
framesPerSecond = 35
cellWidth :: Proc_Float
cellWidth = 17
cycleLength :: Num a => a
cycleLength = 6
maxFrame :: Proc_Int
maxFrame = 5
scatterTime :: Proc_Int
scatterTime = 5 * framesPerSecond
chaseTime :: Proc_Int
chaseTime = 20 * framesPerSecond
------------------------------
-- (#==) should be generalized to include this case and more.
(#===) :: (Proc_Eq a, Proc_Eq b) => (a,b) -> (a,b) -> Proc_Bool
(a,b) #=== (c,d) = a #== c #&& b #== d
(<+>) :: Num a => (a,a) -> (a,a) -> (a,a)
(a,b) <+> (c,d) = (a+c,b+d)
distance :: Proc_Point -> Proc_Point -> Proc_Float
distance (a,b) (c,d) = sqrt $ (a-c)^2 + (b-d)^2
colorize :: Color -> EventM Draw ()
colorize c = stroke c >> fill c
-- Directions
type Direction = Proc_Float
up,down,left,right :: Direction
up = 3*pi/2
down = pi/2
left = pi
right = 0
frontDirs :: Direction -> [Direction]
frontDirs d = [d, d + down, d + up]
dirVector :: Direction -> Proc_Point
dirVector d = (cos d, sin d)
type Pos = (Proc_Int,Proc_Int)
dirPos :: Direction -> Pos
dirPos d = (pround $ sin d, pround $ cos d)
data State = State {
-- Pacman
pacmanPos :: Pos
, pacmanDir :: Direction
, pacmanWillDir :: Direction
, pacmanTarget :: Pos
-- Ghosts
, blinkyPos :: Pos
, blinkyDir :: Direction
, blinkyTarget :: Pos
, pinkyPos :: Pos
, pinkyDir :: Direction
, pinkyTarget :: Pos
, inkyPos :: Pos
, inkyDir :: Direction
, inkyTarget :: Pos
, clydePos :: Pos
, clydeDir :: Direction
, clydeTarget :: Pos
-- Move cycle
, moveCycle :: Proc_Int
-- Mode
, chaseMode :: Proc_Bool
, modeTimer :: Proc_Int
-- Game State
, isWon :: Proc_Bool
, isLost :: Proc_Bool
} deriving Generic
instance VarLength State
instance CustomValue State
initialState :: State
initialState = State {
pacmanPos = (23,14)
, pacmanTarget = (23,15)
, pacmanDir = right
, pacmanWillDir = right
, blinkyPos = (15,12)
, blinkyDir = up
, blinkyTarget = blinkyPos initialState <+> dirPos (blinkyDir initialState)
, pinkyPos = (15,13)
, pinkyDir = left
, pinkyTarget = pinkyPos initialState <+> dirPos (pinkyDir initialState)
, inkyPos = (15,14)
, inkyDir = right
, inkyTarget = inkyPos initialState <+> dirPos (inkyDir initialState)
, clydePos = (15,15)
, clydeDir = up
, clydeTarget = clydePos initialState <+> dirPos (clydeDir initialState)
, moveCycle = 0
, chaseMode = false -- start in scatter mode
, modeTimer = scatterTime
, isWon = false
, isLost = false
}
data Ghost = Blinky | Pinky | Inky | Clyde
deriving Eq
allGhosts :: [Ghost]
allGhosts = [Blinky, Pinky, Inky, Clyde]
ghostColor :: Ghost -> Color
ghostColor Blinky = Color 255 0 0 255
ghostColor Pinky = Color 255 184 255 255
ghostColor Inky = Color 0 255 255 255
ghostColor Clyde = Color 255 184 81 255
ghostPos :: Ghost -> State -> Pos
ghostPos Blinky = blinkyPos
ghostPos Pinky = pinkyPos
ghostPos Inky = inkyPos
ghostPos Clyde = clydePos
ghostDir :: Ghost -> State -> Direction
ghostDir Blinky = blinkyDir
ghostDir Pinky = pinkyDir
ghostDir Inky = inkyDir
ghostDir Clyde = clydeDir
ghostTarget :: Ghost -> State -> Pos
ghostTarget Blinky = blinkyTarget
ghostTarget Pinky = pinkyTarget
ghostTarget Inky = inkyTarget
ghostTarget Clyde = clydeTarget
ghostHome :: Ghost -> Pos
ghostHome Blinky = (0,27)
ghostHome Pinky = (0,0)
ghostHome Inky = (30,27)
ghostHome Clyde = (30,-10)
cancelPoint :: Num a => (a,a) -> (a,a)
cancelPoint (x,y) = (-x,-y)
scalePoint :: Num a => a -> (a,a) -> (a,a)
scalePoint k (x,y) = (k*x,k*y)
middlePoint :: Proc_Float -- t <- 0~1
-> Proc_Point -- p <- R2
-> Proc_Point -- q <- R2
-> Proc_Point -- (1-t)p + tq
middlePoint t p q = scalePoint (1-t) p <+> scalePoint t q
drawGhost :: Color -> Proc_Point -> Direction -> EventM Draw ()
drawGhost c p d = do
colorize c
let corner = p <+> ((-cellWidth)/2,cellWidth/2)
bezier corner
(corner <+> (0,-cellWidth))
(corner <+> (cellWidth,-cellWidth))
(corner <+> (cellWidth,0))
let eye1 = p <+> ((-cellWidth)/4,(-cellWidth)/6)
eye2 = p <+> (cellWidth/4,(-cellWidth)/6)
eyer = cellWidth/6
-- Eye 1
colorize $ Color 255 255 255 255
uncurry translate eye1
circle (0,0) eyer
colorize $ Color 0 0 0 255
circle (scalePoint (eyer/2) $ dirVector d) $ eyer/2
uncurry translate $ cancelPoint eye1
-- Eye 2
colorize $ Color 255 255 255 255
uncurry translate eye2
circle (0,0) eyer
colorize $ Color 0 0 0 255
circle (scalePoint (eyer/2) $ dirVector d) $ eyer/2
uncurry translate $ cancelPoint eye2
drawPacman :: Proc_Point -> Direction -> Proc_Int -> EventM Draw ()
drawPacman p d fr = do
colorize $ Color 255 255 0 255
let initAngle = (pi*intToFloat fr)/(4*intToFloat maxFrame)
endAngle = 2*pi - initAngle
arc p cellWidth cellWidth (d+initAngle) (d+endAngle)
cornerOf :: Proc_Int -> Proc_Int -> Proc_Point
cornerOf i j = (intToFloat j * cellWidth, intToFloat i * cellWidth)
centerOf :: Proc_Int -> Proc_Int -> Proc_Point
centerOf i j = cornerOf i j <+> (cellWidth/2, cellWidth/2)
pacmanScript :: Int -- Number of rows
-> Int -- Number of columns
-> Int -- Number of dots
-> [Int] -- Map
-> ProcScript
pacmanScript rows cols dots map = execScriptM $ do
mapv <- newArrayVar $ fmap fromInt map
dotv <- newVar $ fromInt dots -- Remaining dots
stv <- newVarC initialState
framev <- newVar 1 -- Pacman frame
mouthv <- newVar 1 -- Mouth switcher
boolv <- newVar false -- Boolean aux var
let tileIndex :: Num a => a -> a -> a
tileIndex i j = i * fromIntegral cols + j
getTile :: Proc_Int -> Proc_Int -> EventM c Tile
getTile i j = readArrayVar mapv $ tileIndex i j
on Setup $ do
setFrameRate framesPerSecond
on Draw $ do
-- Size
size screenWidth screenHeight
-- Background
background $ Color 0 0 0 255
-- Set center at the center of the screen
translate (intToFloat screenWidth/2) (intToFloat screenHeight/2)
-- Read state
st <- readVarC stv
-- Set bottom-left corner as the origin of coordinates
let (cornerX,cornerY) = ((-cellWidth) * intToFloat (fromInt rows)/2
,(-cellWidth) * (intToFloat (fromInt cols)/2))
translate cornerX cornerY
-- Draw map
let drawAt :: Proc_Int -- Row number
-> Proc_Int -- Col number
-> (Proc_Point -> EventM Draw ()) -- Drawing function
-> EventM Draw ()
drawAt i j f = f $ cornerOf i j
tilesOf :: Int -> [(Proc_Int,Proc_Int)]
tilesOf t = [ (fromInt i,fromInt j)
| i <- [ 0 .. rows - 1 ] , j <- [ 0 .. cols - 1 ]
, map !! tileIndex i j == t ]
twalls = tilesOf 0
tdots = tilesOf 1
tenergizers = tilesOf 2
tgwalls = tilesOf 4
mapM_ (\(i,j) -> drawAt i j drawWall) twalls -- Walls
mapM_ (\(i,j) -> drawAt i j drawGWall) tgwalls -- Ghost walls
mapM_ (\(i,j) -> do t <- getTile i j
when (t #== 1) $ drawAt i j drawDot) tdots -- Dots
mapM_ (\(i,j) -> do t <- getTile i j
when (t #== 2) $ drawAt i j drawEnergizer) tenergizers -- Energizers
-- Draw helper
fill $ Color 255 255 255 255
drawAt (-1) 0 $ \p -> drawtext (fromStText $ "Use Arrow keys to move. Yellow dots will make "
<> "ghosts not follow you for a few seconds.") p 500 200
-- Remove ghost walls after the first scatter time
when (chaseMode st) $ mapM_ (\(i,j) -> writeArrayVar mapv (tileIndex i j) 0) tgwalls
-- Cycle scalar
let cycleScalar = intToFloat (moveCycle st) / cycleLength
-- Draw ghosts
mapM_ (\g -> drawGhost (ghostColor g)
(middlePoint cycleScalar (uncurry centerOf $ ghostPos g st)
(uncurry centerOf $ ghostTarget g st))
(ghostDir g st)) allGhosts
-- Read pacman frame
fr <- readVar framev
mouth <- readVar mouthv
-- Draw pacman
let pacmanR2 = middlePoint cycleScalar (uncurry centerOf $ pacmanPos st)
(uncurry centerOf $ pacmanTarget st)
drawPacman pacmanR2 (pacmanDir st) fr
-- Update pacman frame
ifM (fr #== maxFrame #|| fr #== 0)
(do writeVar mouthv $ negate mouth
writeVar framev $ fr + negate mouth)
(writeVar framev $ fr + mouth)
-- Pacman movement
let (willI,willJ) = pacmanTarget st <+> dirPos (pacmanWillDir st)
willwall <- fmap isWall $ getTile willI willJ
let newDir = if_ (moveCycle st #== cycleLength #&& pnot willwall) (pacmanWillDir st) (pacmanDir st)
(targetI,targetJ) = pacmanTarget st
targetTile <- getTile targetI targetJ
comment "Eating"
when (moveCycle st #== cycleLength)
(do -- Eat dot
when (targetTile #== 1) $ do writeArrayVar mapv (tileIndex targetI targetJ) 3
readVar dotv >>= writeVar dotv . (+ negate 1)
-- Eat energizer
when (targetTile #== 2) $ do writeArrayVar mapv (tileIndex targetI targetJ) 3
)
let (ntargetI,ntargetJ) = pacmanTarget st <+> dirPos newDir
headedToWall <- fmap isWall $ getTile ntargetI ntargetJ
-- Ghosts movement (blinky, pinky, inky, clyde)
[ (bpos,bdir,btar),(ppos,pdir,ptar)
, (ipos,idir,itar),(cpos,cdir,ctar)] <- mapM (
\g -> moveGhost g st getTile
) allGhosts
-- Check if game is lost
let nextToGhost g =
(pacmanPos st #=== ghostTarget g st #&& pacmanDir st #/= ghostDir g st) #||
(pacmanPos st #=== ghostPos g st)
lose = foldr (#||) false $ fmap nextToGhost allGhosts
-- End of the game titles
resetMatrix
translate (intToFloat screenWidth/2) (intToFloat screenHeight/2)
scale 5 5
fill $ Color 255 255 255 255
-- Lost title
when (isLost st) $ drawtext "You have been eaten!" (-65,-5) 500 500
-- Won title
when (isWon st) $ drawtext "You win!" (-28,-5) 500 500
-- Remaining dots
remdots <- readVar dotv
-- Update state
comment "Update state"
when (pnot $ isWon st #|| isLost st) $ writeVarC stv $ st {
pacmanPos = ifC (moveCycle st #== cycleLength)
(pacmanTarget st)
(pacmanPos st)
, pacmanDir = newDir
, pacmanTarget = ifC (moveCycle st #== cycleLength #&& pnot headedToWall)
(ntargetI,ntargetJ)
(pacmanTarget st)
, moveCycle = if_ (moveCycle st #== cycleLength) 0 (moveCycle st + 1)
, blinkyPos = bpos
, blinkyDir = bdir
, blinkyTarget = btar
, pinkyPos = ppos
, pinkyDir = pdir
, pinkyTarget = ptar
, inkyPos = ipos
, inkyDir = idir
, inkyTarget = itar
, clydePos = cpos
, clydeDir = cdir
, clydeTarget = ctar
, chaseMode = if_ (moveCycle st #== cycleLength #&& targetTile #== 2)
false
(if_ (modeTimer st #== 0) (pnot $ chaseMode st) (chaseMode st))
, modeTimer = if_ (moveCycle st #== cycleLength #&& targetTile #== 2)
(2*scatterTime)
(if_ (modeTimer st #== 0)
(if_ (chaseMode st) scatterTime chaseTime)
(modeTimer st - 1))
, isLost = lose
, isWon = remdots #== 0
}
on KeyPressed $ do
st <- readVarC stv
-- Change will dir
matchKey boolv $ ArrowKey UP
bUP <- readVar boolv
matchKey boolv $ ArrowKey DOWN
bDOWN <- readVar boolv
matchKey boolv $ ArrowKey LEFT
bLEFT <- readVar boolv
matchKey boolv $ ArrowKey RIGHT
bRIGHT <- readVar boolv
let will = if_ bUP up
$ if_ bDOWN down
$ if_ bLEFT left
$ if_ bRIGHT right
$ pacmanWillDir st
writeVarC stv $ st { pacmanWillDir = will }
wallColor :: Color
wallColor = Color 0 0 255 255
dotColor :: Color
dotColor = Color 240 240 255 255
energizerColor :: Color
energizerColor = Color 255 255 0 255
ghostWallColor :: Color
ghostWallColor = Color 255 192 203 255
when :: Proc_Bool -> EventM c () -> EventM c ()
when b x = ifM b x $ return ()
drawWall :: Proc_Point -> EventM Draw ()
drawWall p = colorize wallColor >> rect p cellWidth cellWidth
drawGWall :: Proc_Point -> EventM Draw ()
drawGWall p = colorize ghostWallColor >> rect p cellWidth cellWidth
drawDot :: Proc_Point -> EventM Draw ()
drawDot p = colorize dotColor >> circle (p <+> (cellWidth/2,cellWidth/2)) (cellWidth/10)
drawEnergizer :: Proc_Point -> EventM Draw ()
drawEnergizer p = colorize energizerColor >> circle (p <+> (cellWidth/2,cellWidth/2)) (cellWidth/5)
-- GHOST BEHAVIOR (Blinky, Pinky, Inky and Clyde)
ghostWill :: Ghost -> State -> Pos
ghostWill Blinky st = pacmanPos st
ghostWill Pinky st = pacmanPos st <+> scalePoint 4 (dirPos $ pacmanDir st)
ghostWill Inky st =
let pac2 = pacmanPos st <+> scalePoint 2 (dirPos $ pacmanDir st)
in blinkyPos st <+> scalePoint 2 (pac2 <+> cancelPoint (blinkyPos st))
ghostWill Clyde st =
let d = distance (uncurry centerOf $ clydePos st) (uncurry centerOf $ pacmanPos st)
in ifC (d #> 8*cellWidth) (pacmanPos st) (ghostHome Clyde)
closestTo :: Pos -- Current position
-> Pos -- Target position
-> [(Direction,Tile)] -- List of directions to take, and what is in each one
-> (Direction,Tile)
closestTo _ _ [(d,t)] = (d,t)
closestTo p0 pT ((d,t):xs) = ifC (t' #== 0) (d,t) $
ifC (t #== 0) (d',t') $
ifC (d1 #<= d2) (d,t) (d',t')
where
(d',t') = closestTo p0 pT xs
d1 = distance (uncurry centerOf $ p0 <+> dirPos d ) $ uncurry centerOf pT
d2 = distance (uncurry centerOf $ p0 <+> dirPos d') $ uncurry centerOf pT
chooseDir :: Ghost -> State -> [(Direction,Tile)] -> Direction
chooseDir g st xs = fst $ closestTo (ghostPos g st) willPos xs
where
willPos :: Pos
willPos = ifC (chaseMode st) (ghostWill g st) (ghostHome g)
moveGhost :: Ghost -> State
-> (Proc_Int -> Proc_Int -> EventM Draw Tile)
-> EventM Draw (Pos,Direction,Pos)
moveGhost g st getTile = do
let newPos = ifC (moveCycle st #== cycleLength)
(ghostTarget g st)
(ghostPos g st)
-- Direction
let fronts = frontDirs $ ghostDir g st
frontTiles <- mapM (uncurry getTile) $ fmap (\d -> ghostTarget g st <+> dirPos d) fronts
let newDir = if_ (moveCycle st #== cycleLength)
(chooseDir g st $ zip fronts frontTiles)
(ghostDir g st)
-- Target
let newTarget = ifC (moveCycle st #== cycleLength)
(ghostTarget g st <+> dirPos newDir)
(ghostTarget g st)
-- Return
return (newPos,newDir,newTarget)