1 -- This file is part of Intricacy
2 -- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
4 -- This program is free software: you can redistribute it and/or modify
5 -- it under the terms of version 3 of the GNU General Public License as
6 -- published by the Free Software Foundation, or any later version.
8 -- You should have received a copy of the GNU General Public License
9 -- along with this program. If not, see http://www.gnu.org/licenses/.
11 module MainState
where
13 import Control
.Monad
.State
14 import Control
.Applicative
15 import qualified Data
.Vector
as Vector
16 import qualified Data
.Map
as Map
18 import Control
.Monad
.Writer
19 import Control
.Monad
.Trans
.Maybe
23 import Control
.Concurrent
.STM
24 import Control
.Concurrent
25 import System
.Directory
26 import System
.FilePath
27 import Data
.Time
.Clock
29 import Data
.Function
(on
)
48 class (Applicative m
, MonadIO m
) => UIMonad m
where
52 drawMainState
:: MainStateT m
()
53 reportAlerts
:: GameState
-> [Alert
] -> m
()
54 drawMessage
:: String -> m
()
55 drawPrompt
:: Bool -> String -> m
()
57 drawError
:: String -> m
()
58 showHelp
:: InputMode
-> HelpPage
-> m
Bool
59 getInput
:: InputMode
-> m
[ Command
]
60 getChRaw
:: m
( Maybe Char )
61 unblockInput
:: m
(IO ())
62 setUIBinding
:: InputMode
-> Command
-> Char -> m
()
63 getUIBinding
:: InputMode
-> Command
-> m
String
64 impatience
:: Int -> m
Bool
65 toggleColourMode
:: m
()
66 warpPointer
:: HexPos
-> m
()
67 getUIMousePos
:: m
(Maybe HexPos
)
69 onNewMode
:: InputMode
-> m
()
70 withNoBG
:: m
() -> m
()
71 suspend
,redraw
:: m
()
73 doUI
:: m a
-> IO (Maybe a
)
76 if ok
then m
>>= (endUI
>>).return.Just
else return Nothing
78 -- | this could be neatened using GADTs
81 { psCurrentState
::GameState
83 , psLastAlerts
::[Alert
]
84 , wrenchSelected
::Bool
86 , psGameStateMoveStack
::[(GameState
, PlayerMove
)]
87 , psUndoneStack
::[(GameState
, PlayerMove
)]
88 , psTitle
::Maybe String
92 , psMarks
::Map
Char MainState
95 { rsCurrentState
::GameState
96 , rsLastAlerts
::[Alert
]
97 , rsMoveStack
::[PlayerMove
]
98 , rsGameStateMoveStack
::[(GameState
, PlayerMove
)]
99 , rsTitle
::Maybe String
100 , rsMarks
::Map
Char MainState
103 { esGameStateStack
::[GameState
]
104 , esUndoneStack
::[GameState
]
106 , esPath
::Maybe FilePath
107 , esTested
::Maybe (GameState
,Solution
)
108 , lastSavedState
::Maybe (GameState
, Bool)
109 , selectedPiece
::Maybe PieceIdx
110 , selectedPos
::HexPos
112 , esMarks
::Map
Char GameState
115 { curServer
:: ServerAddr
116 , undeclareds
:: [Undeclared
]
117 , partialSolutions
:: PartialSolutions
118 , tutProgress
:: TutProgress
120 , curAuth
:: Maybe Auth
121 , codenameStack
:: [Codename
]
122 , newAsync
:: TVar
Bool
123 , asyncCount
:: TVar
Int
124 , asyncError
:: TVar
(Maybe String)
125 , asyncInvalidate
:: TVar
(Maybe Codenames
)
126 , randomCodenames
:: TVar
[Codename
]
127 , userInfoTVs
:: Map Codename
(TVar FetchedRecord
, UTCTime
)
128 , indexedLocks
:: Map LockSpec
(TVar FetchedRecord
)
129 , retiredLocks
:: Maybe [LockSpec
]
130 , curLockPath
:: FilePath
131 , curLock
:: Maybe (Lock
,Maybe Solution
)
135 type MainStateT
= StateT MainState
137 data HelpPage
= HelpPageInput | HelpPageGame | HelpPageInitiated | HelpPageFirstEdit
138 deriving (Eq
, Ord
, Show, Enum
)
140 ms2im
:: MainState
-> InputMode
141 ms2im mainSt
= case mainSt
of
142 PlayState
{} -> IMPlay
143 ReplayState
{} -> IMReplay
144 EditState
{} -> IMEdit
145 MetaState
{} -> IMMeta
147 newPlayState
(frame
,st
) title isTut sub saved
= PlayState st frame
[] False False [] [] title isTut sub saved Map
.empty
148 newReplayState st soln title
= ReplayState st
[] soln
[] title Map
.empty
149 newEditState
(frame
,st
) msoln mpath
= EditState
[st
] [] frame mpath
150 ((\s
->(st
,s
))<$>msoln
) (Just
(st
, isJust msoln
)) Nothing
(PHS zero
) (PHS zero
) Map
.empty
152 flag
<- atomically
$ newTVar
False
153 errtvar
<- atomically
$ newTVar Nothing
154 invaltvar
<- atomically
$ newTVar Nothing
155 rnamestvar
<- atomically
$ newTVar
[]
156 counttvar
<- atomically
$ newTVar
0
157 (saddr
', auth
, path
) <- confFilePath
"metagame.conf" >>=
158 liftM (fromMaybe (defaultServerAddr
, Nothing
, "")) . readReadFile
159 let saddr
= updateDefaultSAddr saddr
'
160 let names
= maybeToList $ authUser
<$> auth
161 (undecls
,partials
,tut
) <- readServerSolns saddr
162 mlock
<- fullLockPath path
>>= readLock
163 return $ MetaState saddr undecls partials tut
False auth names flag counttvar errtvar invaltvar rnamestvar Map
.empty Map
.empty Nothing path mlock
0
165 type PartialSolutions
= Map LockSpec SavedPlayState
166 type TutProgress
= (Int,Maybe SavedPlayState
)
167 data SavedPlayState
= SavedPlayState
[PlayerMove
] (Map
Char [PlayerMove
])
168 deriving (Eq
, Ord
, Show, Read)
170 savePlayState
:: MainState
-> SavedPlayState
171 savePlayState ps
= SavedPlayState
(getMoves ps
) $ Map
.map getMoves
$ psMarks ps
172 where getMoves
= reverse . map snd . psGameStateMoveStack
174 restorePlayState
:: SavedPlayState
-> Lock
-> (Maybe String) -> Bool -> Bool -> Bool -> MainState
175 restorePlayState
(SavedPlayState pms markPMs
) (frame
,st
) title isTut sub saved
=
176 (stateAfterMoves pms
) { psMarks
= Map
.map stateAfterMoves markPMs
}
178 stateAfterMoves pms
= let (stack
,st
') = applyMoves st pms
179 in (newPlayState
(frame
, st
') title isTut sub saved
) { psGameStateMoveStack
= stack
}
180 applyMoves st pms
= foldl tick
([],st
) pms
181 tick
:: ([(GameState
,PlayerMove
)],GameState
) -> PlayerMove
-> ([(GameState
,PlayerMove
)],GameState
)
182 tick
(stack
,st
) pm
= ((st
,pm
):stack
,fst . runWriter
$ physicsTick pm st
)
184 readServerSolns
:: ServerAddr
-> IO ([Undeclared
],PartialSolutions
,TutProgress
)
185 readServerSolns saddr
= if nullSaddr saddr
then return ([],Map
.empty,(1,Nothing
)) else do
186 undecls
<- confFilePath
("undeclared" ++ [pathSeparator
] ++ saddrPath saddr
) >>=
187 liftM (fromMaybe []) . readReadFile
188 partials
<- confFilePath
("partialSolutions" ++ [pathSeparator
] ++ saddrPath saddr
) >>=
189 liftM (fromMaybe Map
.empty) . readReadFile
190 tut
<- confFilePath
"tutProgress" >>=
191 liftM (fromMaybe (1,Nothing
)) . readReadFile
192 return (undecls
,partials
,tut
)
194 writeServerSolns saddr ms
@(MetaState
{ undeclareds
=undecls
,
195 partialSolutions
=partials
, tutProgress
=tut
}) = unless (nullSaddr saddr
) $ do
196 confFilePath
("undeclared" ++ [pathSeparator
] ++ saddrPath saddr
) >>= flip writeReadFile undecls
197 confFilePath
("partialSolutions" ++ [pathSeparator
] ++ saddrPath saddr
) >>= flip writeReadFile partials
198 confFilePath
("tutProgress") >>= flip writeReadFile tut
200 readLock
:: FilePath -> IO (Maybe (Lock
, Maybe Solution
))
201 readLock path
= runMaybeT
$ msum
202 [ (\l
->(l
,Nothing
)) <$> (MaybeT
$ readReadFile path
)
204 (mlock
,msoln
) <- lift
$ readAsciiLockFile path
205 lock
<- liftMaybe mlock
206 return $ (lock
,msoln
) ]
207 -- writeLock :: FilePath -> Lock -> IO ()
208 -- writeLock path lock = fullLockPath path >>= flip writeReadFile lock
210 writeMetaState ms
@(MetaState
{ curServer
=saddr
, curAuth
=auth
, curLockPath
=path
}) = do
211 confFilePath
"metagame.conf" >>= flip writeReadFile
(saddr
, auth
, path
)
212 writeServerSolns saddr ms
214 getTitle
:: UIMonad uiM
=> MainStateT uiM
(Maybe String)
215 getTitle
= ms2im
<$> get
>>= \im
-> case im
of
218 unsaved
<- editStateUnsaved
219 isTested
<- isJust <$> getCurTestSoln
220 return $ Just
$ "editing " ++ fromMaybe "[unnamed lock]" mpath
++
221 (if isTested
then " (Tested)" else "") ++
222 (if unsaved
then " [+]" else " ")
223 IMPlay
-> gets psTitle
224 IMReplay
-> gets rsTitle
227 editStateUnsaved
:: UIMonad uiM
=> MainStateT uiM
Bool
228 editStateUnsaved
= (isNothing <$>) $ runMaybeT
$ do
229 (sst
,tested
) <- MaybeT
$ gets lastSavedState
230 st
<- MaybeT
$ gets
$ headMay
.esGameStateStack
232 nowTested
<- isJust <$> lift getCurTestSoln
233 guard $ tested
== nowTested
235 getCurTestSoln
:: UIMonad uiM
=> MainStateT uiM
(Maybe Solution
)
236 getCurTestSoln
= runMaybeT
$ do
237 (st
',soln
) <- MaybeT
$ gets esTested
238 st
<- MaybeT
$ gets
$ headMay
.esGameStateStack
242 mgetOurName
:: (UIMonad uiM
) => MaybeT
(MainStateT uiM
) Codename
243 mgetOurName
= MaybeT
$ (authUser
<$>) <$> gets curAuth
244 mgetCurName
:: (UIMonad uiM
) => MaybeT
(MainStateT uiM
) Codename
245 mgetCurName
= MaybeT
$ listToMaybe <$> gets codenameStack
247 getUInfoFetched
:: UIMonad uiM
=> Integer -> Codename
-> MainStateT uiM FetchedRecord
248 getUInfoFetched staleTime name
= do
249 uinfott
<- gets
(Map
.lookup name
. userInfoTVs
)
250 ($uinfott
) $ maybe set
$ \(tvar
,time
) -> do
251 now
<- liftIO getCurrentTime
252 if floor (diffUTCTime now time
) > staleTime
254 else liftIO
$ atomically
$ readTVar tvar
257 now
<- liftIO getCurrentTime
258 tvar
<- getRecordCachedFromCur
True $ RecUserInfo name
259 modify
$ \ms
-> ms
{userInfoTVs
= Map
.insert name
(tvar
, now
) $ userInfoTVs ms
}
260 liftIO
$ atomically
$ readTVar tvar
262 mgetUInfo
:: UIMonad uiM
=> Codename
-> MaybeT
(MainStateT uiM
) UserInfo
264 RCUserInfo
(_
,uinfo
) <- MaybeT
$ (fetchedRC
<$>) $ getUInfoFetched defaultStaleTime name
266 where defaultStaleTime
= 300
269 invalidateUInfo
:: UIMonad uiM
=> Codename
-> MainStateT uiM
()
270 invalidateUInfo name
=
271 modify
$ \ms
-> ms
{userInfoTVs
= Map
.delete name
$ userInfoTVs ms
}
273 invalidateAllUInfo
:: UIMonad uiM
=> MainStateT uiM
()
275 modify
$ \ms
-> ms
{userInfoTVs
= Map
.empty}
277 data Codenames
= AllCodenames | SomeCodenames
[Codename
]
279 invalidateUInfos
:: UIMonad uiM
=> Codenames
-> MainStateT uiM
()
280 invalidateUInfos AllCodenames
= invalidateAllUInfo
281 invalidateUInfos
(SomeCodenames names
) = mapM_ invalidateUInfo names
284 mgetLock
:: UIMonad uiM
=> LockSpec
-> MaybeT
(MainStateT uiM
) Lock
286 tvar
<- msum [ MaybeT
$ (Map
.lookup ls
) <$> gets indexedLocks
288 tvar
<- getRecordCachedFromCur
True $ RecLock ls
289 modify
$ \ms
-> ms
{ indexedLocks
= Map
.insert ls tvar
$ indexedLocks ms
}
291 RCLock lock
<- MaybeT
$ (fetchedRC
<$>) $ liftIO
$ atomically
$ readTVar tvar
292 return $ reframe lock
294 invalidateAllIndexedLocks
:: UIMonad uiM
=> MainStateT uiM
()
295 invalidateAllIndexedLocks
=
296 modify
$ \ms
-> ms
{ indexedLocks
= Map
.empty }
298 refreshUInfoUI
:: (UIMonad uiM
) => MainStateT uiM
()
299 refreshUInfoUI
= void
.runMaybeT
$ do
300 modify
$ \ms
-> ms
{ listOffset
= 0 }
301 mourNameSelected
>>? getRandomNames
302 lift
$ modify
$ \ms
-> ms
{retiredLocks
= Nothing
}
303 --lift.lift $ drawMessage ""
306 rnamestvar
<- gets randomCodenames
307 liftIO
$ atomically
$ writeTVar rnamestvar
[]
308 flag
<- gets newAsync
309 saddr
<- gets curServer
310 void
$ liftIO
$ forkIO
$ do
311 resp
<- makeRequest saddr
$
312 ClientRequest protocolVersion Nothing
$ GetRandomNames
19
314 ServedRandomNames names
-> atomically
$ do
315 writeTVar rnamestvar names
319 mourNameSelected
:: (UIMonad uiM
) => MaybeT
(MainStateT uiM
) Bool
320 mourNameSelected
= liftM2 (==) mgetCurName mgetOurName
322 purgeInvalidUndecls
:: (UIMonad uiM
) => MainStateT uiM
()
323 purgeInvalidUndecls
= do
324 undecls
' <- gets undeclareds
>>= filterM ((not<$>).invalid
)
325 modify
$ \ms
-> ms
{ undeclareds
= undecls
' }
327 invalid
(Undeclared _ ls
(ActiveLock name idx
)) =
328 (fromMaybe False <$>) $ runMaybeT
$ do
329 uinfo
<- mgetUInfo name
330 ourName
<- mgetOurName
331 (`mplus`
return True) $ do
332 linfo
<- liftMaybe
$ userLocks uinfo
! idx
333 return $ public linfo
334 || ourName `
elem` accessedBy linfo
335 || lockSpec linfo
/= ls
338 curServerAction
:: UIMonad uiM
=> Protocol
.Action
-> MainStateT uiM ServerResponse
339 curServerAction act
= do
340 saddr
<- gets curServer
342 cOnly
<- gets cacheOnly
343 if cOnly
then return $ ServerError
"Can't contact server in cache-only mode"
344 else (fromMaybe (ServerError
"Request aborted") <$>) $
345 lift
$ withImpatience
$ makeRequest saddr
$ ClientRequest protocolVersion auth act
347 curServerActionAsyncThenInvalidate
:: UIMonad uiM
=> Protocol
.Action
-> Maybe Codenames
-> MainStateT uiM
()
348 curServerActionAsyncThenInvalidate act names
= do
349 saddr
<- gets curServer
351 flag
<- gets newAsync
352 count
<- gets asyncCount
353 errtvar
<- gets asyncError
354 invaltvar
<- gets asyncInvalidate
355 cOnly
<- gets cacheOnly
356 void
$ liftIO
$ forkIO
$ do
357 atomically
$ modifyTVar count
(+1)
358 resp
<- if cOnly
then return $ ServerError
"Can't contact server in cache-only mode"
359 else makeRequest saddr
$ ClientRequest protocolVersion auth act
361 ServerError err
-> atomically
$ writeTVar errtvar
$ Just err
362 _
-> atomically
$ writeTVar invaltvar names
363 atomically
$ writeTVar flag
True
364 atomically
$ modifyTVar count
(+(-1))
366 checkAsync
:: UIMonad uiM
=> MainStateT uiM
()
369 errtvar
<- lift
$ gets asyncError
370 err
<- MaybeT
$ liftIO
$ atomically
$
371 readTVar errtvar
<* writeTVar errtvar Nothing
372 lift
.lift
$ drawError err
374 invaltvar
<- lift
$ gets asyncInvalidate
375 names
<- MaybeT
$ liftIO
$ atomically
$
376 readTVar invaltvar
<* writeTVar invaltvar Nothing
377 lift
$ invalidateUInfos names
>> refreshUInfoUI
379 getRecordCachedFromCur
:: UIMonad uiM
=> Bool -> Record
-> MainStateT uiM
(TVar FetchedRecord
)
380 getRecordCachedFromCur flagIt rec
= do
381 saddr
<- gets curServer
383 cOnly
<- gets cacheOnly
384 flag
<- gets newAsync
385 liftIO
$ getRecordCached saddr auth
386 (if flagIt
then Just flag
else Nothing
) cOnly rec
388 getFreshRecBlocking
:: UIMonad uiM
=> Record
-> MainStateT uiM
(Maybe RecordContents
)
389 getFreshRecBlocking rec
= do
390 tvar
<- getRecordCachedFromCur
False rec
391 cOnly
<- gets cacheOnly
392 mfetched
<- lift
$ withImpatience
$ atomically
$ do
393 fetched
@(FetchedRecord fresh _ _
) <- readTVar tvar
394 check
$ fresh || cOnly
397 Nothing
-> lift
(drawError
"Request aborted") >> return Nothing
399 case fetchError fetched
of
400 Nothing
-> return $ fetchedRC fetched
401 Just err
-> lift
(drawError err
) >> return Nothing
403 -- |indicate waiting for server, and allow cancellation
404 withImpatience
:: UIMonad uiM
=> IO a
-> uiM
(Maybe a
)
405 withImpatience m
= do
406 finishedTV
<- liftIO
$ atomically
$ newTVar Nothing
407 id <- liftIO
$ forkIO
$ m
>>= atomically
. writeTVar finishedTV
. Just
408 let waitImpatiently ticks
= do
409 finished
<- liftIO
$ atomically
$ readTVar finishedTV
413 abort
<- impatience ticks
415 then liftIO
$ killThread
id >> return Nothing
416 else waitImpatiently
$ ticks
+1
420 getRelScore
:: (UIMonad uiM
) => Codename
-> MainStateT uiM
(Maybe Int)
421 getRelScore name
= (fst<$>) <$> getRelScoreDetails name
422 getRelScoreDetails name
= runMaybeT
$ do
423 ourName
<- mgetOurName
424 guard $ ourName
/= name
425 uinfo
<- mgetUInfo name
426 ourUInfo
<- mgetUInfo ourName
427 let (neg
,pos
) = (countPoints ourUInfo uinfo
, countPoints uinfo ourUInfo
)
428 return $ (pos
-neg
,(pos
,neg
))
430 countPoints mugu masta
= length $ filter (maybe False winsPoint
) $ getAccessInfo mugu masta
432 accessedAL
:: (UIMonad uiM
) => ActiveLock
-> MainStateT uiM
Bool
433 accessedAL
(ActiveLock name idx
) = (isJust <$>) $ runMaybeT
$ do
434 ourName
<- mgetOurName
435 guard $ ourName
/= name
436 uinfo
<- mgetUInfo name
437 ourUInfo
<- mgetUInfo ourName
438 guard $ isJust $ getAccessInfo uinfo ourUInfo
!! idx
440 getNotesReadOn
:: UIMonad uiM
=> LockInfo
-> MainStateT uiM
[NoteInfo
]
441 getNotesReadOn lockinfo
= (fromMaybe [] <$>) $ runMaybeT
$ do
442 ourName
<- mgetOurName
443 ourUInfo
<- mgetUInfo ourName
444 return $ filter (\n -> isNothing (noteBehind n
)
445 || n `
elem` notesRead ourUInfo
) $ lockSolutions lockinfo
447 testAuth
:: UIMonad uiM
=> MainStateT uiM
()
448 testAuth
= isJust <$> gets curAuth
>>?
do
449 resp
<- curServerAction
$ Authenticate
451 ServerMessage msg
-> (lift
$ drawMessage
$ "Server: " ++ msg
)
452 ServerError err
-> do
453 lift
$ drawMessage err
454 modify
$ \ms
-> ms
{curAuth
= Nothing
}
457 metagameHelpText
:: [String]
459 [ "By ruthlessly guarded secret arrangement, the council's agents can pick any lock in the city."
460 , "A secret guild produces the necessary locks - apparently secure, but with fatal hidden flaws."
461 , "A ritual game is played to determine the best designs."
462 , "To master it, you must build locks which can be picked only by one who knows the secret,"
463 , "and you must discover the secret flaws in the locks designed by your colleagues."
465 , "You may put forward up to three prototype locks. They will guard the secrets you discover."
466 , "If you pick a colleague's lock, the rules require that a note is written on your solution."
467 , "A note proves that a solution was found, while revealing no more details than necessary."
468 --, "Composing notes is a tricky and ritual-bound art of its own, performed by independent experts."
469 , "To declare your solution, you must secure your note behind a lock of your own."
470 , "If you are able to unlock a lock, you automatically read all the notes it secures."
471 , "If you read three notes on a lock, you will piece together the secrets of unlocking that lock."
473 , "The game judges players relative to each of their peers. There are no absolute rankings."
474 , "You win a point of esteem against another player for each of their locks for which either:"
475 , "you have solved the lock and declared a note which the lock's owner has not read, or"
476 , "you have read three notes on the lock."
477 , "You also win a point for each empty lock slot if you can unlock all full slots."
478 , "Relative esteem is the points you win minus the points they win; +3 is best, -3 is worst."
480 , "If the secrets to one of your locks become widely disseminated, you may wish to replace it."
481 , "However: once replaced, a lock is \"retired\", and the notes it secured are read by everyone."
484 initiationHelpText
:: [String]
490 , "You have exhibited a level of manual and mental dexterity adequate for the picking of locks."
492 , "Whether you also possess the deviousness required for their design, remains to be seen."
494 , "Nonetheless, we welcome you to our number. For reasons that doubtless require no elaboration,"
495 , "our members are known exclusively by pseudonyms - by tradition, a triplet of letters or symbols."
496 , "I am eager to hear what codename you will choose for yourself."
498 , "But first, let us remind you of the purpose and rules of our game here..."
501 firstEditHelpText
:: [String]
503 [ "Design a lock to protect your secrets."
504 , "It must be possible to pick your lock by pulling a sprung bolt from the hole in the top-right,"
505 , "but you should place blocks, springs, pivots, and balls to make this as difficult as possible."
507 , "Place pieces with keyboard or mouse. Springs must be set next to blocks, and arms next to pivots."
508 , "Repeatedly placing a piece in the same hex cycles through ways it can relate to its neighbours."
510 , "Use Test to prove your lock is solvable, or Play to alternate between testing and editing."
511 , "When you are done, Write your lock, then Quit from editing and Place your lock in a slot."
512 , "You will then be able to Declare locks you solve, and others will attempt to solve your lock."
514 , "Your first lock is unlikely to stand for long against your more experienced peers;"
515 , "examine their solutions to spot flaws in your design, and study their locks for ideas."