From 83659e69b45b84ed19e038356f5d4330200d5459 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 17 Dec 2022 23:14:04 +0100 Subject: [PATCH] #18 #14 Inital parser commit Added basic parser functionality & tests for these functionalites. Split tests in several files --- README.md | 249 ++++++++++++++++++++++++++++++++++++++- lib/control/Parse.hs | 132 +++++++++++++++++++++ rpg-engine.cabal | 13 +- stack.yaml | 1 + test/InteractionSpec.hs | 9 ++ test/ParsedToGameSpec.hs | 52 ++++++++ test/ParserSpec.hs | 52 ++++++++ test/RPG-Engine-Test.hs | 7 -- test/RPGEngineSpec.hs | 1 + 9 files changed, 504 insertions(+), 12 deletions(-) create mode 100644 lib/control/Parse.hs create mode 100644 test/InteractionSpec.hs create mode 100644 test/ParsedToGameSpec.hs create mode 100644 test/ParserSpec.hs delete mode 100644 test/RPG-Engine-Test.hs create mode 100644 test/RPGEngineSpec.hs diff --git a/README.md b/README.md index 5f92037..49869ed 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,250 @@ # RPG-Engine -Schrijf een game-engine voor een rollenspel \ No newline at end of file +Schrijf een game-engine voor een rollenspel + +https://pixel-poem.itch.io/dungeon-assetpuck +https://kyrise.itch.io/kyrises-free-16x16-rpg-icon-pack + +# RPG Engine requirements + +## Functional requirements + +- [ ] Parsing of engine configuration file to game object +- [ ] Rendering of all game objects (Levels, objects, entities, ...) +- [ ] A start menu with the possibility of selecting a level +- [ ] An end screen that shows wether or not a player won +- [ ] Support for built-in engine functions + +- [ ] Player can move around in grid-world. +- [ ] Player can pick up objects. +- [ ] Player can use objects. +- [ ] Player can loose and gain health points. +- [ ] Player can interact with other entities (fight enimies, open doors, ...). +- [ ] Player can go to the next level. + +## Not-functional requirements + +- [ ] Use Parsing. +- [ ] Use at least one (1) monad transformer. +- [ ] Write good and plenty of documentation.:w + +- [ ] Write tests (for example, using HSpec). + +--- + +# Plaats om dingen neer te jotten + +``` +Play <--- HandleInput + | + | + v +Level <--- LoadLevel <--- Parse + | + | + v +RenderLevel +``` + +- [ ] State paradigma gebruiken om van startscherm naar playscherm naar pause scherm naar endscherm te gaan + +Nuttige links: + +- https://jakewheat.github.io/intro_to_parsing/ + +``` +Jarne — Today at 22:44 +Da kan hoor en had da eerst, me gloss eeft geen goede text dus... +ListDirectory, en er was ook een fuctie takeBaseName +``` + +# RPG-Engine Documentation + +## Playing the game + +TODO + +- Input commands etc +- An example playthrough + +## Writing your own stages + +A stage description file, conventionally named `.txt` is a file with a JSON-like format. It is used to describe + everything inside a single stage of your game, including anything related to the player, the levels your game contains + and what happens in that level. It is essentially the raw representation of the initial state of a single game. + +> Note: At the moment, every game has a single stage description file. Chaining several files together is not possible yet. + +A stage description file consists of several elements. + +| Element | Short description | +| --------------- | --------------------------------------------------------------------------------------------------------- | +| `Block` | optionally surrounded by `{ ... }`, consists of several `Entry`'s, optionally separated by commas `,` | +| `Entry` | is a `Key` - `Value` pair, optionally separated by a colon `:` | +| `Key` | is a unique, predefined `String` describing `Value` | +| `Value` | is either a `Block` or a `BlockList` or a traditional value, such as `String` or `Int` | +| `BlockList` | is a number of `Block`'s, surrounded by `[ ... ]`, separated by commas, can be empty | + +We'll look at the following example to explain these concepts. + +```javascript +player: { + hp: 50, + inventory: [ + { + id: "dagger", + x: 0, + y: 0, + name: "Dagger", + description: "Basic dagger you found somewhere", + useTimes: infinite, + value: 10, + + actions: {} + } + ] +} + +levels: [ + { + layout: { + | * * * * * * + | * s . . e * + | * * * * * * + }, + items: [], + entities: [] + }, + { + layout: { + | * * * * * * * * + | * s . . . . e * + | * * * * * * * * + }, + items: [ + { + id: "key", + x: 3, + y: 1, + name: "Doorkey", + description: "Unlocks a secret door", + useTimes: 1, + value: 0, + actions: { + [not(inventoryFull())] retrieveItem(key), + [] leave() + } + } + ], + entities: [ + { + id: "door", + x: 4, + y: 1, + name: "Secret door", + description: "This secret door can only be opened with a key", + direction: left, + actions: { + [inventoryContains(key)] useItem(key), + [] leave() + } + } + ] + } +] +``` + +This stage description file consists of a single `Block`. A stage description file always does. This top level `Block` + contains two `Value`s `player` and `levels`, not separated by commas. + +`player` describes a `Block` that represents the player of the game. Its `Entry`s are `hp` (a traditional value) and + `inventory` (a `BlockList` of several other `Block`s). They are both separated by commas this time. It is possible for + the inventory to be an empty list `[]`. + +`levels` is a `BlockList` that contains all the information to construct your game. + +### `layout` syntax + +If `Key` has the value `layout`, `Value` is none of the types discussed so far. Instead `Layout` is specifically made + to describe the layout of a level. This object is surrounded by `{ ... }` and consists of multiple lines, starting with + a vertical line `|` and several characters of the following: + +- `x` is an empty tile a.k.a. void. +- `.` is a tile walkable by the player. +- `*` is a tile not walkable by the player. +- `s` is the starting position of the player. +- `e` is the exit. + +All characters are interspersed with spaces. + +### `actions` syntax + +If `Key` has the value `actions`, the following changes are important for its `Value`, which in this case is a `Block` + with zero or more `Entry`s like so: + +- `Key` has type `ConditionList`. + + A `ConditionList` consists of several `Condition`s, surrounded by `[ ... ]`, separated by commas. A `ConditionList` + can be empty. If so, the conditional is always fulfilled. + + A `Condition` is one of the following: + + - `inventoryFull()`: the players inventory is full. + - `inventoryContains(objectId)`: the players inventory contains an object with id `objectId`. + - `not(condition)`: logical negation of `condition`. + +- `Value` is an `Action`. + + An `Action` is one of the following: + + - `leave()` + - `retrieveItem(objectId)` + - `useItem(objectId)` + - `decreaseHp(entityId, objectId)` + - `increasePlayerHp(objectId)` + +### Back to the example + +If we look at the example, all the objects are + +``` +>Block< + Entry = Key ('player') + >Block< + Entry = Key ('hp') + Value (50) + Entry = Key ('inventory') + >BlockList< + length = 1 + Block + Entry = Key ('id') + Value ("dagger") + ... + Entry = Key ('actions') + empty Block + Entry = Key ('levels') + >BlockList< + length = 2 + >Block< + Entry = Key ('layout') + Layout + + Entry = Key ('items') + empty BlockList + Entry = Key ('entities') + empty BlockList + >Block< + Entry = Key ('layout') + Layout + + Entry = Key ('items') + >BlockList< + length = 1 + >Block< + Entry = Key ('id') + Value ("key") + ... + Entry = Key ('actions') + >Block< + Entry = >ConditionList< + Action ('retrieveItem(key)') + length = 1 + Condition ('not(inventoryFull())')) + Entry = empty ConditionList + Action ('leave()') + Entry = Key ('entities') + >BlockList< + length = 1 + >Block< + Entry = Key ('id') + Value ("door") + ... + Entry = Key ('actions') + >Block< + Entry = >ConditionList< + Action ('useItem(key)') + length = 1 + Condition ('inventoryContains(key)') + Entry = empty ConditionList + Action ('leave()') +``` \ No newline at end of file diff --git a/lib/control/Parse.hs b/lib/control/Parse.hs new file mode 100644 index 0000000..376f29b --- /dev/null +++ b/lib/control/Parse.hs @@ -0,0 +1,132 @@ +module Parse where + +-- TODO Maak wrapper module +-- TODO This module should not be used by anything except for wrapper module and tests + +import Game +import Player +import Text.Parsec +import Text.Parsec.Char +import Text.Parsec.String +import Data.List +import Data.Maybe +import Text.Parsec.Error (Message(UnExpect)) + +-- TODO parseFromFile gebruiken + +-- Parser type +-- type Parser = Parsec String () + +-- A wrapper, which takes a parser and some input and returns a +-- parsed output. +parseWith :: Parser a -> String -> Either ParseError a +parseWith parser = parse parser "" + +ignoreWS :: Parser a -> Parser a +ignoreWS parser = spaces >> parser + +-- Also return anything that has not yet been parsed +parseWithRest :: Parser a -> String -> Either ParseError (a, String) +-- fmap (,) over Parser monad and apply to rest +parseWithRest parser = parse ((,) <$> parser <*> rest) "" + where rest = manyTill anyToken eof + +parseToGame :: Game +parseToGame = undefined + +-- Info in between brackets, '(..)', '[..]', '{..}' or '<..>' +data Brackets a = Brackets a + deriving (Eq, Show) + +parseToPlayer :: Player +parseToPlayer = undefined + +-- any words separated by whitespace +parseWord :: Parser String +parseWord = do many alphaNum + +-- TODO Expand to allow different kinds of brackets, also see Brackets data type. +-- TODO Check if brackets match order. +-- TODO Allow nested brackets. +brackets :: Parser (Brackets String) +brackets = do + ignoreWS $ char '(' + e <- ignoreWS $ many1 alphaNum + ignoreWS $ char ')' + return $ Brackets e + +------------------------ + +data Value = String String + | Integer Int + | Infinite + deriving (Show, Eq) + +-- See documentation for more details, only a short description is +--provided here. +data StructureElement = Block [StructureElement] + | Entry String StructureElement-- Key + Value + | Regular Value -- Regular value, Integer or String or Infinite + | ConditionList [StructureElement] + -- TODO + | Condition -- inventoryFull() etc. + -- TODO + | Action -- leave(), useItem(objectId) etc. + deriving (Show, Eq) + +-- TODO Add ConditionList and Action +structureElement :: Parser StructureElement +structureElement = choice [block, regular] + +-- A Block is a list of Entry s +block :: Parser StructureElement +block = do + ignoreWS $ char '{' + list <- ignoreWS $ many1 entry + ignoreWS $ char '}' + return $ Block list + +entry :: Parser StructureElement +entry = do + key <- ignoreWS $ many1 alphaNum + ignoreWS $ char ':' + value <- ignoreWS structureElement -- TODO Is this the correct one to use? + return $ Entry key value + +regular :: Parser StructureElement +regular = do + value <- ignoreWS $ choice [integer, valueString, infinite] + return $ Regular value + +integer :: Parser Value +integer = do + value <- ignoreWS $ many1 digit + return $ Integer (read value :: Int) + +valueString :: Parser Value +valueString = do + ignoreWS $ char '"' + value <- ignoreWS $ many1 (noneOf ['"']) + ignoreWS $ char '"' + return $ String value + +infinite :: Parser Value +infinite = do + ignoreWS $ string "infinite" + notFollowedBy alphaNum + return Infinite + +conditionList :: Parser StructureElement +conditionList = do + ignoreWS $ char '[' + list <- ignoreWS $ many1 condition + ignoreWS $ char ']' + return $ ConditionList list + +-- TODO +condition :: Parser StructureElement +condition = undefined + +-- TODO YOU ARE HERE +action :: Parser StructureElement +action = undefined diff --git a/rpg-engine.cabal b/rpg-engine.cabal index cf4a6f8..3775de7 100644 --- a/rpg-engine.cabal +++ b/rpg-engine.cabal @@ -8,11 +8,12 @@ library hs-source-dirs: lib, lib/control, lib/data, lib/render build-depends: base >= 4.7 && <5, - gloss >= 1.11 && < 1.14, gloss-juicy >= 0.2.3 + gloss >= 1.11 && < 1.14, gloss-juicy >= 0.2.3, + parsec >= 3.1.15.1 exposed-modules: RPGEngine, -- Control - Input, InputHandling, + Input, InputHandling, Parse, -- Data Game, Internals, Player, State, -- Render @@ -26,7 +27,11 @@ executable rpg-engine test-suite rpg-engine-test type: exitcode-stdio-1.0 - main-is: RPG-Engine-Test.hs + main-is: RPGEngineSpec.hs hs-source-dirs: test default-language: Haskell2010 - build-depends: base >=4.7 && <5, hspec <= 2.10.6, rpg-engine + build-depends: base >=4.7 && <5, hspec <= 2.10.6, hspec-discover, rpg-engine + other-modules: + InteractionSpec, + -- Parsing + ParsedToGameSpec, ParserSpec diff --git a/stack.yaml b/stack.yaml index 2539f5a..344fbba 100644 --- a/stack.yaml +++ b/stack.yaml @@ -42,6 +42,7 @@ extra-deps: # # extra-deps: [] - gloss-juicy-0.2.3@sha256:0c3bca95237cbf91f8b3b1936a0661f1e0457acd80502276d54d6c5210f88b25,1618 +- parsec-3.1.15.1@sha256:8c7a36aaadff12a38817fc3c4ff6c87e3352cffd1a58df640de7ed7a97ad8fa3,4601 # Override default flag values for local packages and extra-deps # flags: {} diff --git a/test/InteractionSpec.hs b/test/InteractionSpec.hs new file mode 100644 index 0000000..63801af --- /dev/null +++ b/test/InteractionSpec.hs @@ -0,0 +1,9 @@ +module InteractionSpec where + +import Test.Hspec + +spec :: Spec +spec = do + describe "Player with Inventory" $ do + it "TODO: Simple test" $ do + pending \ No newline at end of file diff --git a/test/ParsedToGameSpec.hs b/test/ParsedToGameSpec.hs new file mode 100644 index 0000000..b1d3930 --- /dev/null +++ b/test/ParsedToGameSpec.hs @@ -0,0 +1,52 @@ +module ParsedToGameSpec where + +import Test.Hspec +import Parse + +spec :: Spec +spec = do + describe "Game" $ do + it "TODO: Simple game" $ do + pending + it "TODO: More complex game" $ do + pending + it "TODO: Game with multiple levels" $ do + pending + + describe "Player" $ do + it "TODO: Simple player" $ do + pending + + describe "Inventory" $ do + it "TODO: Empty inventory" $ do + pending + it "TODO: Singleton inventory" $ do + pending + it "TODO: Filled inventory" $ do + pending + + describe "Items" $ do + it "TODO: Simple item" $ do + pending + -- Check id + -- Check x + -- Check y + -- Check name + -- Check description + -- Check useTimes + -- Check value + -- Check actions + + describe "Actions" $ do + it "TODO: Simple action" $ do + pending + + describe "Entities" $ do + it "TODO: Simple entity" $ do + pending + + describe "Level" $ do + it "TODO: Simple layout" $ do + pending + it "TODO: Complex layout" $ do + pending \ No newline at end of file diff --git a/test/ParserSpec.hs b/test/ParserSpec.hs new file mode 100644 index 0000000..b5db0cc --- /dev/null +++ b/test/ParserSpec.hs @@ -0,0 +1,52 @@ +module ParserSpec where + +import Test.Hspec +import Parse +import Data.Either + +spec :: Spec +spec = do + describe "Basics of entries" $ do + it "can parse integers" $ do + let correct = Right $ Regular $ Integer 1 + correct `shouldBe` parseWith regular "1" + it "can parse string" $ do + let input = "dit is een string" + correct = Right $ Regular $ String input + correct `shouldBe` parseWith regular ("\"" ++ input ++ "\"") + it "can parse infinite" $ do + let correct = Right $ Regular Infinite + correct `shouldBe` parseWith regular "infinite" + + let wrong = Right $ Regular Infinite + wrong `shouldNotBe` parseWith regular "infinitee" + + it "can parse entries" $ do + let input = "id : \"dagger\"" + correct = Right $ Entry "id" $ Regular $ String "dagger" + correct `shouldBe` parseWith entry input + + let input = "x: 0" + correct = Right $ Entry "x" $ Regular $ Integer 0 + correct `shouldBe` parseWith entry input + + let input = "useTimes: infinite" + correct = Right $ Entry "useTimes" $ Regular Infinite + correct `shouldBe` parseWith entry input + + describe "Special kinds" $ do + it "can parse actions" $ do + let input = "actions: {}" + correct = Right $ Entry "actions" $ Regular Infinite -- TODO Change this + correct `shouldBe` parseWith action input + + it "can parse conditions" $ do + pending + + it "can parse layouts" $ do + pending + + describe "Lists and blocks" $ do + it "can parse entities" $ do + pending + diff --git a/test/RPG-Engine-Test.hs b/test/RPG-Engine-Test.hs deleted file mode 100644 index 39131fd..0000000 --- a/test/RPG-Engine-Test.hs +++ /dev/null @@ -1,7 +0,0 @@ -import Test.Hspec - -main :: IO() -main = hspec $ do - describe "Dummy category" $ do - it "Dummy test" $ do - 0 `shouldBe` 0 \ No newline at end of file diff --git a/test/RPGEngineSpec.hs b/test/RPGEngineSpec.hs new file mode 100644 index 0000000..52ef578 --- /dev/null +++ b/test/RPGEngineSpec.hs @@ -0,0 +1 @@ +{-# OPTIONS_GHC -F -pgmF hspec-discover #-} \ No newline at end of file