/
Turn.hs
277 lines (255 loc) · 10.9 KB
/
Turn.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
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
module Turn where
import Control.Monad
import Control.Monad.State hiding (State)
import Data.Map as M
import qualified Data.Char as Char
import Action
import Actions
import Actor
import Command
import qualified Config
import Display2 hiding (display)
import Keybindings
import qualified Keys as K
import Level
import Monster
import Random
import State
import Strategy
import StrategyState
import Version
-- One turn proceeds through the following functions:
--
-- handle
-- handleMonsters, handleMonster
-- nextMove
-- handle (again)
--
-- OR:
--
-- handle
-- handlePlayer, playerCommand
-- handleMonsters, handleMonster
-- nextMove
-- handle (again)
--
-- What's happening where:
--
-- handle: check for hero's death, HP regeneration, determine who moves next,
-- dispatch to handleMonsters or handlePlayer
--
-- handlePlayer: remember, display, get and process commmand(s),
-- advance player time, update smell map, update perception
--
-- handleMonsters: find monsters that can move or die
--
-- handleMonster: determine and process monster action, advance monster time
--
-- nextMove: advance global game time, monster generation
--
-- This is rather convoluted, and the functions aren't named very aptly, so we
-- should clean this up later. TODO.
-- | Decide if the hero is ready for another move. Dispatch to either 'handleMonsters'
-- or 'handlePlayer'.
handle :: Action ()
handle =
do
debug "handle"
state <- get
pl <- gets splayer
let ptime = mtime (getPlayerBody state) -- time of hero's next move
let time = stime state -- current game time
regenerate pl -- heroes regenerate even if outside the player's turn
debug $ "handle: time check. ptime = " ++ show ptime ++ ", time = " ++ show time
if ptime > time
then do
-- the hero can't make a move yet; monsters first
-- we redraw the map even between player moves so that the movements of fast
-- monsters can be traced on the map; we disable this functionality if the
-- player is currently running, as it would slow down the running process
-- unnecessarily
ifRunning (const $ return True) displayWithoutMessage
handleMonsters
else do
handlePlayer -- it's the hero's turn!
-- | Handle monster moves. Perform moves for individual monsters as long as
-- there are monsters that have a move time which is less than or equal to
-- the current time.
handleMonsters :: Action ()
handleMonsters =
do
debug "handleMonsters"
ms <- gets (lmonsters . slevel)
time <- gets stime
case ms of
[] -> nextMove
(m@(Movable { mtime = mt }) : rest)
| mt > time -> -- no monster is ready for another move
nextMove
| otherwise -> -- monster m should move; we temporarily remove m from the level
-- TODO: removal isn't nice. Actor numbers currently change during
-- a move. This could be cleaned up.
-- Note: however this has a nice side-effect: monsters
-- move in reversed order wrt the previous turn,
-- so there is 2 times less changes of focus
-- (in particular hero selection) in case of two
-- simultaneous battles.
do
modify (updateLevel (updateMonsters (const rest)))
handleMonster m
-- | Handle the move of a single monster.
-- Precondition: monster must not currently be in the monster list of the level.
handleMonster :: Monster -> Action ()
handleMonster m =
do
debug "handleMonster"
state <- get
let time = stime state
let ms = lmonsters (slevel state)
per <- currentPerception
-- run the AI; it currently returns a direction; TODO: it should return an action
dir <- liftIO $ rndToIO $ frequency (head (runStrategy (strategy m state per .| wait)))
let waiting = dir == (0,0)
let nmdir = if waiting then Nothing else Just dir
-- advance time and reinsert monster
let nm = m { mtime = time + mspeed m, mdir = nmdir }
let (act, nms) = insertMonster nm ms
modify (updateLevel (updateMonsters (const nms)))
let actor = AMonster act
try $ -- if the following action aborts, we just continue
if waiting
then
-- monster is not moving, let's try to pick up an object
actorPickupItem actor
else
moveOrAttack True True actor dir
handleMonsters
-- | After everything has been handled for the current game time, we can
-- advance the time. Here is the place to do whatever has to be done for
-- every time unit; currently, that's monster generation.
-- TODO: nextMove may not be a good name. It's part of the problem of the
-- current design that all of the top-level functions directly call each
-- other, rather than being called by a driver function.
nextMove :: Action ()
nextMove =
do
debug "nextMove"
modify (updateTime (+1))
generateMonster
handle
-- | Handle the move of the hero.
handlePlayer :: Action ()
handlePlayer =
do
debug "handlePlayer"
remember -- the hero perceives his (potentially new) surroundings
-- determine perception before running player command, in case monsters
-- have opened doors ...
withPerception playerCommand -- get and process a player command
-- at this point, the command was successful
pl <- gets splayer
advanceTime pl -- TODO: the command handlers should advance the move time
state <- get
let time = stime state
loc = mloc (getPlayerBody state)
smellTimeout = Config.get (sconfig state) "monsters" "smellTimeout"
-- update smell
modify (updateLevel (updateSMap (M.insert loc (time + smellTimeout))))
-- determine player perception and continue with monster moves
withPerception handleMonsters
-- | Determine and process the next player command.
playerCommand :: Action ()
playerCommand =
do
display -- draw the current surroundings
history -- update the message history and reset current message
tryRepeatedlyWith stopRunning $ do -- on abort, just ask for a new command
ifRunning continueRun $ do
k <- session nextCommand
handleKey stdKeybindings k
-- Design thoughts (in order to get rid or partially rid of the somewhat
-- convoluted design we have): We have three kinds of commands.
--
-- Normal commands: they take time, so after handling the command, state changes,
-- time passes and monsters get to move.
--
-- Instant commands: they take no time, and do not change the state.
--
-- Meta commands: they take no time, but may change the state.
--
-- Ideally, they can all be handled via the same (event) interface. We maintain an
-- event queue where we store what has to be handled next. The event queue is a sorted
-- list where every event contains the timestamp when the event occurs. The current game
-- time is equal to the head element of the event queue. Currently, we only have action
-- events. An actor gets to move on an event. The actor is responsible for reinsterting
-- itself in the event queue. Possible new events may include HP regeneration events,
-- monster generation events, or actor death events.
--
-- If an action does not take any time, the actor just reinserts itself with the current
-- time into the event queue. If the insert algorithm makes sure that later events with
-- the same time get precedence, this will work just fine.
--
-- It's important that we decouple issues like HP regeneration from action events if we
-- do it like that, because otherwise, HP regeneration may occur multiple times.
--
-- Given this scheme, we may get orphaned events: a HP regeneration event for a dead
-- monster may be scheduled. Or a move event for a monster suddenly put to sleep. We
-- therefore have to given handlers the option of accessing and cleaning up the event
-- queue.
-- The remaining functions in this module are individual actions or helper
-- functions.
-- TODO: Should be defined in Command module.
helpCommand = Described "display help" displayHelp
-- | Display command help. TODO: Should be defined in Actions module.
displayHelp :: Action ()
displayHelp = messageOverlayConfirm "Basic keys:" helpString >> abort
where
helpString = keyHelp stdKeybindings
heroSelection :: [(K.Key, Command)]
heroSelection =
let heroSelect k = (K.Char (Char.intToDigit k),
Undescribed $
selectHero (AHero k) >> withPerception playerCommand)
in fmap heroSelect [0..9]
stdKeybindings :: Keybindings
stdKeybindings = Keybindings
{ kdir = moveDirCommand,
kudir = runDirCommand,
kother = M.fromList $
heroSelection ++
[ -- interaction with the dungeon
(K.Char 'o', openCommand),
(K.Char 'c', closeCommand),
(K.Char 's', searchCommand),
(K.Char '<', ascendCommand),
(K.Char '>', descendCommand),
(K.Char '*', monsterCommand),
(K.Char '/', floorCommand),
(K.Char ':', floorCommand), -- synonym for backward compat.
(K.Tab , Described "cycle among heroes on level" $ cycleHero >> playerCommand),
-- items
(K.Char ',', pickupCommand),
(K.Char 'd', dropCommand),
(K.Char 'i', inventoryCommand),
(K.Char 'q', drinkCommand),
-- wait
-- (K.Char ' ', waitCommand),
(K.Char '.', waitCommand),
-- saving or ending the game
(K.Char 'S', saveCommand),
(K.Char 'Q', quitCommand),
(K.Esc , cancelCommand),
-- debug modes
(K.Char 'V', Undescribed $ modify toggleVision >> withPerception playerCommand),
(K.Char 'R', Undescribed $ modify toggleSmell >> playerCommand),
(K.Char 'O', Undescribed $ modify toggleOmniscient >> playerCommand),
(K.Char 'T', Undescribed $ modify toggleTerrain >> playerCommand),
(K.Char 'I', Undescribed $ gets (lmeta . slevel) >>= abortWith),
-- information for the player
(K.Char 'v', Undescribed $ abortWith version),
(K.Char 'M', historyCommand),
(K.Char 'D', dumpCommand),
(K.Char '?', helpCommand),
(K.Return , acceptCommand displayHelp)
]
}