-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGame.elm
344 lines (284 loc) · 10.4 KB
/
Game.elm
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
module Game (Game, GameStatus(..), Location, CellStatus(..), Action(..), init, update) where
import Array exposing (Array)
import Maybe exposing (andThen)
import Random
import Set exposing (Set)
-- MODEL
{-| Game represents the root state of a game of Minesweeper, it contains the
field, some statistics and the status (won, game-over or still playing).
-}
type alias Game =
{ status : GameStatus
, totalCount : Int
, mineCount : Int
, flagCount : Int
, openCount : Int
, field : Field
}
type GameStatus
= Playing
| GameOver
| GameWon
{-| A two-dimensional array of Cells.
-}
type alias Field =
Array (Array Cell)
{-| A Cell contains all information about a single location in the field.
-}
type alias Cell =
{ mine : Bool
, closeMines : Int
, status : CellStatus
}
type CellStatus
= Closed
| Open
| Flagged
| Exploded
{-| Represents the location of mines in the field as a two dimensional array.
Is used as a intermediate representation before creating the Field.
-}
type alias MineDistribution =
Array (Array Bool)
{-| Coordinates in the Field.
-}
type alias Location =
(Int, Int)
type alias Width = Int
type alias Height = Int
type alias Difficulty = Float
type alias Level = Int
{-| Creates a new game with given width, height and difficulty. The difficulty
should be given between 0 and 1 and represents the ratio of mines in the field.
A difficulty of 0.2 means that 1 in every 5 Cells contains a mine.
The Random.Seed is currently initialised with the level number, which makes our
game predictable and very boring, but easy to test at the moment. :-) We could
use the current time as seed, or allow the user to choose a random "level".
-}
init : Width -> Height -> Difficulty -> Level -> Game
init width height difficulty level =
let seed = Random.initialSeed level
mines = createMineDistribution width height difficulty seed
createRow y = Array.indexedMap (createCell mines y)
field = Array.indexedMap createRow mines
mineCount = mines
|> Array.foldl (Array.filter identity >> Array.length >> (+)) 0
in
Game Playing (width * height) mineCount 0 0 field
{-| Vectors to all neighboring cells.
-}
neighborVectors : List Location
neighborVectors =
cartesianProduct [-1..1] [-1..1]
|> List.filter ((/=) (0, 0))
{-| The coordinates of all neighbors.
-}
neighborLocs : Location -> List Location
neighborLocs =
flip List.map neighborVectors << addTuple
{-| Creates a cell given a mine distribution and a position. To be used
with Array.indexedMap.
-}
createCell : MineDistribution -> Int -> Int -> Bool -> Cell
createCell mineDist y x mine =
let closeMines = neighborLocs (x, y)
|> List.filterMap (getCell mineDist)
|> count identity
in
Cell mine closeMines Closed
{-| Get an element of a 2D Array.
-}
getCell : Array (Array a) -> Location -> Maybe a
getCell field (x, y) =
Array.get y field `andThen` Array.get x
{-| Creates a pseudo-random mine distribution given the Random.Seed.
-}
createMineDistribution : Width -> Height -> Difficulty -> Random.Seed -> MineDistribution
createMineDistribution width height difficulty =
let randomArray length generator =
Random.list length generator |> Random.map Array.fromList
boolGenerator = Random.map ((>) difficulty) (Random.float 0 1)
rowGenerator = randomArray width boolGenerator
fieldGenerator = randomArray height rowGenerator
in
Random.generate fieldGenerator >> fst
-- UPDATE
{-| Currently two actions are supported on a Game:
1. Try to open a Location. If it is Closed it can either open succesfully or
explode. If it is open and has nearby closed Cells it will open those Cells
if the number of Flags imply that it is safe to do so.
2. ToggleFlag on a Location. Will only toggle between Closed and Flagged,
otherwise nothing happens.
-}
type Action
= Try Location
| ToggleFlag Location
{-| Update the game given the Action. Only Games with status Playing will be
processed.
-}
update : Action -> Game -> Game
update action game =
case game.status of
Playing ->
let updatedGame =
case action of
Try loc -> ifValidLoc tryCell loc game
ToggleFlag loc -> ifValidLoc toggleFlag loc game
gameWon = updatedGame.status == Playing &&
updatedGame.openCount + updatedGame.mineCount == updatedGame.totalCount
in
if gameWon then
{ updatedGame | status = GameWon }
else
updatedGame
_ -> game
{-| Lookup the Cell belonging to the Location and run the updater iff the Cell
was found.
-}
ifValidLoc : (Location -> Cell -> Game -> Game) -> Location -> Game -> Game
ifValidLoc updater loc game =
case getCell game.field loc of
Just cell -> updater loc cell game
Nothing -> game
toggleFlag : Location -> Cell -> Game -> Game
toggleFlag loc cell game =
case cell.status of
Closed ->
{ game
| field = updateCellInField (setStatus Flagged) loc game.field
, flagCount = game.flagCount + 1
}
Flagged ->
{ game
| field = updateCellInField (setStatus Closed) loc game.field
, flagCount = game.flagCount - 1
}
_ -> game
tryCell : Location -> Cell -> Game -> Game
tryCell loc cell game =
case cell.status of
-- Closed cells need to be opened.
Closed -> openCell loc cell game
-- Clicking on an open cell with neighboring closed cells will open
-- the neighbors when possible.
Open -> openNeighbors loc cell game
_ -> game
openCell : Location -> Cell -> Game -> Game
openCell loc cell game =
if cell.mine then
-- BOOM!
{ game
| field = updateCellInField (setStatus Exploded) loc game.field
, status = GameOver
}
else if cell.closeMines > 0 then
-- This is simple, just open this cell and show the number of mines that
-- are close.
{ game
| field = updateCellInField (setStatus Open) loc game.field
, openCount = game.openCount + 1
}
else
-- This is harder, we are in "open field" and need to open all cells we can
-- find in this "open field".
openAll game <| findCellsToOpen game loc
openAll : Game -> Set Location -> Game
openAll game locs =
{ game
| field = updateCellsInField (setStatus Open) locs game.field
, openCount = game.openCount + (Set.size locs)
}
openNeighbors : Location -> Cell -> Game -> Game
openNeighbors loc cell =
if cell.closeMines == 0 then identity else openNeighbors' loc cell
openNeighbors' : Location -> Cell -> Game -> Game
openNeighbors' loc cell game =
let neighbors = neighborLocs loc
|> lookupCells game.field
closedCells = List.filter (snd >> .status >> (==) Closed) neighbors
flags = count (snd >> .status >> (==) Flagged) neighbors
explodingCells = neighbors
|> List.filter (\(_, cell) -> cell.mine && cell.status /= Flagged)
|> List.map fst
|> Set.fromList
in
if List.length closedCells == 0 || flags /= cell.closeMines then
-- Protection against premature clicks... if the number of flags don't
-- match the number of mines -> do nothing.
game
else if Set.size explodingCells > 0 then
{ game
| field = updateCellsInField (setStatus Exploded) explodingCells game.field
, status = GameOver
}
else
closedCells
|> List.map fst
|> List.concatMap (findCellsToOpen game >> Set.toList)
|> Set.fromList
|> openAll game
{-| Kicks-off the recursive search for the complete field starting with the
given Location.
-}
findCellsToOpen : Game -> Location -> Set Location
findCellsToOpen game loc =
findCellsToOpen' game.field (Set.singleton loc) Set.empty
{-
I'm not so sure about this function. It works well, also with fields of size
100x100, but it is really a while loop disguised as recursion. I got the
feeling there is a more functional approach to this that doesn't impact
performance. Anyone any idea?
-}
findCellsToOpen' : Field -> Set Location -> Set Location -> Set Location
findCellsToOpen' field locsToTest locsToOpen =
let newLocsToOpen = locsToTest
|> Set.toList
|> lookupCells field
|> List.filter (snd >> .status >> (==) Closed)
-- Using flip Set.union instead of Set.union because it turns out the
-- performance is much better when adding a small set to a larger one
-- than the other way around.
updatedLocsToOpen = List.map fst newLocsToOpen
|> Set.fromList
|> flip Set.union locsToOpen
updatedLocsToTest = newLocsToOpen
|> List.filter (snd >> .closeMines >> (==) 0)
|> List.concatMap (fst >> neighborLocs)
|> Set.fromList
|> flip Set.diff updatedLocsToOpen
in
if Set.size updatedLocsToTest == 0 then
updatedLocsToOpen
else
findCellsToOpen' field updatedLocsToTest updatedLocsToOpen
updateCellInField : (Cell -> Cell) -> Location -> Field -> Field
updateCellInField cellUpdater =
uncurry (arrayUpdate cellUpdater >> arrayUpdate)
updateCellsInField : (Cell -> Cell) -> Set Location -> Field -> Field
updateCellsInField cellUpdater locs =
Array.indexedMap (\y ->
Array.indexedMap (\x ->
if Set.member (x, y) locs then cellUpdater else identity
)
)
-- UTILS
setStatus status record =
{ record | status = status }
addTuple (a, b) (a', b') =
(a + a', b + b')
cartesianProduct a b =
a |> List.map (,) |> List.concatMap (flip List.map b)
{-| Counts the number of locations in the 2D array that satisfy the given Bool
function.
-}
count : (a -> Bool) -> List a -> Int
count f =
List.foldl (f >> (\b -> if b then 1 else 0) >> (+)) 0
lookupCells : Array (Array a) -> List Location -> List (Location, a)
lookupCells field =
List.filterMap (\loc -> getCell field loc |> Maybe.map ((,) loc))
arrayUpdate : (a -> a) -> Int -> Array a -> Array a
arrayUpdate updater i array =
case Array.get i array of
Nothing -> array
Just v -> Array.set i (updater v) array