wonky but working first pass at a table-rolling application
Getty Ritter
6 years ago
1 | dist | |
2 | dist-* | |
3 | *~ | |
4 | cabal-dev | |
5 | *.o | |
6 | *.hi | |
7 | *.chi | |
8 | *.chs.h | |
9 | *.dyn_o | |
10 | *.dyn_hi | |
11 | .hpc | |
12 | .hsenv | |
13 | .cabal-sandbox/ | |
14 | cabal.sandbox.config | |
15 | *.prof | |
16 | *.aux | |
17 | *.hp | |
18 | *.eventlog | |
19 | cabal.project.local | |
20 | .ghc.environment.* |
1 | dungeon | |
2 | size | |
3 | 1-3: small (2 themes, 6 areas) | |
4 | 4-9: medium (3 themes, 12 areas) | |
5 | 10-11: large (4 themes, 16 aras) | |
6 | 12: huge (5 themes, 24 areas) | |
7 | discovery | |
8 | 1: unthemed, common, empty | |
9 | 2: unthemed, common, danger | |
10 | 3-4: unthemed, common, danger + discovery | |
11 | 5-6: unthemed, common, discovery | |
12 | ||
13 | 7: themed, common, danger | |
14 | 8: themed, common, danger + discovery | |
15 | 9: themed, common, discovery | |
16 | 10: themed, unique, danger | |
17 | 11: themed, unique, danger + discovery | |
18 | 12: themed, unique, discovery | |
19 | ruination | |
20 | 1: arcane disaster | |
21 | 2: damnation/curse | |
22 | 3-4: earthquate/fire/flood | |
23 | 5-6: plague/famine/drought | |
24 | 7-8: overrun by monsters | |
25 | 9-10: war/invasion | |
26 | 11: depleted resources | |
27 | 12: better prospects elsewhere | |
28 | ||
29 | foundation | |
30 | builder | |
31 | 1: aliens/precursors | |
32 | 2: demigod/demon | |
33 | 3-4: natural (caves etc.) | |
34 | 5: religious order/cult | |
35 | 6-7: humanoid | |
36 | 8-9: dwarves/gnomes | |
37 | 10: elves | |
38 | 11: wizard/madman | |
39 | 12: monarch/warlord | |
40 | function | |
41 | 1: source/portal | |
42 | 2: mine | |
43 | 3-4: tomb/crypt | |
44 | 5: prison | |
45 | 6-7: lair/den/hideout | |
46 | 8-9: stronghold/sanctuary | |
47 | 10: shrine/temple/oracle | |
48 | 11: archive/library | |
49 | 12: unknown/mystery | |
50 | ||
51 | theme | |
52 | 1-5: @dungeon/theme/mundane | |
53 | 6-9: @dungeon/theme/unusual | |
54 | 10-12: @dungeon/theme/extraordinary | |
55 | mundane | |
56 | 1: rot/decay | |
57 | 2: torture/agony | |
58 | 3: madness | |
59 | 4: all is lost | |
60 | 5: noble sacrifice | |
61 | 6: savage fury | |
62 | 7: survival | |
63 | 8: criminal activity | |
64 | 9: secrets/treachery | |
65 | 10: tricks and traps | |
66 | 11: invasion/infestation | |
67 | 12: factions at war | |
68 | unusual | |
69 | 1: creation/invention | |
70 | 2: element | |
71 | 3: knowledge/learning | |
72 | 4: growth/expansion | |
73 | 5: deepening mystery | |
74 | 6: transformation/change | |
75 | 7: chaos and destruction | |
76 | 8: shadowy forces | |
77 | 9: forbidden knowledge | |
78 | 10: poison/disease | |
79 | 11: corruption/blight | |
80 | 12: impending disaster | |
81 | extraordinary | |
82 | 1: scheming evil | |
83 | 2: divination/scrying | |
84 | 3: blasphemy | |
85 | 4: arcane research | |
86 | 5: occult forces | |
87 | 6: an ancient curse | |
88 | 7: mutation | |
89 | 8: the unquiet dead | |
90 | 9: bottomless hunger | |
91 | 10: incredible power | |
92 | 11: unspeakable horrors | |
93 | 12: holy war | |
94 | ||
95 | discovery | |
96 | 1-3: @dungeon/discovery/dressing | |
97 | dressing | |
98 | 1: junk/debris | |
99 | 2: tracks/marks | |
100 | 3: signs of battle | |
101 | 4: writing/carving | |
102 | 5: warning | |
103 | 6: dead creature | |
104 | 7: bones/remains | |
105 | 8: book/scroll/map | |
106 | 9: broken door/wall | |
107 | 10: breeze/wind/smell | |
108 | 11: lichen/moss/fungus | |
109 | 12: @details/oddity | |
110 | ||
111 | details | |
112 | oddity | |
113 | 1: weird color/smell/sound | |
114 | 2: geometric | |
115 | 3: web/network/system | |
116 | 4: crystalline/glass-like | |
117 | 5: fungal | |
118 | 6: gaseous/smokey | |
119 | 7: mirage/illusion | |
120 | 8: volcanic/explosive | |
121 | 9: magnetic/repellant | |
122 | 10: devoid of life | |
123 | 11: unexpectedly alive | |
124 | 12: roll twice |
1 | name: potrero | |
2 | version: 0.1.0.0 | |
3 | license: BSD3 | |
4 | author: Getty Ritter <gettylefou@gmail.com> | |
5 | maintainer: Getty Ritter <gettylefou@gmail.com> | |
6 | copyright: @2018 Getty Ritter | |
7 | build-type: Simple | |
8 | cabal-version: >=1.14 | |
9 | ||
10 | executable potrero | |
11 | hs-source-dirs: src | |
12 | main-is: Main.hs | |
13 | other-modules: Types | |
14 | Parser | |
15 | default-language: Haskell2010 | |
16 | ghc-options: -Wall | |
17 | build-depends: base >=4.7 && <5 | |
18 | , containers | |
19 | , random | |
20 | , readline | |
21 | , text |
1 | {-# LANGUAGE OverloadedStrings #-} | |
2 | {-# LANGUAGE OverloadedLists #-} | |
3 | {-# LANGUAGE TypeFamilies #-} | |
4 | ||
5 | module Main where | |
6 | ||
7 | import qualified Control.Monad as M | |
8 | import qualified Data.IORef as IO | |
9 | import qualified Data.Map.Strict as Map | |
10 | import Data.Monoid ((<>)) | |
11 | import qualified Data.Text as Text | |
12 | import qualified Data.Text.IO as Text | |
13 | import qualified System.Console.Readline as Readline | |
14 | import qualified System.Exit as Exit | |
15 | ||
16 | import qualified Parser | |
17 | import qualified Types | |
18 | ||
19 | readMap :: FilePath -> IO Types.TableMap | |
20 | readMap path = do | |
21 | cs <- Text.readFile path | |
22 | pure $ Map.fromList | |
23 | [ (Types.tableName t, t) | |
24 | | t <- Parser.parseTable cs | |
25 | ] | |
26 | ||
27 | main :: IO () | |
28 | main = do | |
29 | tablesRef <- IO.newIORef =<< readMap "tables.txt" | |
30 | Readline.setCompletionEntryFunction $ Just $ \ rs -> do | |
31 | tables <- IO.readIORef tablesRef | |
32 | pure [ Text.unpack k | |
33 | | k <- Map.keys tables | |
34 | , Text.pack rs `Text.isPrefixOf` k | |
35 | ] | |
36 | M.forever $ do | |
37 | input <- Readline.readline "\x1b[31m--> \x1b[39m" | |
38 | case input of | |
39 | Nothing -> do | |
40 | putStrLn "farewell" | |
41 | Exit.exitSuccess | |
42 | Just "" -> pure () | |
43 | Just ":l" -> do | |
44 | tables <- IO.readIORef tablesRef | |
45 | Text.putStrLn "Available tables: " | |
46 | Text.putStrLn (" " <> Text.unwords (Map.keys tables)) | |
47 | IO.writeIORef tablesRef =<< readMap "tables.txt" | |
48 | Just ":r" -> | |
49 | IO.writeIORef tablesRef =<< readMap "tables.txt" | |
50 | Just choice -> do | |
51 | tables <- IO.readIORef tablesRef | |
52 | let names = Text.unwords (Map.keys tables) | |
53 | Readline.addHistory choice | |
54 | case Map.lookup (Text.strip (Text.pack choice)) tables of | |
55 | Nothing -> do | |
56 | Text.putStrLn ("table not found: " <> Text.pack (show choice)) | |
57 | Text.putStrLn (" valid tables include: " <> names) | |
58 | Just t -> Types.rollTable tables t |
1 | {-# LANGUAGE OverloadedStrings #-} | |
2 | ||
3 | module Parser where | |
4 | ||
5 | import Data.Char as Char | |
6 | import qualified Data.Text as Text | |
7 | ||
8 | import Types | |
9 | ||
10 | data LineType | |
11 | = TableDecl Int Text.Text | |
12 | | TableEntry Range Result | |
13 | deriving (Eq, Show) | |
14 | ||
15 | indentAmount :: Text.Text -> Int | |
16 | indentAmount = Text.length . Text.takeWhile Char.isSpace | |
17 | ||
18 | parseRange :: Text.Text -> Range | |
19 | parseRange t | |
20 | | Text.all Char.isNumber (Text.strip t) = | |
21 | let n = read (Text.unpack t) in Range n n | |
22 | | otherwise = | |
23 | let (x, y) = Text.breakOn "-" (Text.strip t) | |
24 | n = read (Text.unpack x) | |
25 | m = read (Text.unpack (Text.tail y)) | |
26 | in Range n m | |
27 | ||
28 | parseResult :: Text.Text -> Result | |
29 | parseResult t | |
30 | | "@" `Text.isPrefixOf` Text.strip t = | |
31 | ResultRoll (Text.tail (Text.strip t)) | |
32 | | otherwise = | |
33 | ResultText (Text.strip t) | |
34 | ||
35 | parseLines :: [Text.Text] -> [LineType] | |
36 | parseLines = go | |
37 | where | |
38 | go [] = [] | |
39 | go (t:ts) | |
40 | | Text.all Char.isSpace t = go ts | |
41 | | Text.any (== ':') t = | |
42 | let (rangeTxt, message) = Text.breakOn ":" t | |
43 | range = parseRange rangeTxt | |
44 | msg = parseResult (Text.tail message) | |
45 | in TableEntry range msg : go ts | |
46 | | otherwise = | |
47 | TableDecl (indentAmount t) (Text.strip t) : go ts | |
48 | ||
49 | parseTable :: Text.Text -> [Table] | |
50 | parseTable = parseTop [] . parseLines . Text.lines | |
51 | where | |
52 | parseTop _ [] = [] | |
53 | parseTop ctx (TableDecl n name:xs) = | |
54 | parseTop ((n, name) : [ c | c <- ctx, fst c < n]) xs | |
55 | parseTop ctx (TableEntry r m:xs) = | |
56 | let (table, rest) = gatherEntries ctx xs [(r, m)] | |
57 | in table : parseTop ctx rest | |
58 | gatherEntries ctx (TableEntry r m:xs) es = | |
59 | gatherEntries ctx xs ((r, m) : es) | |
60 | gatherEntries ctx rs es = | |
61 | let name = Text.intercalate "/" (reverse (map snd ctx)) | |
62 | in (Table name (reverse es), rs) |
1 | {-# LANGUAGE OverloadedStrings #-} | |
2 | ||
3 | module Types where | |
4 | ||
5 | import qualified Data.Map.Strict as Map | |
6 | import qualified Data.Text as Text | |
7 | import qualified Data.Text.IO as Text | |
8 | import qualified System.Random as Rand | |
9 | ||
10 | data Range = Range { rFrom :: Int, rTo :: Int } | |
11 | deriving (Eq, Show) | |
12 | ||
13 | type TableMap = Map.Map Text.Text Table | |
14 | ||
15 | data Table = Table | |
16 | { tableName :: Text.Text | |
17 | , tableChoices :: [(Range, Result)] | |
18 | } deriving (Eq, Show) | |
19 | ||
20 | data Result | |
21 | = ResultText Text.Text | |
22 | | ResultRoll Text.Text | |
23 | deriving (Eq, Show) | |
24 | ||
25 | computeResult :: Int -> TableMap -> Result -> IO () | |
26 | computeResult r _ (ResultText msg) = do | |
27 | Text.putStr ("\x1b[36m" <> Text.pack (show r) <> ":\x1b[39m ") | |
28 | Text.putStrLn msg | |
29 | computeResult r ts (ResultRoll name) | |
30 | | Just t <- Map.lookup name ts = do | |
31 | Text.putStr ("\x1b[36m" <> Text.pack (show r)) | |
32 | Text.putStrLn (": (roll " <> name <> ")\x1b[39m") | |
33 | rollTable ts t | |
34 | | otherwise = Text.putStrLn ("error: no such table: " <> name) | |
35 | ||
36 | tableDie :: Table -> Int | |
37 | tableDie t = maximum [ x | (Range _ x, _) <- tableChoices t ] | |
38 | ||
39 | rollTable :: TableMap -> Table -> IO () | |
40 | rollTable tables t = do | |
41 | roll <- Rand.randomRIO (1, tableDie t) | |
42 | case [ result | |
43 | | (range, result) <- tableChoices t | |
44 | , roll >= rFrom range && roll <= rTo range | |
45 | ] of | |
46 | [choice] -> computeResult roll tables choice | |
47 | _ -> Text.putStrLn $ Text.unwords | |
48 | [ "bad table " | |
49 | , tableName t | |
50 | , "(roll of" | |
51 | , Text.pack (show roll) | |
52 | , "has no matching result)" | |
53 | ] |