3 -- Copyright : (c) 2008 Bertram Felgenhauer
6 -- Maintainer : Bertram Felgenhauer <int-e@gmx.de>
7 -- Stability : experimental
8 -- Portability : portable
10 -- This module is part of Haskell PGMS.
12 -- It provides types and a monad for implementing and running Minesweeper
13 -- strategies. It's the core of PGMS.
16 {-# LANGUAGE GADTs, FlexibleContexts, Rank2Types, GeneralizedNewtypeDeriving #-}
19 -- * Minesweeper configurations
25 -- * Minesweeper boards
31 -- * Minesweeper strategies
41 -- * Running Minesweeper games
48 import Control
.Monad
.Prompt
49 import Control
.Monad
.State
50 import Data
.Array.IArray
51 import Data
.Array.Unboxed
54 -- | A point in 2D space with integer coordinates.
56 -- Used to adress cells on a Minesweeper board, and also to describe board
58 data Pos
= Pos
{ pX
:: Int, pY
:: Int } deriving (Show, Ord
, Eq
, Ix
)
60 -- | A cell on a Minesweeper board.
61 data Cell
= Hidden
-- ^ a hidden cell
62 | Marked
-- ^ a marked cell
63 | Exploded
-- ^ oops, you stepped on a mine here!
64 | Exposed
Int -- ^ an exposed cell with a count of neighbours
67 -- | A view of the Minesweeper board.
68 type View
= Array Pos Cell
70 -- | A complete Minesweeper board, including hidden state.
72 bConfig
:: Config
, -- ^ board size etc.
73 bMines
:: Array Pos
Bool, -- ^ array indicating the position of the mines
74 bView
:: View
, -- ^ current view
75 bTodo
:: Int -- ^ number of mines left to find
78 instance Show Board
where
79 show Board
{ bConfig
= Config
{ cSize
= p
}, bMines
= b
, bView
= v
} =
80 '\n' : unlines [ "|" ++ concat [cell
(Pos x y
) | x
<- [1..pX p
]] ++ " |"
83 cell p | b
! p
= case v
! p
of
87 |
otherwise = case v
! p
of
91 Exposed i
-> ' ' : toEnum (48 + i
) : ""
93 -- | Description of a mine sweeper configuration (or difficulty).
94 data Config
= Config
{
95 cSize
:: Pos
, -- ^ the board size
96 cMines
:: Int -- ^ the number of mines placed on the board
99 -- | Check validity of a config.
101 -- The width and height must be at least 2, and the number of mines must be
102 -- between 1 and the number of cells on the board, minus 1.
103 validConfig
:: Config
-> Bool
104 validConfig Config
{ cSize
= sz
@(Pos sX sY
), cMines
= m
} =
105 sX
>= 2 && sY
>= 2 && m
>= 1 && m
< sX
* sY
107 -- | Default config: 9x9 with 10 mines
109 beginner
= Config
{ cSize
= Pos
9 9, cMines
= 10 }
111 -- | Default config: 16x16 with 40 mines
112 intermediate
:: Config
113 intermediate
= Config
{ cSize
= Pos
16 16, cMines
= 40 }
115 -- | Default config: 30x16 with 99 mines
117 expert
= Config
{ cSize
= Pos
30 16, cMines
= 99 }
119 -- Create a random board according to the given config.
120 mkBoard
:: Config
-> StdGen -> Board
121 mkBoard cfg
@Config
{ cSize
= sz
@(Pos sX sY
), cMines
= m
} gen
122 |
not (validConfig cfg
) = error "invalid mine config"
123 |
otherwise = Board
{
125 bView
= listArray (Pos
1 1, sz
) (repeat Hidden
),
126 bMines
= listArray (Pos
1 1, sz
) (pick gen
(sX
* sY
) m
),
130 pick gen n m | r
<= m
= True : pick gen
' (n
-1) (m
-1)
131 |
otherwise = False : pick gen
' (n
-1) m
133 (r
, gen
') = randomR (1, n
) gen
135 -- | Find the neighbouring cells of a given cell.
137 -- The 'Config' parameter is used to find the boundaries of the board.
138 neighbours
:: Config
-> Pos
-> [Pos
]
139 neighbours Config
{ cSize
= Pos sX sY
} (Pos x y
) =
140 [ Pos
(x
+ dx
) (y
+ dy
)
141 | dx
<- if x
== 1 then [0..1] else if x
== sX
then [-1..0] else [-1..1],
142 dy
<- if y
== 1 then [0..1] else if y
== sY
then [-1..0] else [-1..1],
145 -- count the mines in the neighbourhood of the given cell
146 mines
:: Board
-> Pos
-> Int
147 mines Board
{ bConfig
= cfg
, bMines
= m
} =
148 length . filter (m
!) . neighbours cfg
151 Move
:: Pos
-> Request
Int
152 Mark
:: Pos
-> Request
()
153 GetView
:: Request View
154 GetConfig
:: Request Config
155 TraceMine
:: String -> Request
()
157 -- | The monad for implementing Minesweeper strategies.
158 newtype StrategyM a
= StrategyM
{
159 runStrategyM
:: Prompt Request a
162 -- | Reveal a cell. Returns the number of mines in the neighbourhood.
164 -- Note: Revealing a cell with a mine beneath will lose the game.
165 move
:: Pos
-> StrategyM
Int
166 move
= StrategyM
. prompt
. Move
168 -- | Like 'move', but with no return value.
169 move_
:: Pos
-> StrategyM
()
170 move_
= (>> return ()) . move
174 -- Note: Marking a cell without a mine beneath will lose the game. This is
175 -- a deviation from standard Minesweeper.
176 mark
:: Pos
-> StrategyM
()
177 mark
= StrategyM
. prompt
. Mark
179 -- | Get a view of the current board.
180 getView
:: StrategyM View
181 getView
= StrategyM
(prompt GetView
)
183 -- | Get the current board's config.
185 -- Note: the config will never change throughout a game.
186 getConfig
:: StrategyM Config
187 getConfig
= StrategyM
(prompt GetConfig
)
189 -- | Provide a debug message.
191 -- These will be displayed in the status line in the GUI or on the
192 -- terminal when running the command line version in verbose mode.
193 traceMine
:: String -> StrategyM
()
194 traceMine
= StrategyM
. prompt
. TraceMine
197 data Result a
= Won
-- ^ The game was won.
198 | Unfinished a
-- ^ The strategy implementation finished
199 -- before the game was over.
200 | Lost
-- ^ The game was lost.
203 -- | A strategy with some meta-information.
205 -- It's advisable to define your own strategies in terms of 'defaultStrategy'
206 -- so that future additions to that record don't break your code.
207 data Strategy
= Strategy
{
208 sName
:: String, -- ^ The strategy's name. It should be ASCII
209 -- and not contain spaces.
210 sAuthor
:: String, -- ^ The strategy's author.
211 sDescription
:: String, -- ^ A description of the strategy.
212 sRun
:: StdGen -> StrategyM
String
213 -- ^ The strategy's implementation.
216 -- | Default values for 'Strategy'.
218 -- > myStrategy :: Strategy
219 -- > myStrategy = defaultStrategy {
221 -- > sRun = \_ -> return "I don't want to play anymore, see you!"
223 defaultStrategy
:: Strategy
224 defaultStrategy
= Strategy
{
225 sName
= "<unknown strategy>",
226 sAuthor
= "<unknown author>",
227 sDescription
= "This strategy has no description.",
228 sRun
= \_
-> return "<unimplemented strategy>"
233 -- These are actions for the 'MonadPrompt' monad.
235 -- * 'Start' - A new game just started.
237 -- * 'Update' - A move was made, and the indicated cell changed
239 -- * 'Trace' - The strategy provided a trace message.
242 Start
:: Board
-> Play
() -- (^ A new game just started.
243 Update
:: Pos
-> Board
-> Play
()
244 -- (^ A move was made, and the indicated cell
246 Trace
:: String -> Board
-> Play
()
247 -- (^ The strategy provided a trace message.
249 -- internally, we work in this monad.
250 type PlayM a
= StateT Board
(Prompt Play
) (Result a
)
254 -- The result is a 'Prompt' action, which is suitable for implementing
255 -- a UI that displays the game's progress.
256 playGameP
:: Config
-> StdGen -> StrategyM a
-> Prompt Play
(Result a
, Board
)
257 playGameP cfg gen strategy
= runStateT
(game strategy
) (mkBoard cfg gen
)
259 game
:: StrategyM a
-> PlayM a
261 get
>>= lift
. prompt
. Start
262 runPromptC
(return . Unfinished
) handle
(runStrategyM strategy
)
264 handle
:: Request p
-> (p
-> PlayM a
) -> PlayM a
265 handle GetView cont
= gets bView
>>= cont
266 handle GetConfig cont
= gets bConfig
>>= cont
267 handle
(Move p
) cont
= do
268 b
@Board
{ bMines
= bm
, bView
= bv
, bTodo
= bt
} <- get
269 if bm
! p
then do put b
{ bView
= bv
// [(p
, Exploded
)] }
270 get
>>= lift
. prompt
. Update p
274 _
-> do let n
= mines b p
275 put b
{ bView
= bv
// [(p
, Exposed n
)],
277 get
>>= lift
. prompt
. Update p
278 if bt
== 1 then return Won
else cont n
279 handle
(Mark p
) cont
= do
280 b
@Board
{ bMines
= bm
, bView
= bv
} <- get
281 when (bv
! p
== Hidden
) $ do
282 put b
{ bView
= bv
// [(p
, Marked
)] }
283 get
>>= lift
. prompt
. Update p
288 handle
(TraceMine s
) cont
= get
>>= lift
. prompt
. Trace s
>> cont
()
290 -- | A pure version of 'playGameP'.
291 playGame
:: Config
-> StdGen -> StrategyM a
-> (Result a
, Board
)
292 playGame cfg gen strat
= runPrompt handle
(playGameP cfg gen strat
) where
293 handle
:: Play a
-> a
295 handle Update
{} = ()
296 handle
(Trace s b
) = ()
300 playGame True beginner (mkStdGen 164806687) (mark (Pos 9 1) >> mark (Pos 3 4) >> mark (Pos 5 4) >> mark (Pos 1 5) >> mark (Pos 5 5) >> mark (Pos 9 5) >> mark (Pos 1 8) >> mark (Pos 3 8) >> mark (Pos 8 8) >> mark (Pos 3 9) >> getView >>= \l -> sequence [move p | (p, Hidden) <- assocs l])