#11 Write report

This commit is contained in:
Tibo De Peuter 2022-12-23 21:39:56 +01:00
parent c3f7e47703
commit 0a6e1d7ffb
27 changed files with 328 additions and 134 deletions

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ extra/
*.dll
stack.yaml.lock
.vscode/settings.json

3
.vscode/tasks.json vendored
View file

@ -49,9 +49,10 @@
"args": [
"-s",
"-o", "verslag.pdf",
"-f", "markdown+smart+header_attributes+yaml_metadata_block+auto_identifiers",
"-f", "markdown+smart+header_attributes+yaml_metadata_block+auto_identifiers+implicit_figures",
"--pdf-engine", "lualatex",
"--template", "eisvogel",
"--dpi=300",
"header.yaml",
"README.md"
],

305
README.md
View file

@ -1,40 +1,9 @@
<!--
## 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
- [x] 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 enemies, open doors, ...).
- [ ] Player can go to the next level.
## Not-functional requirements
- [x] Use Parsing.
- [ ] Use at least one (1) monad transformer.
- [ ] Write good and plenty of documentation.:w
- [x] Write tests (for example, using HSpec).
---
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
```
---
Config files cannot end with blank line
<div style="page-break-after: always;"></div>
-->
@ -45,40 +14,193 @@ RPG-Engine is a game engine for playing and creating your own RPG games.
If you are interested in the development side of things, [development notes can be found here](#Development-notes).
This README serves as both documentation and project report, so excuse the details that might not be important for the average user.
This README serves as both documentation and project report, so excuse the details that might not be important for the average reader.
## Playing the game
These are the keybinds *in* the game. All other keybinds in the menus should be straightforward.
These are the keybinds while *in* game. All other keybinds in menus etc. should be straightforward.
| Action | Primary | Secondary |
| -------------- | ------------- | ----------- |
| Move up | `Arrow Up` | `w` |
| Move left | `Arrow Left` | `a` |
| Move down | `Arrow Down` | `s` |
| Move right | `Arrow Right` | `d` |
| Interaction | `Space` | `f` |
| Show inventory | `i` | `Tab` |
| Restart level | `r` | |
| Quit game | `Esc` | |
| Action | Primary | Secondary |
| -------------- | ------------- | ------------ |
| Move up | `Arrow Up` | `w` |
| Move left | `Arrow Left` | `a` |
| Move down | `Arrow Down` | `s` |
| Move right | `Arrow Right` | `d` |
| Interaction | `Space` | `f`, `Enter` |
| Show inventory | `i` | `Tab` |
| Restart level | `r` | |
| Quit game | `Esc` | |
### Example playthrough
TODO
![Select level 5](./extra/walkthrough/selection.png){ width=600 }
![Move to the first exit](./extra/walkthrough/5-1-01.png){ width=600 }
![Move to the first key](./extra/walkthrough/5-2-02.png){ width=600 }
![Pick up key](./extra/walkthrough/5-2-03.png){ width=600 }
![Move to door](./extra/walkthrough/5-2-04.png){ width=600 }
![Open door with key](./extra/walkthrough/5-2-05.png){ width=600 }
![Move to exit](./extra/walkthrough/5-2-06.png){ width=600 }
![Move to devil](./extra/walkthrough/5-3-01.png){ width=600 }
![Try to attack with dagger](./extra/walkthrough/5-3-02.png){ width=600 }
![Go pick up sword](./extra/walkthrough/5-3-03.png){ width=600 }
![Attack devil using sword](./extra/walkthrough/5-3-05.png){ width=600 }
![Pick up key](./extra/walkthrough/5-3-06.png){ width=600 }
![Open door](./extra/walkthrough/5-3-08.png){ width=600 }
![Move to exit](./extra/walkthrough/5-3-09.png){ width=600 }
![You win](./extra/walkthrough/you-win.png){ width=600 }
- An example playthrough, with pictures and explanations
## Development notes
### Engine architecture
`RPGEngine` is the main module. It contains the `playRPGEngine` function which bootstraps the whole game. It is also
the game loop. From here, `RPGEngine` talks to its submodules.
These submodules are `Config`, `Data`, `Input`, `Parse` & `Render`. They are all responsible for their own part. However,
each of these submodules has their own submodules to divide the work. They are conveniently named after the state of
the game that they work with, e.g. the main menu has a module & when the game is playing is a different module. A special
one is `Core`, which is kind of like a library for every piece. It contains functions that are regularly used by the other modules.
- `Config`: Configuration values, should ultimately be moved into parsing from a file.
- `Data`: Data containers and accessors of information.
- `Input`: Anything that handles input or changes the state of the game.
- `Parse`: Parsing
- `Render`: Rendering of the game and everything below that.
#### Monads/Monad stack
Monads:
- Extensive use of `Maybe` for integers or infinity and `do` in parser implementation.
- `IO` to handle external information
- ...
Monad transformer: ??
I am afraid I did not write any monad transformers in this project. I think I could (and should) have focused more on
writing monads and monad transformers. In hindsight, I can see where I could and should have used them. I can think of
plenty of ways to make the current implementation simpler. This is unfortunate. However, I want to believe that my next
time writing a more complex Haskell program, I will remember using monad transformers. Sadly, I forgot this time.
An example of where I would use a monad transformer - in hindsight:
1. Interactions in game: something along the lines of ...
```haskell
newtype StateT m a = StateT { runStateT :: m a }
instance Monad m => Monad (StateT m) where
return = lift . return
x >>= f = StateT $ do
v <- runStateT x
case v of
Playing level -> runStateT ( f level )
Paused continue -> runStateT ( continue >>= f )
-- etc
class MonadTransformer r where
lift :: Monad m => m a -> (r m) a
instance MonadTransformer StateT where
lift = StateT
```
2. Interaction with the outside world should also be done with Monad(transformers) instead of using `unsafePerformIO`.
### Tests
Overall, only parsing is tested using Hspec. However, parsing is tested *thoroughly* and I am quite sure that there aren't
a lot of edge cases that I did not catch. This makes for a relaxing environment where you can quickly check if a change
you made breaks anything.
`Spec` is the main module. It does not contain any tests, but functions as the 'discover' module to find the other tests
in its folder.
`Parser.StructureSpec` tests functionality of `RPGEngine.Parse.TextToStructure`, `Parser.GameSpec` tests functionality
of `RPGEngine.Parse.StructureToGame`.
Known issues:
- [ ] Rendering is still not centered, I am sorry for those with small screens.
- [ ] Config files cannot end with an empty line. I could not get that to work and I decided that it was more important
to implement other functionality first. Unfortunately, I was not able to get back to it yet.
- [ ] The parser is unable to parse layouts with trailing whitespace.
## Conclusion
Parsing was way harder than I initially expected. I believe over half my time on this project was spent trying to write the
parser. I am still not absolutely sure that it will work with *everything*, but it gets the job done at the moment. I don't
know if parsing into a structure before transforming the structure into a game was a good move. It might have saved me some
time if I did it straight to `Game`. I want to say that I have a parser-to-structure module now, but even so, there are some
links between `TextToStructure` and `Game` that make it almost useless to any other project (without changing anything).
Player-object interaction was easier than previous projects. I believe this is both because I am getting used to it by now
and because I spent a lot of time beforehand structuring everything. I also like to think that structuring the project is
what I did right. There is a clear hierarchy and you can find what you are looking for fairly easy, without having to search
for a function in file contents or having to scavenge multiple different files before finding what you want. However, I
absolutely wasted a lot of time restructuring the project multiple times, mostly because I was running into dependency cycles.
Overall, I believe the project was a success. I am proud of the end result. Though, please note my comments on monad transformers.
### Assets & dependencies
The following assets were used (and modified if specified):
- Kyrise's Free 16x16 RPG Icon Pack<sup>[[1]](#1)</sup>
- 2D Pixel Dungeon Asset Pack by Pixel_Poem<sup>[[2]](#2)</sup>
Every needed asset was taken and put into its own `.png`, instead of in the overview.
RPG-Engine makes use of the following libraries:
- [directory](https://hackage.haskell.org/package/directory) for listing levels in a directory
- [gloss](https://hackage.haskell.org/package/gloss) for game rendering
- [gloss-juicy](https://hackage.haskell.org/package/gloss-juicy) for rendering images
- [hspec](https://hackage.haskell.org/package/hspec) for testing
- [hspec-discover](https://hackage.haskell.org/package/hspec-discover) for allowing to split test files in multiple files
- [parsec](https://hackage.haskell.org/package/parsec) for parsing configuration files
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
## Writing your own stages
## References
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
<a id="1">[1]</a> [Kyrise's Free 16x16 RPG Icon Pack](https://kyrise.itch.io/kyrises-free-16x16-rpg-icon-pack) © 2018
by [Kyrise](https://kyrise.itch.io/) is licensed under [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)
<a id="2">[2]</a> [2D Pixel Dungeon Asset Pack](https://pixel-poem.itch.io/dungeon-assetpuck) by [Pixel_Poem](https://pixel-poem.itch.io/)
is not licensed
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
## Appendix A: Future development ideas
The following ideas could (or should) be implemented in the future of this project.
- [ ] **Entity system:** With en ES, you can implement moving entities and repeated input. It also resembles the typical
game loop more closely which can make it easier to implement other ideas in the future.
- [ ] **Game music:** Ambient game music and sound effects can improve the gaming experience I think.
- [ ] **Expand configuration file:** Implement the same methods for parsing stage description files to a configuration file,
containing keybinds, dimension sizes, even window titles, making this a truly customizable engine.
- [ ] **Camera follows player:** The camera should follow the player, making it always center. This allows for larger levels
increases the immersion of the game.
Changes in the backend:
- [ ] **Make inventory a state** At the moment, there is a boolean for inventory rendering. This should be turned into a state,
so it makes more sense to call it from other places as well.
- [ ] **Direction of entities** Change the rendering based on the direction of an entity.
- [ ] **Inventory with more details** The inventory should show more details of items, e.g. name, value, remaining use
times and description.
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
## Appendix B: Writing your own worlds
A world description file, conventionally named `<world_name_or_level_x>.txt` is a file with a JSON-like format. It is used to describe
everything inside a single world 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.
A world description file consists of several elements.
| Element | Short description |
| --------------- | --------------------------------------------------------------------------------------------------------- |
@ -159,7 +281,7 @@ levels: [
```
</details>
This stage description file consists of a single `Block`. A stage description file always does. This top level `Block`
This world description file consists of a single `Block`. A world 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
@ -252,79 +374,4 @@ If we look at the example, all the objects are
length = 1
Condition ('inventoryContains(key)')
Entry = empty ConditionList + Action ('leave()')
```
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
## Development notes
### Engine architecture
<mark>TODO</mark>
`RPGEngine` is the main module. It contains the `playRPGEngine` function which bootstraps the whole game. It is also
the game loop. From here, `RPGEngine` talks to its submodules.
These submodules are `Config`, `Data`, `Input`, `Parse` & `Render`. They are all responsible for their own part, either
containing the program configuration, data containers, everything needed to handle input, everything needed to parse a
source file & everything needed to render the game. However, each of these submodules has their own submodules to
divide the work. They are conveniently named after the state of the game that they work with, e.g. the main menu has a
module & when the game is playing is a different module. A special one is `Core`, which is kind of like a library for
every piece. It contains functions that are regularly used by the other modules.
#### Monads/Monad stack
<mark>TODO</mark>
### Tests
<mark>TODO</mark>
### Assets & dependencies
The following assets were used (and modified if specified):
- Kyrise's Free 16x16 RPG Icon Pack<sup>[[1]](#1)</sup>
- 2D Pixel Dungeon Asset Pack by Pixel_Poem<sup>[[2]](#2)</sup>
Every needed asset was taken and put into its own `.png`, instead of in the overview.
RPG-Engine makes use of the following libraries:
- [directory](https://hackage.haskell.org/package/directory) for listing levels in a directory
- [gloss](https://hackage.haskell.org/package/gloss) for game rendering
- [gloss-juicy](https://hackage.haskell.org/package/gloss-juicy) for rendering images
- [hspec](https://hackage.haskell.org/package/hspec) for testing
- [hspec-discover](https://hackage.haskell.org/package/hspec-discover) for allowing to split test files in multiple files
- [parsec](https://hackage.haskell.org/package/parsec) for parsing configuration files
### Future development ideas
The following ideas could (or should) be implemented in the future of this project.
- [ ] **Entity system:** With en ES, you can implement moving entities and repeated input. It also resembles the typical
game loop more closely which can make it easier to implement other ideas in the future.
- [ ] **Game music:** Ambient game music and sound effects can improve the gaming experience I think.
- [ ] **Expand configuration file:** Implement the same methods for parsing stage description files to a configuration file,
containing keybinds, dimension sizes, even window titles, making this a truly customizable engine.
- [ ] **Camera follows player:** The camera should follow the player, making it always center. This allows for larger levels
increases the immersion of the game.
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
## Conclusion
Parsing was way harder than I initially expected. About half of my time on this project was spent writing the parser.
<mark>TODO</mark>
<div style="page-break-after: always; visibility: hidden">\pagebreak</div>
## References
<a id="1">[1]</a> [Kyrise's Free 16x16 RPG Icon Pack](https://kyrise.itch.io/kyrises-free-16x16-rpg-icon-pack) © 2018
by [Kyrise](https://kyrise.itch.io/) is licensed under [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)
<a id="2">[2]</a> [2D Pixel Dungeon Asset Pack](https://pixel-poem.itch.io/dungeon-assetpuck) by [Pixel_Poem](https://pixel-poem.itch.io/)
is not licensed
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 905 B

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

144
levels/level5.txt Normal file
View file

@ -0,0 +1,144 @@
player: {
hp: 100,
inventory: [
{
id: "dagger",
x: 0,
y: 0,
name: "Swiss army knife",
description: "Your trustworthy army knife will never let you down",
useTimes: infinite,
value: 5,
actions: {}
},
{
id: "potion",
x: 0,
y: 0,
name: "Small healing potion",
description: "Will recover you from small injuries",
useTimes: 5,
value: 5,
actions: {}
}
]
}
levels: [
{
layout: {
| * * * * * * *
| * s . . . e *
| * * * * * * *
},
items: [],
entities: []
},
{
layout: {
| x x * * * x x x x
| x x * . * x x x x
| * * * . * * * * *
| * s . . . . . e *
| * * * * * * * * *
},
items: [
{
id: "key",
x: 3,
y: 3,
name: "Secret key",
description: "What if this key opens 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 door can only be opened with a secret key",
direction: left,
actions: {
[inventoryContains(key)] useItem(key),
[] leave()
}
}
]
},
{
layout: {
| * * * * * * * * * * *
| * . . . . . . . . . *
| * * * * * * * * * . *
| * e . . . . . . . s *
| * * * * * * * * * . *
| x x x x x x x x * . *
| * * * * * * * * * . *
| * . . . . . . . . . *
| * * * * * * * * * * *
},
items: [
{
id: "key",
x: 1,
y: 1,
name: "Key to sturdy door",
description: "You have proven worthy",
useTimes: 1,
value: 0,
actions: {
[not(inventoryFull())] retrieveItem(key),
[] leave()
}
},
{
id: "sword",
x: 1,
y: 7,
name: "Mighty sword",
description: "Slayer of evil",
useTimes: 3,
value: 100,
actions: {
[not(inventoryFull())] retrieveItem(sword),
[] leave()
}
}
],
entities: [
{
id: "door",
x: 8,
y: 5,
name: "Sturdy door",
description: "I wonder what's behind it?",
direction: right,
actions: {
[inventoryContains(key)] useItem(key),
[] leave()
}
},
{
id: "devil",
x: 6,
y: 1,
name: "Evil powers",
description: "Certainly from hell",
hp: 55,
value: 10,
actions: {
[inventoryContains(dagger)] decreaseHp(devil, dagger),
[inventoryContains(sword)] decreaseHp(devil, sword),
[] leave()
}
}
]
}
]

View file

@ -16,6 +16,7 @@ import Data.Foldable (find)
handleInputActionSelection :: InputHandler Game
handleInputActionSelection = composeInputHandlers [
handleKey (SpecialKey KeySpace) Down selectAction,
handleKey (SpecialKey KeyEnter) Down selectAction,
handleKey (SpecialKey KeyUp) Down $ moveSelector North,
handleKey (SpecialKey KeyDown) Down $ moveSelector South

View file

@ -36,6 +36,7 @@ handleInputPlaying = composeInputHandlers [
-- Interaction with entities and items
handleKey (SpecialKey KeySpace) Down checkForInteraction,
handleKey (SpecialKey KeyEnter) Down checkForInteraction,
handleKey (Char 'f') Down checkForInteraction,
handleKey (Char 'i') Down $ toggleInventoryShown True,

View file

@ -42,8 +42,7 @@ focusPlayer _ = id
renderLevel :: Renderer Level
renderLevel Level{ layout = l, items = i, entities = e } = level
where level = pictures [void, layout, items, entities]
-- void = createVoid
void = blank
void = createVoid
layout = renderLayout l
items = renderItems i
entities = renderEntities e
@ -92,12 +91,12 @@ renderInventory :: Player -> Picture
renderInventory Player{ showInventory = False } = blank
renderInventory Player{ inventory = list } = pictures [overlay, title, items]
where title = translate 0 (offset (-1)) $ scale uizoom uizoom $ color white $ text "Inventory"
items = pictures $ map move $ zip [0::Int ..] (map (getRender . itemId) list)
items = pictures $ zipWith (curry move) [0::Int ..] (map (getRender . itemId) list)
move (i, pic) = translate 0 (offset i) pic
offset i = negate (zoom * resolution * fromIntegral i)
withHealthBar :: HP -> Picture -> Picture
withHealthBar (Nothing) renderedEntity = renderedEntity
withHealthBar Nothing renderedEntity = renderedEntity
withHealthBar (Just hp) renderedEntity = pictures [renderedEntity, positionedBar]
where positionedBar = scale smaller smaller $ translate left up renderedBar
renderedBar = pictures [heart, counter]

Binary file not shown.