#18 #14 Inital parser commit

Added basic parser functionality & tests for these functionalites.
Split tests in several files
This commit is contained in:
Tibo De Peuter 2022-12-17 23:14:04 +01:00
parent 4c1f25e49d
commit 83659e69b4
9 changed files with 504 additions and 12 deletions

249
README.md
View file

@ -1,3 +1,250 @@
# RPG-Engine
Schrijf een game-engine voor een rollenspel
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 `<stage_name>.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")
... <several traditional entries like this>
Entry = Key ('actions') + empty Block
Entry = Key ('levels') + >BlockList<
length = 2
>Block<
Entry = Key ('layout') + Layout
<multiple lines that describe the layout>
Entry = Key ('items') + empty BlockList
Entry = Key ('entities') + empty BlockList
>Block<
Entry = Key ('layout') + Layout
<multiple lines that describe the layout>
Entry = Key ('items') + >BlockList<
length = 1
>Block<
Entry = Key ('id') + Value ("key")
... <several traditional entries like this>
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")
... <several traditional entries like this>
Entry = Key ('actions') + >Block<
Entry = >ConditionList< + Action ('useItem(key)')
length = 1
Condition ('inventoryContains(key)')
Entry = empty ConditionList + Action ('leave()')
```

132
lib/control/Parse.hs Normal file
View file

@ -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

View file

@ -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

View file

@ -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: {}

9
test/InteractionSpec.hs Normal file
View file

@ -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

52
test/ParsedToGameSpec.hs Normal file
View file

@ -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

52
test/ParserSpec.hs Normal file
View file

@ -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

View file

@ -1,7 +0,0 @@
import Test.Hspec
main :: IO()
main = hspec $ do
describe "Dummy category" $ do
it "Dummy test" $ do
0 `shouldBe` 0

1
test/RPGEngineSpec.hs Normal file
View file

@ -0,0 +1 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}