-
Notifications
You must be signed in to change notification settings - Fork 0
/
Game.hs
180 lines (157 loc) · 6.7 KB
/
Game.hs
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
{-
File : Game.hs
Contains functions used to progress the game.
-}
module Game
(
GameState(..),
gameLoop,
initState,
gLevel
) where
import Sprite
import Ship
import Enemy
import Bullet
import HighScores
import Graphics.Gloss
import Data.Maybe
import Data.List
import Explosion
import System.Random
data GameState = MainMenu
| Game { gShip::Ship,
gBullets::[Bullet],
gEnemies::[Enemy],
gExp::[Explosion],
gElapsedTime::Float,
gPlayerLives::Int,
gOpenSpots::[Point],
gPlayerScore::Int }
| HighScores Bool
| GameOver Int
{- Initial gameplay state -}
initState :: GameState
initState = Game initShip [] mkEnemies [] 0.0 3 [] 0
{- Progress to harder levels as more enemies are killed -}
gLevel :: GameState -> Float
gLevel (Game _ _ _ _ _ _ _ score) = realToFrac $ score `div` 18
gLevel g = -1
{- Calculate probability of enemy leaving formation based on current level -}
rogueProb :: Float -> Float
rogueProb level = 0.0004 + (0.0001 * level)
{- Calculate probability of rogue enemy firing based on current level -}
fireProb :: Float -> Float
fireProb level = 0.02 + (0.01 * level)
{- Determine whether enemies will go rogue or fire based on calculated probs -}
determineEnemyBehavior :: [Float] -> Float -> ([Bool], [Bool])
determineEnemyBehavior determinants level = (firingStatuses, rogueStatuses)
where
firingStatuses = map (\fd -> fd < fireProb level) determinants
rogueStatuses = map (\fd -> fd < rogueProb level) determinants
{- Game Over if player dies on their last life -}
gameLoop :: Float -> GameState -> IO GameState
gameLoop deltaTime (Game _ _ _ _ _ (-1) _ score) = do
playerGotHighScore <- evaluateScore score
if (playerGotHighScore)
then (return $ HighScores True)
else (return $ GameOver score)
{- Regenerate enemies at the start of each new level -}
gameLoop deltaTime g@(Game ship bullets [] exps et lives spots score) =
return (Game ship bullets mkEnemies exps 0.0 lives spots score)
{- Main gameplay loop -}
gameLoop deltaTime g@(Game ship bullets enemies exps et lives spots score) = do
let g' = updateSprites g deltaTime
let (survivingEnemies, killedEnemies) = evaluateEnemyStates g'
let survivingBullets = evaluateBulletStates g'
let (ship', lives', shipExplosion) = evaluatePlayerState g'
let explosions' = newExplosions g' shipExplosion killedEnemies
{- Random number used to determine whether enemy will go rogue or fire -}
eBehaviorDeterminants <- mapM (\e -> randomRIO (0.0,1.0) :: IO Float)
survivingEnemies
{- Pick path to traverse if the enemy does go rogue -}
pathsForRogues <- mapM (\e -> randomRIO (0,2) :: IO Int) survivingEnemies
let (firingStatuses, rogueStatuses) =
determineEnemyBehavior eBehaviorDeterminants (gLevel g)
let newBullets = catMaybes $ zipWith enemyFire survivingEnemies firingStatuses
let (updatedFormation, newlyOpenSpots) =
deployNewRogues survivingEnemies rogueStatuses pathsForRogues
{- Player scores a point for every enemy killed -}
let score' = score + (length enemies) - (length updatedFormation)
return $ Game
ship'
(survivingBullets ++ newBullets)
updatedFormation
explosions'
(et + deltaTime)
lives'
((gOpenSpots g') ++ newlyOpenSpots)
score'
{- Game loop for static screens -}
gameLoop deltaTime g = return g
{- Move or remove sprites based on their current move -}
updateSprites :: GameState -> Float -> GameState
updateSprites g@(Game {}) dt =
g {
gShip = updateShip dt (gShip g),
gEnemies = fst updatedEnemiesAndSpots,
gOpenSpots = snd updatedEnemiesAndSpots,
gBullets = catMaybes $ updateBullet dt <$> (gBullets g),
gExp = updateExplosion dt <$> (gExp g)
}
where
updatedEnemiesAndSpots =
updateEnemiesAndSpots dt (gElapsedTime g) (gEnemies g) (gOpenSpots g)
{- Updating enemies and spots are closely related (see documentation for
individual functions) -}
updateEnemiesAndSpots ::
Float -> Float -> [Enemy] -> [Point] -> ([Enemy], [Point])
updateEnemiesAndSpots dt et enemies spots = (updatedEnemies, updatedSpots)
where
formationMove = formationDirection et
updatedEnemySprites = updateEnemy dt formationMove <$> enemies
(offscreenEnemies, onscreenEnemies) =
partition (spriteOffScreen . enemySprite) updatedEnemySprites
updatedEnemies =
(onscreenEnemies ++) $ zipWith repositionEnemy offscreenEnemies spots
updatedSpots =
drop (length offscreenEnemies) (updateOpenSpot formationMove <$> spots)
{- Separate surviving enemies from ones that have been killed -}
evaluateEnemyStates :: GameState -> ([Enemy], [Enemy])
evaluateEnemyStates g = (survivingEnemies, killedEnemies)
where
shipBullets = fst $ partitionedBullets (gBullets g)
killedEnemies =
snd $ partition isAlive $ killEnemy <$> shipBullets <*> (gEnemies g)
survivingEnemies =
filter (\e -> not $ enemyInList e killedEnemies) (gEnemies g)
{- Separate surviving bullets from ones that have collided with targets -}
evaluateBulletStates :: GameState -> [Bullet]
evaluateBulletStates g = checkShipBullets ++ checkEnemyBullets
where
(sBullets, eBullets) = partitionedBullets (gBullets g)
{- Ship bullets are destroyed on collision with an enemy -}
sbDangers = enemySprite <$> (gEnemies g)
{- Enemy bullets are destroyed on collision with the ship -}
ebDangers = (shipSprite $ gShip g):[]
detectBulletDeath dangers b =
not . or $ (detectCollision . bulletSprite) b <$> dangers
checkShipBullets = filter (detectBulletDeath sbDangers) sBullets
checkEnemyBullets = filter (detectBulletDeath ebDangers) eBullets
{- Determine if the player died; create explosion and decrement life count
if necessary -}
evaluatePlayerState :: GameState -> (Ship, Int, Maybe Explosion)
evaluatePlayerState g@(Game ship bullets enemies _ _ lives _ _) =
if (detectShipDeath)
then (initShip, lives - 1, Just $ explodeShip ship)
else (ship, lives, Nothing)
where
{- Ships are vulnerable to both enemy bullets and enemy ships -}
sDangers = (bulletSprite <$> bullets) ++ (enemySprite <$> enemies)
detectShipDeath = or $ detectCollision <$> sDangers <*> (shipSprite ship):[]
{- Filter out completed explosions, add new enemy explosions, and explode
the player if they died -}
newExplosions :: GameState -> Maybe Explosion -> [Enemy] -> [Explosion]
newExplosions g sExp kEnemies =
(maybeToList sExp ++) $ filter stillExploding $ (gExp g) ++ newEnemyExplosions
where newEnemyExplosions = (explodeEnemy <$> kEnemies)