Double-click to edit a todo
" 190 | <> "Written by Justin Le on " 191 | <> "ghcjs " 192 | <> "as an auto demo " 193 | <> "(source: logic " 194 | <> "view)
" 195 | <> "Spec, templates, and assets from TodoMVC
" 196 | 197 | -- one render with initial GUI conditions, to set things up 198 | _ <- renderGui doc main_ footer inputChan (mempty, GUI All Nothing) 199 | 200 | return (main_, footer) 201 | 202 | -- | Render the view for a given "output state" `(IntMap Task, GUIOpts)`, 203 | -- and add the callbacks. 204 | -- 205 | -- One thing to remember that is there is basically no logic going on here. 206 | -- All we are doing is rendering the output of the `Auto` in a "dumb" way, 207 | -- and adding callback hooks to add inputs into the `Chan` queue whenever 208 | -- stuff happens. 209 | -- 210 | -- If we had a nice high-level DOM language this could all pretty much be 211 | -- half as long and very expressive...but yeah, the only "Auto" part is 212 | -- that whenever someone clicks something or does something, it adds a new 213 | -- command to the `Chan` queue. 214 | -- 215 | -- Most of the fancy display tricks are handled with css, anyway :) 216 | renderGui :: Document 217 | -> HTMLElement 218 | -> HTMLElement 219 | -> Chan (Either TodoInp GUIInp) 220 | -> (IntMap Task, GUIOpts) 221 | -> IO Bool 222 | renderGui doc main_ footer inputChan (tasks, GUI filt selc) = do 223 | htmlElementSetHidden main_ (IM.size tasks == 0) 224 | htmlElementSetHidden footer (IM.size tasks == 0) 225 | 226 | htmlElementSetInnerHTML main_ "" 227 | htmlElementSetInnerHTML footer "" 228 | 229 | toggle_all <- createAppend doc main_ "input" castToHTMLInputElement 230 | elementSetAttribute toggle_all "type" "checkbox" 231 | elementSetId toggle_all "toggle-all" 232 | htmlInputElementSetName toggle_all "toggle" 233 | htmlInputElementSetChecked toggle_all allCompleted 234 | 235 | -- send a new command to the queue whenever button is pressed 236 | _ <- elementOnclick toggle_all . liftIO $ do 237 | let newCompl = not allCompleted 238 | writeChan inputChan (Left (IAll (TEComplete newCompl))) 239 | 240 | toggle_all_label <- createAppend doc main_ "label" castToHTMLLabelElement 241 | htmlLabelElementSetHtmlFor toggle_all_label "toggle-all" 242 | htmlElementSetInnerHTML toggle_all_label "Mark all as complete" 243 | 244 | todo_list <- createAppend doc main_ "ul" castToHTMLUListElement 245 | elementSetId todo_list "todo-list" 246 | 247 | _ <- IM.traverseWithKey (renderTask todo_list) tasks' 248 | 249 | todo_count <- createAppend doc footer "span" castToHTMLElement 250 | elementSetId todo_count "todo-count" 251 | htmlElementSetInnerHTML todo_count $ "" 252 | <> show (IM.size uncompl) 253 | <> " tasks left" 254 | 255 | filters <- createAppend doc footer "ul" castToHTMLUListElement 256 | elementSetId filters "filters" 257 | forM_ [All ..] $ \filtType -> do 258 | filtLi <- createAppend doc filters "li" castToHTMLLIElement 259 | 260 | -- send a new command to the queue whenever button is pressed 261 | _ <- elementOnclick filtLi . liftIO $ 262 | writeChan inputChan (Right (GIFilter filtType)) 263 | 264 | filtA <- createAppend doc filtLi "a" castToHTMLAnchorElement 265 | when (filtType == filt) $ elementSetClassName filtA "selected" 266 | htmlAnchorElementSetText filtA (show filtType) 267 | htmlAnchorElementSetHref filtA "javascript:void();" 268 | 269 | 270 | clear_completed <- createAppend doc footer "button" castToHTMLButtonElement 271 | elementSetId clear_completed "clear-completed" 272 | elementSetClassName clear_completed "clear-completed" 273 | htmlElementSetHidden clear_completed (IM.size compl == 0) 274 | htmlElementSetInnerHTML clear_completed $ "Clear completed (" 275 | <> show (IM.size compl) 276 | <> ")" 277 | 278 | -- send a new command to the queue whenever button is pressed 279 | _ <- elementOnclick clear_completed . liftIO $ 280 | writeChan inputChan (Left (IAll TEPrune)) 281 | 282 | 283 | -- tells `runOnChan` that we want to continue. 284 | return True 285 | where 286 | tasks' = case filt of 287 | All -> tasks 288 | Active -> IM.filter (not . taskCompleted) tasks 289 | Completed -> IM.filter taskCompleted tasks 290 | allCompleted = all taskCompleted tasks 291 | (compl, uncompl) = IM.partition taskCompleted tasks 292 | 293 | renderTask :: HTMLUListElement -> TaskID -> Task -> IO () 294 | renderTask todo_list tid t = do 295 | li <- createAppend doc todo_list "li" castToHTMLLIElement 296 | elementSetClassName li . unwords 297 | . map snd . filter fst $ [ (taskCompleted t, "completed") 298 | , (selc == Just tid, "editing") 299 | ] 300 | 301 | view <- createAppend doc li "div" castToHTMLDivElement 302 | elementSetClassName view "view" 303 | 304 | toggle <- createAppend doc view "input" castToHTMLInputElement 305 | elementSetAttribute toggle "type" "checkbox" 306 | elementSetClassName toggle "toggle" 307 | htmlInputElementSetChecked toggle (taskCompleted t) 308 | 309 | -- send a new command to the queue whenever button is pressed 310 | _ <- elementOnclick toggle . liftIO $ do 311 | let newCompl = not (taskCompleted t) 312 | writeChan inputChan (Left (ITask tid (TEComplete newCompl))) 313 | 314 | descr <- createAppend doc view "label" castToHTMLLabelElement 315 | htmlElementSetInnerHTML descr (taskDescr t) 316 | 317 | -- send a new command to the queue whenever button is pressed 318 | _ <- elementOndblclick descr . liftIO $ 319 | writeChan inputChan (Right (GISelect (Just tid))) 320 | 321 | destroy <- createAppend doc view "button" castToHTMLButtonElement 322 | elementSetClassName destroy "destroy" 323 | 324 | _ <- elementOnclick destroy . liftIO $ 325 | writeChan inputChan (Left (ITask tid TEDelete)) 326 | 327 | edit <- createAppend doc li "input" castToHTMLInputElement 328 | elementSetClassName edit "edit" 329 | htmlInputElementSetValue edit (taskDescr t) 330 | htmlInputElementSetName edit "title" 331 | elementSetId edit $ "todo-" <> show tid 332 | 333 | let callback = liftIO $ do 334 | editString <- htmlInputElementGetValue edit 335 | if null editString 336 | then writeChan inputChan (Left (ITask tid TEDelete)) 337 | else do 338 | writeChan inputChan (Left (ITask tid (TEModify editString))) 339 | writeChan inputChan (Right (GISelect Nothing)) 340 | 341 | -- send a new command to the queue whenever button is pressed 342 | _ <- elementOnblur edit callback 343 | _ <- elementOnkeypress edit $ do 344 | k <- uiKeyCode 345 | when (k `elem` [13, 27]) callback 346 | 347 | return () 348 | 349 | 350 | -- Utility function to create an item on the document with a given type and 351 | -- tag and append it to a given parent. 352 | createAppend :: ( IsDocument self 353 | , ToJSString tagName 354 | , IsNode parent 355 | , IsNode b 356 | ) 357 | => self 358 | -> parent 359 | -> tagName 360 | -> (Element -> b) 361 | -> IO b 362 | createAppend doc parent tag coercer = do 363 | c@(Just child) <- fmap coercer <$> documentCreateElement doc tag 364 | _ <- nodeAppendChild parent c 365 | return child 366 | -------------------------------------------------------------------------------- /src/TodoJSOld.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE Arrows #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE MonadComprehensions #-} 4 | 5 | -- | "Todo-JS" 6 | -- 7 | -- Simple todo app on ghcjs, with logic straight from the non-javascript 8 | -- version; that is, an identical 'Auto'. Mostly a ghcjs wrapper working 9 | -- with GHCJS.DOM ... which is admittedly a little messy. All of the todo 10 | -- logic is in `Todo.hs`, so check that out first :) This is just the 11 | -- "view". 12 | -- 13 | -- https://github.com/mstksg/auto-examples/blob/master/src/Todo.hs 14 | -- 15 | -- If you're building this, be sure to grab the css asset files from the 16 | -- project directory. 17 | -- 18 | -- Still missing persistence to localStorage and routing. While 19 | -- persistance might be a useful demonstration of implicit serialiation, 20 | -- a part of it (and routing too) might be outside of the range of domain 21 | -- of `auto`...so these aren't really high-priority for now. 22 | -- 23 | -- This is compiled and hosted online at: 24 | -- http://mstksg.github.io/auto-examples/todo 25 | 26 | module Main (main) where 27 | 28 | import Control.Applicative 29 | import Control.Auto hiding (All) 30 | import Control.Auto.Run 31 | import Control.Concurrent 32 | import Control.Monad (unless, when) 33 | import Control.Monad.IO.Class 34 | import Data.Foldable (forM_, all) 35 | import Data.IntMap (IntMap) 36 | import Data.Maybe 37 | import Data.Serialize 38 | import GHC.Generics 39 | import GHCJS.DOM 40 | import GHCJS.DOM.Document 41 | import GHCJS.DOM.Element 42 | import GHCJS.DOM.EventM 43 | import GHCJS.DOM.HTMLAnchorElement 44 | import GHCJS.DOM.HTMLButtonElement 45 | import GHCJS.DOM.HTMLElement 46 | import GHCJS.DOM.HTMLInputElement 47 | import GHCJS.DOM.HTMLLabelElement 48 | import GHCJS.DOM.HTMLLinkElement 49 | import GHCJS.DOM.HTMLMetaElement 50 | import GHCJS.DOM.HTMLTitleElement 51 | import GHCJS.DOM.Node 52 | import GHCJS.DOM.Types 53 | import GHCJS.Foreign 54 | import Prelude hiding ((.), id, all) 55 | import Todo 56 | import qualified Data.IntMap.Strict as IM 57 | 58 | data GUIOpts = GUI { _currFilter :: Filter -- currently applied filter 59 | , _currSelected :: Maybe TaskID -- currently selected task 60 | } 61 | 62 | data GUIInp = GIFilter Filter 63 | | GISelect (Maybe TaskID) 64 | 65 | data Filter = All | Active | Completed 66 | deriving (Show, Generic, Enum, Eq) 67 | 68 | instance Serialize Filter 69 | 70 | -- | A new `Auto` that takes in commands from the GUI (that can either be 71 | -- commands for the Todo app logic itself, or commands to change some GUI 72 | -- option). 73 | -- 74 | -- Basically filters the input stream into three different blip streams and 75 | -- recombines them all together in the end. 76 | -- 77 | -- The result is a tuple with all of the alive `Task` items, and GUI option 78 | -- settings. 79 | todoAppGUI :: Auto' (Either TodoInp GUIInp) (IntMap Task, GUIOpts) 80 | todoAppGUI = proc inp -> do 81 | -- process the input items that are for the Todo app itself. pretty 82 | -- much just feeds it to the `todoApp` auto, from `Todo.hs`, which 83 | -- has the actual logic. 84 | outp <- holdWith mempty . perBlip todoApp . emitJusts todoInps -< inp 85 | -- `filt` will be the last seen filter setting inputted, starting with 86 | -- `All`. It is the currently applied filter. 87 | filt <- holdWith All . emitJusts filtInps -< inp 88 | -- `selc` will be the last seen selection setting inputted, starting with 89 | -- `Nothing`. It is the currently selected/edited task. 90 | selc <- holdWith Nothing . emitJusts selcInps -< inp 91 | 92 | id -< (outp, GUI filt selc) 93 | where 94 | -- monad comprehensions to act as primitive lenses/filters 95 | todoInps :: Either TodoInp GUIInp -> Maybe TodoInp 96 | todoInps etg = [ ti | Left ti <- Just etg ] 97 | filtInps :: Either TodoInp GUIInp -> Maybe Filter 98 | filtInps etg = [ filt | Right (GIFilter filt) <- Just etg ] 99 | selcInps :: Either TodoInp GUIInp -> Maybe (Maybe TaskID) 100 | selcInps etg = [ selc | Right (GISelect selc) <- Just etg ] 101 | 102 | 103 | 104 | main :: IO () 105 | main = do 106 | -- The `Chan` queue to dump all commands triggered by GUI actions 107 | inputChan <- newChan :: IO (Chan (Either TodoInp GUIInp)) 108 | 109 | runWebGUI $ \ webView -> do 110 | Just doc <- webViewGetDomDocument webView 111 | 112 | -- render the skeleton, giving a reference to the todo list and the 113 | -- info footer on the DOM 114 | (main_, footer) <- renderInitial doc inputChan 115 | 116 | -- Run the `Auto` `todoAppGUI` on the `inputChan` queue, by waiting 117 | -- for new commands (deposited by the GUI) to show up on the queue, 118 | -- feeding them through `todoAppGUI`, and "rendering" the output 119 | -- with `renderGui doc main_ footer inputChan`. 120 | -- 121 | _ <- runOnChan (renderGui doc main_ footer inputChan) 122 | inputChan 123 | todoAppGUI 124 | 125 | return () 126 | 127 | -- | Set up the "static" skeleton of the GUI that won't be updated. 128 | -- Returns a reference the todo list body and the footer with information. 129 | -- 130 | -- Admittedly pretty hairy, but there's no real "logic" here, only view 131 | -- manipulation. If we had a high-level DOM manipulation library for ghcjs 132 | -- this could probably just be half as long and much more clean. 133 | renderInitial :: Document 134 | -> Chan (Either TodoInp GUIInp) 135 | -> IO (HTMLElement, HTMLElement) 136 | renderInitial doc inputChan = do 137 | Just hd <- documentGetHead doc 138 | 139 | meta <- createAppend doc hd "meta" castToHTMLMetaElement 140 | elementSetAttribute meta "charset" "utf-8" 141 | 142 | title <- createAppend doc hd "title" castToHTMLTitleElement 143 | htmlTitleElementSetText title "auto :: TodoMVC" 144 | 145 | forM_ ["assets/base.css","assets/index.css"] $ \lnk -> do 146 | cssLink <- createAppend doc hd "link" castToHTMLLinkElement 147 | htmlLinkElementSetRel cssLink "stylesheet" 148 | elementSetAttribute cssLink "charset" "utf-8" 149 | elementSetAttribute cssLink "type" "text/css" 150 | htmlLinkElementSetHref cssLink lnk 151 | 152 | Just body <- documentGetBody doc 153 | 154 | todomvc_wrapper <- createAppend doc body "div" castToHTMLDivElement 155 | elementSetClassName todomvc_wrapper "todomvc-wrapper" 156 | 157 | todoapp <- createAppend doc todomvc_wrapper "section" castToHTMLElement 158 | elementSetId todoapp "todoapp" 159 | 160 | header <- createAppend doc todoapp "header" castToHTMLElement 161 | 162 | heading <- createAppend doc header "h1" castToHTMLHeadingElement 163 | htmlElementSetInnerHTML heading "todo" 164 | 165 | new_todo <- createAppend doc header "input" castToHTMLInputElement 166 | elementSetId new_todo "new-todo" 167 | htmlInputElementSetPlaceholder new_todo "What needs to be done?" 168 | htmlInputElementSetAutofocus new_todo True 169 | htmlInputElementSetName new_todo "newTodo" 170 | 171 | -- add an `IEAdd` command to the queue whenever a new task is submitted 172 | _ <- elementOnkeypress new_todo $ do 173 | k <- uiKeyCode 174 | when (k == 13) . liftIO $ do 175 | inp <- htmlInputElementGetValue new_todo 176 | unless (null inp) $ do 177 | writeChan inputChan (Left (IEAdd inp)) 178 | htmlInputElementSetValue new_todo "" 179 | 180 | main_ <- createAppend doc todoapp "section" castToHTMLElement 181 | elementSetId main_ "main" 182 | 183 | footer <- createAppend doc todoapp "footer" castToHTMLElement 184 | elementSetId footer "footer" 185 | 186 | info <- createAppend doc todomvc_wrapper "footer" castToHTMLElement 187 | elementSetId info "info" 188 | htmlElementSetInnerHTML info $ 189 | "Double-click to edit a todo
" 190 | <> "Written by Justin Le on " 191 | <> "ghcjs " 192 | <> "as an auto demo " 193 | <> "(source: logic " 194 | <> "view)
" 195 | <> "Spec, templates, and assets from TodoMVC
" 196 | 197 | -- one render with initial GUI conditions, to set things up 198 | _ <- renderGui doc main_ footer inputChan (mempty, GUI All Nothing) 199 | 200 | return (main_, footer) 201 | 202 | -- | Render the view for a given "output state" `(IntMap Task, GUIOpts)`, 203 | -- and add the callbacks. 204 | -- 205 | -- One thing to remember that is there is basically no logic going on here. 206 | -- All we are doing is rendering the output of the `Auto` in a "dumb" way, 207 | -- and adding callback hooks to add inputs into the `Chan` queue whenever 208 | -- stuff happens. 209 | -- 210 | -- If we had a nice high-level DOM language this could all pretty much be 211 | -- half as long and very expressive...but yeah, the only "Auto" part is 212 | -- that whenever someone clicks something or does something, it adds a new 213 | -- command to the `Chan` queue. 214 | -- 215 | -- Most of the fancy display tricks are handled with css, anyway :) 216 | renderGui :: Document 217 | -> HTMLElement 218 | -> HTMLElement 219 | -> Chan (Either TodoInp GUIInp) 220 | -> (IntMap Task, GUIOpts) 221 | -> IO Bool 222 | renderGui doc main_ footer inputChan (tasks, GUI filt selc) = do 223 | htmlElementSetHidden main_ (IM.size tasks == 0) 224 | htmlElementSetHidden footer (IM.size tasks == 0) 225 | 226 | htmlElementSetInnerHTML main_ "" 227 | htmlElementSetInnerHTML footer "" 228 | 229 | toggle_all <- createAppend doc main_ "input" castToHTMLInputElement 230 | elementSetAttribute toggle_all "type" "checkbox" 231 | elementSetId toggle_all "toggle-all" 232 | htmlInputElementSetName toggle_all "toggle" 233 | htmlInputElementSetChecked toggle_all allCompleted 234 | 235 | -- send a new command to the queue whenever button is pressed 236 | _ <- elementOnclick toggle_all . liftIO $ do 237 | let newCompl = not allCompleted 238 | writeChan inputChan (Left (IEAll (TEComplete newCompl))) 239 | 240 | toggle_all_label <- createAppend doc main_ "label" castToHTMLLabelElement 241 | htmlLabelElementSetHtmlFor toggle_all_label "toggle-all" 242 | htmlElementSetInnerHTML toggle_all_label "Mark all as complete" 243 | 244 | todo_list <- createAppend doc main_ "ul" castToHTMLUListElement 245 | elementSetId todo_list "todo-list" 246 | 247 | _ <- IM.traverseWithKey (renderTask todo_list) tasks' 248 | 249 | todo_count <- createAppend doc footer "span" castToHTMLElement 250 | elementSetId todo_count "todo-count" 251 | htmlElementSetInnerHTML todo_count $ "" 252 | <> show (IM.size uncompl) 253 | <> " tasks left" 254 | 255 | filters <- createAppend doc footer "ul" castToHTMLUListElement 256 | elementSetId filters "filters" 257 | forM_ [All ..] $ \filtType -> do 258 | filtLi <- createAppend doc filters "li" castToHTMLLIElement 259 | 260 | -- send a new command to the queue whenever button is pressed 261 | _ <- elementOnclick filtLi . liftIO $ 262 | writeChan inputChan (Right (GIFilter filtType)) 263 | 264 | filtA <- createAppend doc filtLi "a" castToHTMLAnchorElement 265 | when (filtType == filt) $ elementSetClassName filtA "selected" 266 | htmlAnchorElementSetText filtA (show filtType) 267 | htmlAnchorElementSetHref filtA "javascript:void();" 268 | 269 | 270 | clear_completed <- createAppend doc footer "button" castToHTMLButtonElement 271 | elementSetId clear_completed "clear-completed" 272 | elementSetClassName clear_completed "clear-completed" 273 | htmlElementSetHidden clear_completed (IM.size compl == 0) 274 | htmlElementSetInnerHTML clear_completed $ "Clear completed (" 275 | <> show (IM.size compl) 276 | <> ")" 277 | 278 | -- send a new command to the queue whenever button is pressed 279 | _ <- elementOnclick clear_completed . liftIO $ 280 | writeChan inputChan (Left (IEAll TEPrune)) 281 | 282 | 283 | -- tells `runOnChan` that we want to continue. 284 | return True 285 | where 286 | tasks' = case filt of 287 | All -> tasks 288 | Active -> IM.filter (not . taskCompleted) tasks 289 | Completed -> IM.filter taskCompleted tasks 290 | allCompleted = all taskCompleted tasks 291 | (compl, uncompl) = IM.partition taskCompleted tasks 292 | 293 | renderTask :: HTMLUListElement -> TaskID -> Task -> IO () 294 | renderTask todo_list tid t = do 295 | li <- createAppend doc todo_list "li" castToHTMLLIElement 296 | elementSetClassName li . unwords 297 | . map snd . filter fst $ [ (taskCompleted t, "completed") 298 | , (selc == Just tid, "editing") 299 | ] 300 | 301 | view <- createAppend doc li "div" castToHTMLDivElement 302 | elementSetClassName view "view" 303 | 304 | toggle <- createAppend doc view "input" castToHTMLInputElement 305 | elementSetAttribute toggle "type" "checkbox" 306 | elementSetClassName toggle "toggle" 307 | htmlInputElementSetChecked toggle (taskCompleted t) 308 | 309 | -- send a new command to the queue whenever button is pressed 310 | _ <- elementOnclick toggle . liftIO $ do 311 | let newCompl = not (taskCompleted t) 312 | writeChan inputChan (Left (IETask tid (TEComplete newCompl))) 313 | 314 | descr <- createAppend doc view "label" castToHTMLLabelElement 315 | htmlElementSetInnerHTML descr (taskDescr t) 316 | 317 | -- send a new command to the queue whenever button is pressed 318 | _ <- elementOndblclick descr . liftIO $ 319 | writeChan inputChan (Right (GISelect (Just tid))) 320 | 321 | destroy <- createAppend doc view "button" castToHTMLButtonElement 322 | elementSetClassName destroy "destroy" 323 | 324 | _ <- elementOnclick destroy . liftIO $ 325 | writeChan inputChan (Left (IETask tid TEDelete)) 326 | 327 | edit <- createAppend doc li "input" castToHTMLInputElement 328 | elementSetClassName edit "edit" 329 | htmlInputElementSetValue edit (taskDescr t) 330 | htmlInputElementSetName edit "title" 331 | elementSetId edit $ "todo-" <> show tid 332 | 333 | let callback = liftIO $ do 334 | editString <- htmlInputElementGetValue edit 335 | if null editString 336 | then writeChan inputChan (Left (IETask tid TEDelete)) 337 | else do 338 | writeChan inputChan (Left (IETask tid (TEModify editString))) 339 | writeChan inputChan (Right (GISelect Nothing)) 340 | 341 | -- send a new command to the queue whenever button is pressed 342 | _ <- elementOnblur edit callback 343 | _ <- elementOnkeypress edit $ do 344 | k <- uiKeyCode 345 | when (k `elem` [13, 27]) callback 346 | 347 | return () 348 | 349 | 350 | -- Utility function to create an item on the document with a given type and 351 | -- tag and append it to a given parent. 352 | createAppend :: ( IsDocument self 353 | , ToJSString tagName 354 | , IsNode parent 355 | , IsNode b 356 | ) 357 | => self 358 | -> parent 359 | -> tagName 360 | -> (Element -> b) 361 | -> IO b 362 | createAppend doc parent tag coercer = do 363 | c@(Just child) <- fmap coercer <$> documentCreateElement doc tag 364 | _ <- nodeAppendChild parent c 365 | return child 366 | -------------------------------------------------------------------------------- /src/Experimental/Connect4.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE Arrows #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE TupleSections #-} 4 | {-# LANGUAGE LambdaCase #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | 7 | -- | Usage: 8 | -- 9 | -- $ connect4 [controller X] [controller O] 10 | -- 11 | -- Controllers: 12 | -- * h : human 13 | -- * cR: computer (random moves) 14 | -- * cE: computer (easy) 15 | -- * cH: computer (sorta hard) 16 | -- 17 | -- Defaults to h vs cH 18 | -- 19 | 20 | module Main (main) where 21 | 22 | import Control.Auto hiding (loop) 23 | import Control.Auto.Blip 24 | import Control.Auto.Collection 25 | import Control.Auto.Process.Random 26 | import Control.Auto.Switch 27 | import Control.Auto.Time 28 | import Control.Monad 29 | import Control.Monad.Fix 30 | import Data.Foldable (toList) 31 | import Data.Function hiding ((.), id) 32 | import Data.Functor.Identity 33 | import Data.List 34 | import Data.Maybe 35 | import Data.Ord 36 | import Data.Serialize 37 | import GHC.Generics 38 | import Prelude hiding ((.), id, mapM_) 39 | import System.Console.ANSI 40 | import System.Environment 41 | import System.Random 42 | import qualified Data.Map.Strict as M 43 | 44 | -- Types 45 | type Board = [Column] 46 | type Column = [Piece] 47 | type Player = Piece 48 | 49 | -- The Piece type, also representing a player. 50 | data Piece = X | O deriving (Show, Read, Eq, Ord, Generic) 51 | 52 | -- The output of the 'Board' Auto that allows the other Autos and stuff to 53 | -- display it and react to it. Contains the board, the winner (Nothing 54 | -- if no winner, Just (Just p) if p won, and Just Nothing if a tie), the 55 | -- next player up, and if the last move failed. 56 | data BoardOut = BoardOut { _boBoard :: !Board 57 | , _boWinner :: !(Maybe (Maybe Player)) 58 | , _boNext :: !Player 59 | , _boFailed :: !Bool 60 | } deriving Generic 61 | 62 | -- The type of the generic controllers (human, CPU, etc). If the output is 63 | -- Just, it means...that's the move it wants to make. If the output is 64 | -- Nothing, then the Controller is "asking" for a User input (the Maybe 65 | -- Int). 66 | type Controller m = Auto m (Maybe Int, BoardOut) (Maybe Int) 67 | -- ^ ^ ^ 68 | -- | | +-- Possible output 69 | -- | +-- Game output, for decision making 70 | -- +-- Possible user input 71 | 72 | instance Serialize Piece 73 | instance Serialize BoardOut 74 | 75 | -- config, and initializations 76 | boardWidth, boardHeight :: Int 77 | boardWidth = 7 78 | boardHeight = 6 79 | 80 | emptyBoard :: Board 81 | emptyBoard = replicate boardWidth [] 82 | 83 | emptyBoardOut :: BoardOut 84 | emptyBoardOut = BoardOut emptyBoard Nothing X False 85 | 86 | main :: IO () 87 | main = do 88 | -- build the two controllers, from command line arguments. 89 | args <- getArgs 90 | (cX, cO) <- case args of 91 | [] -> (,) <$> interfOf "h" <*> interfOf "cH" 92 | cO:[] -> (,) <$> interfOf "h" <*> interfOf cO 93 | cX:cO:_ -> (,) <$> interfOf cX <*> interfOf cO 94 | 95 | -- The initial game Auto 96 | let gameAuto = game cX cO 97 | 98 | -- off we go! 99 | loop gameAuto 0 100 | where 101 | -- Build controllers from command line arguments. 102 | interfOf :: MonadFix m => String -> IO (Controller m) 103 | interfOf "cH" = cpuAlphaBeta 8 <$> newStdGen 104 | interfOf "cE" = cpuAlphaBeta 4 <$> newStdGen 105 | interfOf "cR" = cpuRandom <$> newStdGen 106 | interfOf _ = return human 107 | 108 | -- the main loop 109 | loop a i0 = do 110 | let (bout, a') = stepAuto' a i0 111 | clearScreen 112 | putStrLn (showOut bout) 113 | when (isNothing (_boWinner bout)) $ do 114 | i1 <- fromMaybe 0 . readMaybe <$> getLine 115 | loop a' i1 116 | 117 | -- the main game Auto. Given two controllers. 118 | -- 119 | -- Controllers are just a type alias for a normal Auto: 120 | -- 121 | -- > type Controller m = Auto m (Maybe Int, BoardOut) (Maybe Int) 122 | -- 123 | -- See the definition of the type for details on what each field means. 124 | game :: forall m. MonadFix m 125 | => Controller m -- ^ X Player 126 | -> Controller m -- ^ O Player 127 | -> Auto m Int BoardOut -- ^ Game Auto 128 | -- ^ ^ 129 | -- | +-- Game output 130 | -- +-- Player input 131 | -- game is the "fastForwarded" @game'@ Auto. 132 | -- 133 | -- > fastFoward :: a -> Auto m a (Maybe b) -> Auto m a b 134 | -- 135 | -- Basically, 'fastFoward' takes a default value and an Auto returning 136 | -- 'Maybe b', and then, whenever that Auto is "run"/ticked, it repeatedly 137 | -- runs the Auto until it gets a 'Just' result. It is initially run with 138 | -- the input, and then afterwards "fast forwarded" with the default 139 | -- input. In essense, you get an Auto that "always returns 'Just'"...by 140 | -- "fast fowarding" over the 'Nothing's. Like TiVo! 141 | -- 142 | game cX cO = fastForward Nothing game' <<^ Just 143 | where 144 | -- the fast-forwarded game Auto. It feeds the input directly to the 145 | -- Controller whose current turn it is, and then outputs a 'Just' 146 | -- containing the resulting 'BoardOut' whenever an controller 147 | -- "requests" player input. Also outputs a 'Just' when the game is 148 | -- over. 149 | game' :: Auto m (Maybe Int) (Maybe BoardOut) 150 | -- ^ ^ 151 | -- | +-- Board output, if player interaction is 152 | -- | needed, or game is over. 153 | -- +-- Player interaction, if this is a 154 | -- "non-fastforwarded" tick. 155 | game' = proc i -> do 156 | -- the current BoardOut, bout, is the "last value" of newBout. 157 | -- We will use this to give to our controllers, so that they 158 | -- can decide their next moves. 159 | rec bout <- lastVal emptyBoardOut -< newBout 160 | -- feed (i, bout) (the player input, and the current board) to 161 | -- the player playing next, _boNext bout. 162 | -- 163 | -- 'mux' is an Auto multiplexer. Give it an address/key 164 | -- (_boNext bout, the next player), and it'll feed the input 165 | -- (i, bout) to the Auto produced by that key, and output the 166 | -- result. 167 | -- 168 | -- For example, if you pass in (X, (Just 1, bout)), then it'll 169 | -- pass in (Just 1, bout) to the X Auto. 'interf' is the 170 | -- function that maps the key to the Auto, so the X Auto is 171 | -- 'interf X' = 'cX', the X controller. 172 | move <- mux interf -< (_boNext bout, (i, bout)) 173 | 174 | -- feed the retrieved move into the Board auto. 175 | newBout <- board emptyBoard X -< fromMaybe 0 move 176 | 177 | -- the output of the Auto. 178 | let output = case _boWinner newBout of 179 | -- if a winner, just report the new BoardOut 180 | Just _ -> Just newBout 181 | -- If no winner... 182 | Nothing -> case move of 183 | -- if a move was received from an 184 | -- Controller, all is good; no need to 185 | -- output anything. The "fast 186 | -- forwarding" continues. 187 | Just _ -> Nothing 188 | -- if no move is received from an 189 | -- controller, then we need to spark 190 | -- some player interaction. Return 191 | -- a Just to "halt" the 192 | -- fast-forwarding, and ask for input. 193 | Nothing -> Just newBout 194 | 195 | -- spit out the output. 196 | id -< output 197 | 198 | -- the correct controller for the player piece. 199 | interf X = cX 200 | interf O = cO 201 | 202 | 203 | -- board: behave like 'board b0 p0' until a 'Blip' is received...then 204 | -- switch permanently to the 'gameOver' Auto. See the 'hangman' example 205 | -- for more information on 'switchFromF'. 206 | board :: MonadFix m 207 | => Board -- ^ initial Board 208 | -> Player -- ^ initial Player 209 | -> Auto m Int BoardOut 210 | -- ^ ^ 211 | -- | +-- Resulting board 212 | -- +-- Move to make 213 | board b0 p0 = switchFromF gameOver (board' b0 p0) 214 | where 215 | -- gameOver: repeatedly just return the finished BoardOut, ignoring any 216 | -- input...but every move is a failed move (of course). 217 | gameOver b = (pure b' &&& id) . never 218 | where 219 | b' = b { _boFailed = True } 220 | 221 | -- The main Board Auto. 222 | board' :: forall m. MonadFix m 223 | => Board -- ^ initial Board 224 | -> Player -- ^ initial Player 225 | -> Auto m Int (BoardOut, Blip BoardOut) 226 | -- ^ ^ ^ 227 | -- | | +-- switch to Game Over, with this 228 | -- | | BoardOut. 229 | -- | +-- the resulting Board after the move 230 | -- +-- the move to make. 231 | board' b0 p0 = proc i -> do 232 | 233 | -- wasGood: whether or not the previously attempted move was legal. 234 | rec wasGood <- lastVal False -< goodMove 235 | 236 | -- currP: the current player. Will be "swapped" if the last move 237 | -- was good, and kept the same if it wasn't. 238 | currP <- accum swapP p0 -< wasGood 239 | 240 | -- brd: the Board. uses the 'gather' combinator, which is like the 241 | -- 'mux' combinator, but instead of outputting just the "currently 242 | -- running/requested" Auto, it outputs the result of every Auto so 243 | -- far. Here, it holds every 'column' Auto, and runs the one given 244 | -- by 'i' (the move). Gives the 'column' Auto its input, a piece 245 | -- ('currP'). 246 | -- 247 | -- (toList . fill) fills in the resulting Map as appropriate, and 248 | -- then turns it back into a list of lists (a 'Board'). 249 | -- 250 | -- It needs to be filled because 'gather' "accumultes new columns", 251 | -- as you enter in new moves. So on the first move, if you put 252 | -- a move in column 4, gather will be a Map with keys [4]. In 253 | -- the next move, if you put a move in column 2, gather will 254 | -- return a Map with keys [4,2]. 'fill' fills in the gap and 255 | -- replaces the missing keys/values in the map from the starting 256 | -- columns in 'b0'. 257 | brd <- toList . fill <$> gather col -< (i, currP) 258 | 259 | -- lastBrd: The previous board, before the new move. 260 | lastBrd <- lastVal b0 -< brd 261 | 262 | -- a good move is whether or not the previous board is the same as 263 | -- the new, updated board (the move was not rejected) 264 | let goodMove = lastBrd /= brd 265 | 266 | -- the winner. Just (Just p) if p won, Just Nothing if a tie, and 267 | -- Nothing if the game is still in progress. 268 | let winner | isWinner currP brd = Just (Just currP) 269 | | length (concat brd) >= d = Just Nothing 270 | | otherwise = Nothing 271 | 272 | -- 'win' is a Blip that occurs as soon as 'winner' changes. 'winner' 273 | -- is initially Nothing, so this basically means, it is emitted 274 | -- whenever someone wins or there is a tie. 275 | win <- onChange -< winner 276 | 277 | -- the resulting BoardOut for this tick. 278 | let boardOut = BoardOut brd 279 | winner 280 | (swapP currP goodMove) 281 | (not goodMove) 282 | 283 | -- pop out the BoardOut, and the 'win', "tagged" with the BoardOut, 284 | -- when it occurs. 285 | id -< (boardOut, boardOut <$ win) 286 | where 287 | -- if in the right width 288 | inRange n = n > 0 && n <= length b0 289 | -- number of tiles on the full board 290 | d = boardHeight * boardWidth 291 | -- fill the resulting map from 'gather' with the missing columns. See 292 | -- the binding of 'brd' for more information. 293 | fill = flip M.union (M.fromList (zip [1..] b0)) 294 | 295 | -- the starting Auto for every column. If the column number is not in 296 | -- range, it is an always-Nothing. If the column is, it's a 'column' 297 | -- Auto, with starting confirguration given by b0 !! (n - 1). 298 | -- 299 | -- 'gather' works by "pruning" 'Nothing' results, so the out-of-range 300 | -- columns are instantly pruned, and the in-range columns are forever 301 | -- a part of 'gather'. 302 | col :: Int -> Auto m Piece (Maybe Column) 303 | col n | inRange n = Just <$> column (b0 !! (n - 1)) 304 | | otherwise = pure Nothing 305 | -- swap the player, if the Bool 's' ("if the last move was good or 306 | -- not") is True. 307 | swapP p s | s = opp p 308 | | otherwise = p 309 | 310 | -- the actual 'column' Auto, of which every 'board' is composed out of. 311 | -- This is a basic usage of 'accum', which is sort of like an Auto 312 | -- foldl. Give the initial accumulator, and a merging function, apply 313 | -- the function to every incoming item and the accumulator to get a new 314 | -- accumulator and output value. 315 | -- 316 | -- In our case, we simply add the new incoming piece to the accumulator (a 317 | -- column), and then 'take' only the amount that we need, keeping the 318 | -- height of the column at most 'boardHeight'. 319 | column :: Monad m => Column -> Auto m Piece Column 320 | column = accum (\ps p -> take boardHeight (ps ++ [p])) 321 | 322 | 323 | -- Utilities 324 | 325 | -- check for winner 326 | isWinner :: Player -> Board -> Bool 327 | isWinner p b = (any . any) hasFour [ filled , transpose filled 328 | , wedgeUp, wedgeDown 329 | ] 330 | where 331 | hasFour (j:k:l:m:ns) | and [j,k,l,m] = True 332 | | otherwise = hasFour (k:l:m:ns) 333 | hasFour _ = False 334 | filled = map (take boardHeight . (++ repeat False) . map (== p)) b 335 | wedge = take boardWidth . inits $ repeat False 336 | wedgeUp = transpose $ zipWith (++) wedge filled 337 | wedgeDown = transpose $ zipWith (++) (reverse wedge) filled 338 | 339 | -- show the Board 340 | showBoard :: Board -> String 341 | showBoard = unlines . map concat 342 | . transpose . map fill 343 | where 344 | fill :: [Piece] -> [String] 345 | fill = map (++ "|") . reverse . take boardHeight . (++ repeat "_") . map show 346 | 347 | -- show a BoardOut 348 | showOut :: BoardOut -> String 349 | showOut (BoardOut brd winner nextP _) = 350 | unlines [ unwords (map show [1..boardWidth]) 351 | , showBoard brd 352 | , case winner of 353 | Nothing -> "To play: " ++ show nextP 354 | Just w -> "Game over! " ++ case w of 355 | Just p -> "Winner: " ++ show p 356 | Nothing -> "Tie game." 357 | ] 358 | 359 | -- swap a piece/player. 360 | opp :: Piece -> Piece 361 | opp X = O 362 | opp O = X 363 | 364 | -- read, possibly failing with a 'Nothing'. 365 | readMaybe :: Read a => String -> Maybe a 366 | readMaybe = fmap fst . listToMaybe . reads 367 | 368 | 369 | 370 | -- Controller & AI 371 | 372 | -- Ord-to-bound promoter for AI purposes and fast comparisons. 373 | data Bounder a = BMin | BIn a | BMax deriving (Eq, Show, Generic) 374 | 375 | instance Ord a => Ord (Bounder a) where 376 | compare BMin BMin = EQ 377 | compare BMin _ = LT 378 | compare BMax BMax = EQ 379 | compare BMax _ = GT 380 | compare (BIn _) BMin = GT 381 | compare (BIn _) BMax = LT 382 | compare (BIn x) (BIn y) = compare x y 383 | 384 | instance Serialize a => Serialize (Bounder a) 385 | 386 | -- a human controller. Basically, whatever is received is what is 387 | -- outputted. Remember that an Controller receives a (Maybe Int, 388 | -- BoardOut), and outputs a (Maybe Int). So 'arr fst' just echos out 389 | -- that Maybe Int. 390 | -- 391 | -- So, when there is user input (Just), echo out that user input. When 392 | -- there isn't any (Nothing), "request" new input (Nothing). 393 | human :: Monad m => Controller m 394 | human = arr fst 395 | 396 | -- A randomized controller. Ignores its input and outputs Just a random 397 | -- number between 1 and boardWidth at every tick. Never requires user 398 | -- input. 399 | cpuRandom :: Monad m => StdGen -> Controller m 400 | cpuRandom g = Just <$> stdRands (randomR (1, boardWidth)) g 401 | 402 | -- CPU controller with minimax featuring alpha beta pruning. A somewhat 403 | -- minimal understanding of the minimax + α/β pruning algorithm is 404 | -- assumed :) 405 | -- 406 | -- Right now the heuristic isn't too sophisticated. It rates a victory as 407 | -- +infinity, a loss as -infinity, and neither as 0 ;D 408 | -- 409 | -- Implements a "retry" feature: if it sees that the opponent can 410 | -- potentially force a win several moves ahead, it actually re-tries the 411 | -- search with a smaller lookahead. This is because often times, the 412 | -- algorithm will spot a forced victory before the opponent does...and it 413 | -- would just throw up its hands and give up. The retry allows it to try 414 | -- again and try to optimize for the short-term instead of the long-term. 415 | cpuAlphaBeta :: MonadFix m 416 | => Int -- ^ the suggested lookahead 417 | -> StdGen -- ^ shuffling seed 418 | -> Controller m 419 | cpuAlphaBeta lim g = proc (_, bout) -> do 420 | -- lastRetry is whether or not the last "tick" resulted in a retry. 421 | rec lastRetry <- lastVal False -< retry 422 | 423 | -- currP: current (maximizing) player. 424 | let currP = _boNext bout 425 | -- bo0: the initial BoardOut 426 | bo0 = _boBoard bout 427 | -- a0: the initial board Auto 428 | a0 = board bo0 currP 429 | -- lim: the true lookahead limit. is constrained to 2 if the 430 | -- last move resulted in a retry, or else the number of 431 | -- pieces on the board, so we don't waste time doing an 432 | -- expensive search on the first few moves of the game. 433 | lim' | lastRetry = 2 434 | | otherwise = min (length (concat bo0) * 2) lim 435 | 436 | -- weights on which to assign potential moves, if it comes down to 437 | -- a random choice between equally good moves. 438 | -- 439 | -- stdRands (random g) outputs a new random Double every tick. 440 | -- 'accelerate boardWidth' makes it output 'boardWidth' Doubles 441 | -- every tick, in a list. 442 | weights <- accelerate boardWidth (stdRands random g) -< () 443 | 444 | -- order in which to check the moves. basically a fisher-yates 445 | -- shuffle based on 'weights' 446 | let checkOrder = map fst . sortBy (comparing snd) 447 | . zip [1 .. boardWidth] 448 | $ (weights :: [Double]) 449 | -- result, goal: the result of the minimax algorithm. result 450 | -- is the best move, and goal is the "best we can do if the 451 | -- opponent plays perfectly" position after that move is 452 | -- made. 453 | (res, gl) = maxi checkOrder currP lim' BMin BMax a0 454 | -- retry: if this should be a retry. That is, if the opponent 455 | -- can force a win --- of gl, the "best we can do if the 456 | -- opponent plays perfectly", is a loss. 457 | retry = gl == BMin && not lastRetry 458 | -- the actual result to output. If 'res' is Nothing (the maxi 459 | -- algorithm doesn't find any valid moves), then just output 460 | -- a random result instead. 461 | trueRes = res <|> Just (head checkOrder) 462 | 463 | id -< if retry 464 | -- if a retry is desired, we output a bogus move that the 465 | -- 'board' auto will reject, so it'll "retry" us for another 466 | -- reuslt. 467 | then Just 0 468 | -- otherwise, here's the move! 469 | else trueRes 470 | where 471 | -- minimax. Nothing too related to 'Auto' here...mostly just 472 | -- a not-so-clean implementaiton of minimax w/ alpha beta pruning in 473 | -- Haskell :) 474 | maxi :: [Int] -- ^ check order 475 | -> Player -- ^ maximizing player 476 | -> Int -- ^ limit 477 | -> Bounder Double -- ^ alpha 478 | -> Bounder Double -- ^ beta 479 | -> Auto Identity Int BoardOut -- ^ board Auto 480 | -> (Maybe Int, Bounder Double) -- ^ (best move, score) 481 | maxi ms maxP l α0 β0 a | l <= 0 = (Nothing, BIn 0) 482 | | otherwise = foldr f (Nothing, α0) ms 483 | where 484 | f :: Int -> (Maybe Int, Bounder Double) -> (Maybe Int, Bounder Double) 485 | f m' (m, α) = fromMaybe (m, α) $ do 486 | guard . not $ α >= β0 487 | guard . not $ _boFailed bout' 488 | guard $ α'' > α 489 | return (Just m', α'') 490 | where 491 | (bout', a') = stepAuto' a m' 492 | (_, α') = mini ms maxP (l - 1) α β0 a' 493 | α'' = maybe α' (score maxP) $ _boWinner bout' 494 | mini :: [Int] -> Player -> Int -> Bounder Double -> Bounder Double 495 | -> Auto Identity Int BoardOut -> (Maybe Int, Bounder Double) 496 | mini ms maxP l α0 β0 a | l <= 0 = (Nothing, BIn 0) 497 | | otherwise = foldr f (Nothing, β0) ms 498 | where 499 | f m' (m, β) = fromMaybe (m, β) $ do 500 | guard . not $ α0 >= β 501 | guard . not $ _boFailed bout' 502 | guard $ β'' < β 503 | return (Just m', β'') 504 | where 505 | (bout', a') = stepAuto' a m' 506 | (_, β') = maxi ms maxP (l - 1) α0 β a' 507 | β'' = maybe β' (score maxP) $ _boWinner bout' 508 | score cP (Just p) | p == cP = BMax 509 | | otherwise = BMin 510 | score _ Nothing = BIn (-100) 511 | -------------------------------------------------------------------------------- /src/Experimental/Survive.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE Arrows #-} 2 | {-# LANGUAGE DeriveFunctor #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE FlexibleContexts #-} 5 | {-# LANGUAGE FlexibleInstances #-} 6 | {-# LANGUAGE MonadComprehensions #-} 7 | {-# LANGUAGE ScopedTypeVariables #-} 8 | {-# LANGUAGE TemplateHaskell #-} 9 | {-# LANGUAGE TupleSections #-} 10 | {-# LANGUAGE ViewPatterns #-} 11 | 12 | module Main where 13 | 14 | import Control.Auto 15 | import Control.Auto.Blip 16 | import Control.Auto.Blip.Internal 17 | import Control.Auto.Collection 18 | import Control.Auto.Core 19 | import Control.Auto.Effects 20 | import Control.Auto.Interval 21 | import Control.Auto.Process.Random 22 | import Control.Auto.Run 23 | import Control.Lens 24 | import Control.Monad.Fix 25 | import Control.Monad.Random 26 | import Control.Monad.Reader hiding (forM_, mapM_) 27 | import Control.Monad.Writer hiding ((<>), forM_, mapM_) 28 | import Data.Foldable 29 | import Data.IntMap.Strict (IntMap, Key) 30 | import Data.List (sortBy) 31 | import Data.Map.Strict (Map) 32 | import Data.Maybe 33 | import Data.Ord 34 | import Data.Serialize 35 | import Debug.Trace 36 | import GHC.Generics hiding (to) 37 | import Linear hiding (ei, trace) 38 | import Prelude hiding ((.), id, elem, any, sequence, concatMap, sum, concat, sequence_, mapM_) 39 | import System.Console.ANSI 40 | import System.IO 41 | import Util 42 | import qualified Data.IntMap.Strict as IM 43 | import qualified Data.Map.Strict as M 44 | 45 | 46 | -- | Types for commands, entities, inputs, outputs, etc. 47 | 48 | -- direction 49 | data Dir = DUp | DRight | DDown | DLeft 50 | deriving (Show, Eq, Enum, Ord, Read, Generic) 51 | 52 | -- an action to perform 53 | data Action = Sword 54 | | Bow 55 | | Bomb 56 | | Wall 57 | deriving (Show, Eq, Enum, Ord, Read, Generic) 58 | 59 | -- an item to use 60 | data Item = Potion 61 | deriving (Show, Eq, Enum, Ord, Read, Generic) 62 | 63 | -- something you can pick up 64 | data Pickup = PUArrows 65 | | PUGunpowder 66 | | PUCement 67 | | PUPotion Double 68 | | PUGold Int 69 | deriving (Show, Eq, Ord, Read, Generic) 70 | 71 | -- a command from the outside world/interface 72 | data Cmd = CMove Dir 73 | | CAct Action Dir 74 | | CUse Item 75 | | CNop 76 | deriving (Show, Eq, Ord, Read, Generic) 77 | 78 | -- a way an Entity can respond to the world 79 | data EntResp = ERAtk Double Point -- attack with damage at relative position 80 | | ERShoot Double Int Dir -- shoot with damage and range in direction 81 | | ERBomb Dir -- bomb in direction 82 | | ERBuild Dir -- build in direction 83 | | ERFire Double Int Point -- start a fire with damage and duration in relative position 84 | | ERMonster Char Double Double Point -- create a monster with sprite with health 85 | -- and damage and absolute position 86 | | ERItem Pickup Point -- place an item with pickup at absolute position 87 | | ERGive Key Pickup -- give an entity with key/id a pickup 88 | deriving (Show, Eq, Ord, Read, Generic) 89 | 90 | -- communications an Entity can receive, from another 91 | data EntComm = ECAtk Double -- attack with damage 92 | | ECGive Pickup -- give pickup 93 | deriving (Show, Eq, Ord, Read, Generic) 94 | 95 | -- an entity existing on the map 96 | data Entity = EPlayer 97 | | EBomb 98 | | EWall 99 | | EFire 100 | | EMonster Char 101 | | EItem Pickup 102 | deriving (Show, Eq, Ord, Read, Generic) 103 | 104 | -- input for an Entity auto 105 | data EntityInput = EI { _eiPos :: Point -- new position 106 | , _eiComm :: [(Key, EntComm)] -- communications, from id's 107 | , _eiWorld :: EntityMap -- a map of the world 108 | } deriving (Show, Eq, Ord, Read, Generic) 109 | 110 | -- output for an Entity auto 111 | data EntityOutput a = EO { _eoData :: Maybe a -- extra data; Nothing if none. 112 | , _eoPos :: Point -- position to move from 113 | , _eoMove :: Point -- move 114 | , _eoEntity :: Entity -- the entity 115 | , _eoReact :: Map Entity Double -- "how this would react" when 116 | -- encountering various entities; 117 | -- how much damage it would attack with 118 | , _eoResps :: Maybe [EntResp] -- lists of responses to the world. 119 | -- Nothing if *dead* 120 | } deriving (Show, Eq, Ord, Read, Generic) 121 | 122 | -- output type from the player to the gui/frontend 123 | data PlayerOut = PO { _poMessages :: [OutMessage] -- status messages 124 | , _poHealth :: Double -- health 125 | , _poInventory :: Inventory -- inventory 126 | , _poKills :: Int -- kill count 127 | } deriving (Show, Eq, Ord, Read, Generic) 128 | 129 | -- player inventory, for purpose of PlayerOut rendering. not actaully used 130 | -- for the actual inventory updating of the player itself. 131 | data Inventory = Inv { _invArrows :: Int 132 | , _invGunpowder :: Int 133 | , _invCement :: Int 134 | , _invGold :: Int 135 | } deriving (Show, Eq, Ord, Read, Generic) 136 | 137 | -- a status message to the outside world 138 | data OutMessage = OMAtk Entity Entity Double -- attack from to damage 139 | | OMShot Entity Entity Double -- shot from to damage 140 | | OMMiss Entity -- shot missed by entity 141 | | OMDeath Entity -- entity dies 142 | | OMPickup Entity Pickup -- entity picked up picup 143 | deriving (Show, Eq, Ord, Read, Generic) 144 | 145 | type Point = V2 Int 146 | type GameMap = Map Point [Entity] 147 | type EntityMap = IntMap (Point, Entity) 148 | 149 | instance Serialize EntResp 150 | instance Serialize EntComm 151 | instance Serialize Dir 152 | instance Serialize Pickup 153 | instance Serialize Entity 154 | instance Serialize EntityInput 155 | instance Serialize a => Serialize (EntityOutput a) 156 | instance Serialize Cmd 157 | instance Serialize Item 158 | instance Serialize Action 159 | instance Serialize Inventory 160 | instance Serialize PlayerOut 161 | instance Serialize OutMessage 162 | 163 | instance Semigroup EntityInput where 164 | EI p0 c0 w0 <> EI p1 c1 w1 = EI (p0 `v` p1) (c0 ++ c1) (w0 <> w1) -- watch out, is (<>) right here? 165 | where 166 | v y (V2 (-1) (-1)) = y -- yeah this might not work 167 | v _ y = y 168 | 169 | instance Monoid EntityInput where 170 | mempty = EI (V2 (-1) (-1)) mempty mempty 171 | mappend = (<>) 172 | 173 | instance Semigroup Cmd where 174 | x <> CNop = x 175 | _ <> x = x 176 | 177 | instance Monoid Cmd where 178 | mempty = CNop 179 | mappend x CNop = x 180 | mappend _ x = x 181 | 182 | instance Semigroup PlayerOut where 183 | PO m1 h1 i1 k1 <> PO m2 h2 i2 k2 = PO (m1 ++ m2) (h1 <#> h2) (i1 <> i2) (k1 <#> k2) 184 | where 185 | x <#> (-1) = x 186 | _ <#> y = y 187 | 188 | instance Monoid PlayerOut where 189 | mempty = PO [] (-1) mempty (-1) 190 | mappend = (<>) 191 | 192 | instance Semigroup Inventory where 193 | Inv a1 g1 c1 r1 <> Inv a2 g2 c2 r2 = Inv (a1 <#> a2) (g1 <#> g2) (c1 <#> c2) (r1 <#> r2) 194 | where 195 | x <#> (-1) = x 196 | _ <#> y = y 197 | 198 | instance Monoid Inventory where 199 | mempty = Inv (-1) (-1) (-1) (-1) 200 | mappend = (<>) 201 | 202 | makePrisms ''Cmd 203 | makePrisms ''Pickup 204 | makePrisms ''Item 205 | makePrisms ''EntResp 206 | makePrisms ''EntComm 207 | makePrisms ''Entity 208 | makeLenses ''EntityInput 209 | makeLenses ''EntityOutput 210 | makeLenses ''PlayerOut 211 | makeLenses ''Inventory 212 | 213 | -- | Utility functions 214 | mapSize :: V2 Int 215 | mapSize = V2 70 20 216 | 217 | startPos :: V2 Int 218 | startPos = (`div` 2) <$> mapSize 219 | 220 | initialPO :: PlayerOut 221 | initialPO = PO [] initialHealth initialInv 0 222 | 223 | initialInv :: Inventory 224 | initialInv = Inv 50 10 30 0 225 | 226 | initialHealth :: Double 227 | initialHealth = 50 228 | 229 | dirToV2 :: Dir -> V2 Int 230 | dirToV2 dir = case dir of 231 | DUp -> V2 0 1 232 | DRight -> V2 1 0 233 | DDown -> V2 0 (-1) 234 | DLeft -> V2 (-1) 0 235 | 236 | v2ToDir :: V2 Int -> Maybe Dir 237 | v2ToDir v2 = case v2 of 238 | V2 0 1 -> Just DUp 239 | V2 1 0 -> Just DRight 240 | V2 0 (-1) -> Just DDown 241 | V2 (-1) 0 -> Just DLeft 242 | _ -> Nothing 243 | 244 | -- | Entity `Auto`s 245 | -- 246 | bomb :: Monad m 247 | => Dir 248 | -> Interval m EntityInput (EntityOutput a) 249 | bomb dir = proc ei -> do 250 | -- move constantly 251 | motion <- fromInterval zero . onFor 8 . pure (dirToV2 dir) -< () 252 | 253 | -- damage received 254 | let damage = sumOf (eiComm . traverse . _2 . _ECAtk) ei 255 | 256 | -- trigger: explosion from damage; fuse: explosion from timeout 257 | trigger <- became (<= 0) . sumFrom 2 -< negate damage 258 | fuse <- inB 10 -< 0 259 | 260 | -- explode when either `trigger` or `fuse` emit 261 | let explode = explodes <$ (fuse `mergeL` trigger) 262 | 263 | explosion <- fromBlips [] -< explode 264 | 265 | -- act like the EntityOutput until explosion; then just be on for 1. 266 | before -?> lmap fst (onFor 1) -< (EO Nothing (_eiPos ei) motion EBomb M.empty (Just explosion), explode) 267 | where 268 | explodes = do 269 | x <- [-3..3] 270 | y <- [-3..3] 271 | let r = sqrt (fromIntegral x**2 + fromIntegral y**2) :: Double 272 | guard $ r <= 3 273 | let dur | r < 1 = 2 274 | | r < 2 = 1 275 | | otherwise = 1 276 | str | r < 1 = 16 277 | | r < 2 = 8 278 | | r < 3 = 2 279 | | otherwise = 1 280 | return $ ERFire str dur (V2 x y) 281 | 282 | -- immediately just attack everything and die. 283 | fire :: Monad m 284 | => Double 285 | -> Int 286 | -> Interval m EntityInput (EntityOutput a) 287 | fire str dur = lmap (\ei -> EO Nothing (_eiPos ei) zero EFire M.empty (Just [ERAtk str zero])) (onFor dur) 288 | 289 | -- just sit there and do nothing. 290 | wall :: Monad m 291 | => Auto m EntityInput (EntityOutput a) 292 | wall = arr $ \ei -> EO Nothing (_eiPos ei) zero EWall M.empty (Just []) 293 | 294 | -- sit there and do nothing, but when the player steps on you, send them an 295 | -- `ERGive` response. 296 | itemPu :: Monad m => Pickup -> Point -> Interval m EntityInput (EntityOutput (Double, a)) 297 | itemPu pu p0 = proc ei -> do 298 | pos <- onFor 1 . pure p0 <|!> id -< _eiPos ei -- ignore first ei 299 | let pPos = preview (eiWorld . ix (-1) . _1) ei 300 | 301 | pickedB <- emitOn (uncurry (==)) -< (Just pos, pPos) 302 | picked <- fromBlips [] -< [ERGive (-1) pu] <$ pickedB 303 | 304 | let eOut = EO Nothing pos zero (EItem pu) M.empty (Just picked) 305 | 306 | naturalDeath <- inB 200 -< () 307 | 308 | before -?> dead -< (eOut, (() <$ pickedB) <> naturalDeath) 309 | where 310 | dead = lmap fst (onFor 1) -?> lmap (set eoResps Nothing . fst) (onFor 1) 311 | 312 | -- take an 'Auto' that never dies, and imbues it with health and death. 313 | -- teaches an 'Auto' how to die. 314 | withHealth :: MonadWriter ([OutMessage], Sum Int) m 315 | => Double 316 | -> Auto m EntityInput (EntityOutput (Double, a)) 317 | -> Interval m EntityInput (EntityOutput (Double, a)) 318 | withHealth h0 entA = proc ei -> do 319 | eOut <- entA -< ei 320 | let damage = sumOf (eiComm . traverse . _2 . _ECAtk) ei 321 | 322 | health <- sumFrom h0 -< negate damage 323 | 324 | -- set the EntityOutput data field to be its health 325 | let eOut' = set (eoData . _Just . _1) (max 0 health) eOut 326 | 327 | die <- became (<= 0) -< health 328 | 329 | -- send a mesage if a monster dies 330 | if has (eoEntity . _EMonster) eOut 331 | then arrMB tell -< ([OMDeath (_eoEntity eOut)], 1) <$ die 332 | else never -< () 333 | 334 | -- send a message if the player dies 335 | if has (eoEntity . _EPlayer) eOut 336 | then arrMB (tell . (,mempty)) -< [OMDeath (_eoEntity eOut)] <$ die 337 | else never -< () 338 | 339 | before -?> dead -< (eOut' , die) 340 | where 341 | dead = lmap (set eoResps Nothing . fst) (onFor 1) 342 | 343 | -- the player. move around, send out attacks, pick up recharges, drain 344 | -- inventory.... 345 | player :: MonadReader Cmd m -- environment is the current command 346 | => Auto m EntityInput (EntityOutput (Double, Inventory)) 347 | player = proc (EI p comm _) -> do 348 | inp <- effect ask -< () 349 | move <- fromBlips zero 350 | . modifyBlips dirToV2 351 | . emitJusts (preview _CMove) -< inp 352 | 353 | resps <- fromBlipsWith [] (:[]) 354 | . modifyBlips toResp 355 | . emitJusts (preview _CAct) -< inp 356 | 357 | arrowUsage <- emitJusts $ preview (traverse . _ERShoot) -< resps 358 | gunpowderUsage <- emitJusts $ preview (traverse . _ERBomb) -< resps 359 | cementUsage <- emitJusts $ preview (traverse . _ERBuild) -< resps 360 | 361 | getArrow <- emitOn (> 0) -< length . toListOf (traverse . _2 . _ECGive . _PUArrows) $ comm 362 | getGunpowder <- emitOn (> 0) -< length . toListOf (traverse . _2 . _ECGive . _PUGunpowder) $ comm 363 | getCement <- emitOn (> 0) -< length . toListOf (traverse . _2 . _ECGive . _PUCement) $ comm 364 | 365 | arrows <- scanPos (_invArrows initialInv) -< merge (+) ((-1) <$ arrowUsage) (15 <$ getArrow) 366 | gunpowder <- scanPos (_invGunpowder initialInv) -< merge (+) ((-1) <$ gunpowderUsage) ( 5 <$ getGunpowder) 367 | cement <- scanPos (_invCement initialInv) -< merge (+) ((-1) <$ cementUsage) (15 <$ getCement) 368 | 369 | gold <- sumFrom 0 -< sumOf (traverse . _2 . _ECGive . _PUGold) comm 370 | 371 | let resps' = filter (enough arrows gunpowder cement) resps 372 | 373 | id -< EO (Just (initialHealth, Inv arrows gunpowder cement gold)) p move EPlayer atkMap (Just resps') 374 | where 375 | toResp :: (Action, Dir) -> EntResp 376 | toResp (u,d) = case u of 377 | Sword -> ERAtk 4 (dirToV2 d) 378 | Bow -> ERShoot 1 15 d 379 | Bomb -> ERBomb d 380 | Wall -> ERBuild d 381 | atkMap = M.fromList . map (,4) $ [EWall, EMonster 'Z', EBomb] 382 | scanPos = scanB (\x y -> max 0 (x + y)) 383 | enough ar gp cm resp = case resp of 384 | ERAtk {} -> True 385 | ERShoot {} -> ar > 0 386 | ERBomb {} -> gp > 0 387 | ERBuild {} -> cm > 0 388 | _ -> True 389 | 390 | -- move towards the player if it exists, or move around randomly if not. 391 | monster :: MonadRandom m 392 | => Char 393 | -> Double 394 | -> Auto m EntityInput (EntityOutput a) 395 | monster c damg = proc ei -> do 396 | let pPos = ei ^? eiWorld . traverse . filtered (has (_2 . _EPlayer)) . _1 397 | mPos = _eiPos ei 398 | delta = (^-^ mPos) <$> pPos 399 | moves = flip fmap delta $ \(V2 dx dy) -> 400 | let adx = abs dx 401 | ady = abs dy 402 | in (V2 (signum dx) 0 <$ guard (adx /= 0)) 403 | <|> (V2 0 (signum dy) <$ guard (ady /= 0)) 404 | 405 | move <- during (arrM uniform) -< moves 406 | wander <- effect (dirToV2 `liftM` uniform [DUp ..]) -< () 407 | 408 | let move' = fromMaybe wander move 409 | 410 | id -< EO Nothing mPos move' (EMonster c) atkMap (Just []) 411 | where 412 | atkMap = M.fromList . map (,damg) $ [EPlayer, EWall, EBomb] 413 | 414 | -- the main game loop 415 | game :: MonadFix m 416 | => StdGen 417 | -> Auto m Cmd (PlayerOut, GameMap) 418 | game g = proc inp -> do 419 | -- run game', get the outputs, , count kills, save the last output, 420 | -- output to the client. 421 | (((eo, _), gm), (msgs, newKills)) <- game' -< inp 422 | kills <- sumFrom 0 -< getSum newKills 423 | lastEoDat <- holdJusts 424 | <|!> pure (initialHealth, initialInv) -< _eoData =<< eo 425 | let (hlth, inv) = lastEoDat 426 | let po = PO msgs hlth inv kills 427 | id -< (po, gm) 428 | where 429 | -- run the Writer and the Random over 'bracketA playerA worldA' 430 | -- "bracketA" runs player, then world, then player, so that the player 431 | -- gets a chance to "clean up". 432 | -- bracketA :: Auto m (Either a b) c -> Auto m c b -> Auto m a c runs 433 | -- the first on the `a` Right input, feeds the `c` into the second, 434 | -- runs the `b` output onto the first's Left channel, and outputs the 435 | -- final `c`. 436 | game' = runWriterA (sealRandomStd (bracketA playerA worldA) g) 437 | playerA :: (MonadFix m, MonadWriter ([OutMessage], Sum Int) m) 438 | => Auto m (Either Cmd EntityInput) 439 | ( ( Maybe (EntityOutput (Double, Inventory)) 440 | , IntMap EntityInput 441 | ) 442 | , GameMap 443 | ) 444 | -- manage the player input and wrapping the `player` Auto 445 | playerA = proc inp -> do 446 | -- last received world is the last world received from `Right` 447 | lastWorld <- holdWith IM.empty . emitJusts (preview (_Right . eiWorld)) -< inp 448 | rec lastPos <- delay startPos -< currPos 449 | -- new entity input for player 450 | let ei = set eiPos lastPos . either (const mempty) id $ inp 451 | -- run it through player', with the input 452 | pEo <- player' -< (ei, either id (const CNop) inp) 453 | -- generate the resulting entity inputs for everyone else, and 454 | -- messages 455 | let (pEis, msgs) = IM.mapAccumWithKey (mkEntIns lastWorld) IM.empty $ maybe IM.empty (IM.singleton (-1)) pEo 456 | 457 | -- keep the current position; move when the player intputs ask 458 | -- the player to move 459 | currPos <- holdWith startPos . emitJusts (preview (ix (-1) . eiPos)) -< pEis 460 | 461 | -- log the messages; messages are ([OutMessage], Sum Int) (kill count) 462 | arrM (tell . (,mempty)) -< toListOf (traverse . traverse) msgs 463 | 464 | let outEo = set (_Just . eoPos) currPos pEo 465 | outEi = IM.delete (-1) pEis 466 | outGm = either (const M.empty) (mkGMap lastPos . _eiWorld) inp 467 | 468 | id -< ((outEo, outEi), outGm) 469 | where 470 | -- imbue position, health, and take an extra parameter as the 471 | -- Reader environment 472 | player' = runReaderA . booster startPos . withHealth initialHealth $ player 473 | mkGMap p = M.fromListWith (<>) 474 | . IM.elems 475 | . (fmap . second) (:[]) 476 | . IM.insert (-1) (p, EPlayer) 477 | 478 | -- the rest of the world 479 | worldA :: (MonadFix m, MonadWriter ([OutMessage], Sum Int) m, MonadRandom m) 480 | => Auto m ( ( Maybe (EntityOutput (Double, a)) 481 | , IntMap EntityInput 482 | ), GameMap 483 | ) 484 | EntityInput 485 | worldA = proc ((pEo, pEis), _) -> do 486 | -- make things... monsters and items 487 | mkMonsters <- makeMonsters 25 -< () 488 | mkItems <- makeItems 15 -< () 489 | 490 | -- run all of the entities on all of the inputs, using dynMapF 491 | rec entOuts <- dynMapF makeEntity mempty -< ( -- inputs from player and inputs from entities 492 | IM.unionWith (<>) pEis entInsD' 493 | -- make-new-entity events from everywhere 494 | , newEntsBAll <> mkMonsters <> mkItems 495 | ) 496 | 497 | -- only alive 498 | let entOutsAlive = IM.filter (has (eoResps . _Just)) entOuts 499 | -- alive + player entity output 500 | entOutsFull = maybe entOutsAlive (\po -> IM.insert (-1) po entOutsAlive) pEo 501 | -- map of all locations and entities 502 | entMap = (_eoPos &&& _eoEntity) <$> entOutsFull 503 | -- generate new entity inputs from the entity outputs 504 | (entIns,msgs) = IM.mapAccumWithKey (mkEntIns entMap) IM.empty entOutsAlive 505 | -- update entity maps 506 | entMap' = maybe id (\po -> IM.insert (-1) (_eoPos po, EPlayer)) pEo 507 | . flip IM.mapMaybeWithKey entIns $ \k ei -> do 508 | eo <- IM.lookup k entOutsFull 509 | return (_eiPos ei, _eoEntity eo) 510 | entIns' = flip IM.mapWithKey entIns $ \k -> set eiWorld (IM.delete k entMap') 511 | 512 | -- new entities, to send in as blip stream 513 | newEnts = toList entOutsAlive >>= \(EO _ p _ _ _ ers) -> maybe [] (map (p,)) ers 514 | 515 | -- EntResps from player 516 | plrEResps = toListOf (_Just . eoResps . _Just . traverse) pEo 517 | plrEResps' = case pEo of 518 | Nothing -> [] 519 | Just po -> (_eoPos po,) <$> plrEResps 520 | 521 | -- emit all non-empty newEnts, from "last cycle" 522 | newEntsB <- lagBlips . emitOn (not . null) -< newEnts 523 | -- all entity inputs from last cycle, to send into `entOuts` 524 | entInsD <- delay IM.empty -< entIns' 525 | -- add in the player entity to the world maps 526 | let entInsD' = case pEo of 527 | Just po -> over (traverse . eiWorld) (IM.insert (-1) (_eoPos po, EPlayer)) entInsD 528 | Nothing -> entInsD 529 | 530 | playerB <- emitOn (not . null) -< plrEResps' 531 | 532 | let newEntsBAll = newEntsB <> playerB 533 | 534 | -- write messages to log; messages are ([OutMessage], Sum Int) (kill count) 535 | arrM (tell . (,mempty)) -< toListOf (traverse . traverse) msgs 536 | 537 | id -< set eiWorld (IM.delete (-1) entMap') . IM.findWithDefault mempty (-1) $ entIns' 538 | where 539 | makeMonsters :: MonadRandom m => Int -> Auto m a (Blip [(Point, EntResp)]) 540 | makeMonsters n = onFor 500 . perBlip makeMonster . every n 541 | --> makeMonsters ((n * 3) `div` 4) 542 | makeMonster :: MonadRandom m => Auto m a [(Point, EntResp)] 543 | makeMonster = liftA2 (\x y -> [(zero, ERMonster 'Z' 5 5 (shift (V2 x y)))]) 544 | (effect (getRandomR (0, view _x mapSize `div` 2))) 545 | (effect (getRandomR (0, view _y mapSize `div` 2))) 546 | where 547 | shift = liftA2 (\m x -> (x - (m `div` 4)) `mod` m) mapSize 548 | 549 | makeItems :: MonadRandom m => Double -> Auto m a (Blip [(Point, EntResp)]) 550 | makeItems r = perBlip makeItem . bernoulliMR (1/r) 551 | where 552 | makeItem = liftA3 (\x y i -> [(zero, ERItem i (shift (V2 x y)))]) 553 | (effect (getRandomR (0, 2 * view _x mapSize `div` 3))) 554 | (effect (getRandomR (0, 2 * view _y mapSize `div` 3))) 555 | (effect randomItem) 556 | shift = liftA2 (\m x -> (x + (m `div` 6))) mapSize 557 | randomItem = do 558 | x <- fromList [ (PUArrows, 1.5) 559 | , (PUGunpowder, 1) 560 | , (PUCement, 1) 561 | , (PUPotion 0, 1) 562 | , (PUGold 0, 1) 563 | ] 564 | case x of 565 | PUGold _ -> PUGold `liftM` getRandomR (5,15) 566 | PUPotion _ -> PUPotion `liftM` getRandomR (10,40) 567 | _ -> return x 568 | 569 | -- start off at give position 570 | booster p0 a = (onFor 1 . arr (set (_Just . eoPos) p0) --> id) . a 571 | 572 | -- generating entity inputs from entity outputs of last round. kinda 573 | -- complicated, but this is the beef of the game logic, having every 574 | -- entity communicate with every other one. run using 575 | -- `IM.mapAccumWithKey` 576 | mkEntIns :: EntityMap -- world map 577 | -> IntMap EntityInput -- current "output" map, in-progress 578 | -> Key -- key of this processed entity 579 | -> EntityOutput a -- entity output of this processed entity 580 | -> (IntMap EntityInput, [OutMessage]) -- updated "output" map, and also communications 581 | mkEntIns em eis k (EO _ pos0 mv e react (Just resps)) = (IM.insertWith (<>) k res withGives, messages) 582 | where 583 | em' = IM.delete k em 584 | pos1 = pos0 ^+^ mv 585 | oldCols = IM.mapMaybe (\(p,e') -> e' <$ guard (p == pos1)) em' 586 | newCols = flip IM.mapMaybeWithKey eis $ \k' ei -> do 587 | guard (_eiPos ei == pos1) 588 | snd <$> IM.lookup k' em' 589 | allCols = oldCols <> newCols 590 | pos2 | any isBlocking allCols = pos0 591 | | otherwise = clamp pos1 -- could be short circuited here, really... 592 | colAtks = flip IM.mapMaybe allCols $ \e' -> do 593 | d <- M.lookup e' react 594 | return (over eiComm ((k, ECAtk d):) mempty, [OMAtk e e' d]) 595 | respAtks = IM.unionsWith (<>) . flip mapMaybe resps $ \r -> 596 | case r of 597 | ERAtk a _ -> 598 | let placed = place pos2 r 599 | oldHits = snd <$> IM.filter (\(p,_) -> placed == p) em' 600 | newHits = flip IM.mapMaybeWithKey eis $ \k' ei -> do 601 | guard (placed == _eiPos ei) 602 | snd <$> IM.lookup k' em 603 | allHits = oldHits <> newHits 604 | in Just $ (\e' -> (set eiComm [(k, ECAtk a)] mempty, [OMAtk e e' a])) <$> allHits 605 | ERShoot a rg d -> -- TODO: drop when miss 606 | let rg' = fromIntegral rg 607 | oldHits = flip IM.mapMaybe em' $ \(p, e') -> do 608 | guard $ arrowHit e' 609 | dst <- aligned pos2 p d 610 | dst <$ guard (dst <= rg') 611 | newHits = flip IM.mapMaybeWithKey eis $ \k' ei -> do 612 | guard $ arrowHit (snd (em IM.! k')) 613 | dst <- aligned pos2 (_eiPos ei) d 614 | dst <$ guard (dst <= rg') 615 | allHits = oldHits <> newHits 616 | minHit = fst . minimumBy (comparing snd) $ IM.toList allHits 617 | in Just $ if IM.null allHits 618 | then IM.singleton k (mempty, [OMMiss e]) 619 | else IM.singleton minHit (set eiComm [(k, ECAtk a)] mempty, [OMShot e (snd (em IM.! minHit)) a]) 620 | _ -> 621 | Nothing 622 | 623 | respGives = IM.unionsWith (<>) . flip mapMaybe resps $ \r -> 624 | case r of 625 | ERGive k' pu -> Just $ IM.singleton k' (set eiComm [(k, ECGive pu)] mempty, [OMPickup (snd (em IM.! k')) pu]) 626 | _ -> Nothing 627 | 628 | allAtks = colAtks <> respAtks 629 | messages = toListOf (traverse . traverse) 630 | $ IM.unionWith (<>) (snd <$> allAtks) (snd <$> respGives) 631 | 632 | withAtks = IM.unionWith (<>) (fst <$> IM.delete k allAtks) eis 633 | withGives = IM.unionWith (<>) (fst <$> respGives) withAtks 634 | res = EI pos2 [] em' 635 | isBlocking ent = case ent of 636 | EPlayer -> True 637 | EWall -> True 638 | EBomb -> True 639 | EFire -> False 640 | EMonster _ -> True 641 | EItem _ -> False 642 | aligned :: Point -> Point -> Dir -> Maybe Double 643 | aligned p0 p1 dir = norm r <$ guard (abs (dotted - 1) < 0.001) 644 | where 645 | r = fmap fromIntegral (p1 - p0) :: V2 Double 646 | rUnit = normalize r 647 | dotted = rUnit `dot` fmap fromIntegral (dirToV2 dir) 648 | arrowHit :: Entity -> Bool 649 | arrowHit ent = case ent of 650 | EPlayer -> True 651 | EWall -> False 652 | EBomb -> True 653 | EFire -> False 654 | EMonster _ -> True 655 | EItem _ -> False 656 | mkEntIns _ eis _ _ = (eis, []) 657 | clamp = liftA3 (\mn mx -> max mn . min mx) (V2 0 0) mapSize 658 | 659 | -- make entity from EntResp 660 | makeEntity :: (MonadRandom m, MonadWriter ([OutMessage], Sum Int) m) 661 | => (Point, EntResp) 662 | -> Interval m EntityInput (EntityOutput (Double, a)) 663 | makeEntity (p, er) = case er of 664 | ERBomb dir -> booster placed $ bomb dir 665 | ERBuild {} -> booster placed . withHealth 50 $ wall 666 | ERMonster c h d _ -> booster placed . withHealth h $ monster c d 667 | ERFire s d _ -> booster placed $ fire s d 668 | ERItem pu pos -> itemPu pu pos 669 | ERAtk {} -> off 670 | ERShoot {} -> off 671 | ERGive {} -> off 672 | where 673 | placed = place p er 674 | 675 | -- where to place entity, given initial point and resp? 676 | place :: Point -> EntResp -> Point 677 | place p er = case er of 678 | ERAtk _ disp -> p ^+^ disp 679 | ERBomb {} -> p 680 | ERBuild dir -> p ^+^ dirToV2 dir 681 | ERShoot _ _ dir -> p ^+^ dirToV2 dir 682 | ERFire _ _ d -> p ^+^ d 683 | ERMonster _ _ _ p' -> p' 684 | ERItem _ p' -> p' 685 | ERGive {} -> zero 686 | 687 | 688 | -- handle command stream 689 | handleCmd :: (Serialize b, Monoid b, Monad m) 690 | => Auto m Cmd b 691 | -> Auto m (Maybe Cmd) b 692 | handleCmd a0 = holdWith mempty . perBlip a0 . onJusts 693 | 694 | -- render the board 695 | renderBoard :: (PlayerOut, GameMap) -> String 696 | renderBoard (PO msgs ph (Inv ar gp cm gd) k, mp) = 697 | unlines . concat $ [ map renderOM msgs 698 | , "You dead!" <$ guard (ph <= 0) 699 | , ["[1] Sword\t[2] Bow (" ++ show ar ++ ")\t[3] Bomb (" ++ show gp ++ ")\t[4] Wall (" ++ show cm ++ ")"] 700 | , mapOut 701 | , ["Health: " ++ show (round ph :: Int) ++ "\tKills: " ++ show k ++ "\tGold: " ++ show gd] 702 | ] 703 | where 704 | renderOM om = case om of 705 | OMAtk e1 e2 d -> [entChr e1] ++ " attacked " ++ [entChr e2] ++ " for " ++ show d ++ " HP" 706 | OMShot e1 e2 d -> [entChr e1] ++ " shot " ++ [entChr e2] ++ " for " ++ show d ++ " HP" 707 | OMMiss e1 -> "Shot from " ++ [entChr e1] ++ " missed!" 708 | OMDeath e1 -> [entChr e1] ++ " died" 709 | OMPickup e1 pu -> [entChr e1] ++ " picked up " ++ showPu pu 710 | mapOut = reverse [[ charAt x y | x <- [0..xMax] ] | y <- [0..yMax]] 711 | charAt x y = fromMaybe '.' $ do 712 | es <- M.lookup (V2 x y) mp 713 | let es' | ph > 0 = es 714 | | otherwise = filter (/= EPlayer) es 715 | fmap entChr . listToMaybe . sortBy (comparing entPri) $ es' 716 | xMax = view _x mapSize 717 | yMax = view _y mapSize 718 | entChr e = case e of 719 | EPlayer -> '@' 720 | EBomb -> 'o' 721 | EWall -> '#' 722 | EFire -> '"' 723 | EMonster c -> c 724 | EItem pu -> puChr pu 725 | entPri e = case e of 726 | EPlayer -> 0 :: Int 727 | EFire -> 1 728 | EMonster _ -> 2 729 | EBomb -> 4 730 | EItem _ -> 5 731 | EWall -> 6 732 | puChr pu = case pu of 733 | PUArrows -> '>' 734 | PUGunpowder -> '%' 735 | PUCement -> '=' 736 | PUPotion _ -> '?' 737 | PUGold _ -> '*' 738 | showPu pu = case pu of 739 | PUArrows -> "arrows" 740 | PUGunpowder -> "gunpowder" 741 | PUCement -> "cement" 742 | PUPotion _ -> "an unimplemented potion" 743 | PUGold amt -> show amt ++ " gold" 744 | 745 | 746 | -- primitive command parser 747 | parseCmd :: Auto m Char (Blip (Maybe Cmd)) 748 | parseCmd = go Nothing 749 | where 750 | go Nothing = mkAuto_ $ \x -> case x of 751 | 'h' -> (Blip (Just (CMove DLeft )) , go Nothing ) 752 | 'j' -> (Blip (Just (CMove DDown )) , go Nothing ) 753 | 'k' -> (Blip (Just (CMove DUp )) , go Nothing ) 754 | 'l' -> (Blip (Just (CMove DRight)) , go Nothing ) 755 | '5' -> (Blip (Just (CUse Potion )) , go Nothing ) 756 | ' ' -> (Blip (Just CNop) , go Nothing ) 757 | '1' -> (NoBlip , go (Just Sword)) 758 | '2' -> (NoBlip , go (Just Bow )) 759 | '3' -> (NoBlip , go (Just Bomb )) 760 | '4' -> (NoBlip , go (Just Wall )) 761 | _ -> (Blip Nothing , go Nothing ) 762 | go (Just u) = mkAuto_ $ \x -> case x of 763 | 'h' -> (Blip (Just (CAct u DLeft )), go Nothing ) 764 | 'j' -> (Blip (Just (CAct u DDown )), go Nothing ) 765 | 'k' -> (Blip (Just (CAct u DUp )), go Nothing ) 766 | 'l' -> (Blip (Just (CAct u DRight)), go Nothing ) 767 | _ -> (Blip Nothing , go Nothing ) 768 | 769 | main :: IO () 770 | main = do 771 | g <- newStdGen 772 | hSetBuffering stdin NoBuffering 773 | renderStdout (initialPO, M.singleton startPos [EPlayer]) 774 | _ <- runM generalize getChar process $ hold 775 | . perBlip (handleCmd (game g)) 776 | . parseCmd 777 | return () 778 | where 779 | renderStdout mp = do 780 | clearScreen 781 | putStrLn "" 782 | putStrLn (renderBoard mp) 783 | process mp' = do 784 | mapM_ renderStdout mp' 785 | Just <$> getChar 786 | 787 | -- turn Identity into IO 788 | generalize :: Monad m => Identity a -> m a 789 | generalize = return . runIdentity 790 | --------------------------------------------------------------------------------