#10 #18 Fix parsing

This commit is contained in:
Tibo De Peuter 2022-12-22 22:05:25 +01:00
parent 5cc96cbdba
commit f3bce99120
18 changed files with 289 additions and 103 deletions

View file

@ -58,6 +58,8 @@ These are the keybinds *in* the game. All other keybinds in the menus should be
| Move down | `Arrow Down` | `s` |
| Move right | `Arrow Right` | `d` |
| Show inventory | `i` | |
| Restart level | `r` | |
| Quit game | `Esc` | |
### Example playthrough

View file

@ -15,4 +15,4 @@ levels: [
entities: []
}
]
]

View file

@ -19,8 +19,8 @@ levels: [
items: [
{
id: "key",
x: 0,
y: 1,
x: 1,
y: 2,
name: "Sleutel",
description: "Deze sleutel kan een deur openen",
useTimes: 1,
@ -35,8 +35,8 @@ levels: [
entities: [
{
id: "door",
x: 0,
y: 3,
x: 1,
y: 4,
name: "Deur",
description: "Deze deur kan geopend worden met een sleutel",
direction: up,
@ -48,4 +48,4 @@ levels: [
}
]
}
]
]

View file

@ -76,4 +76,4 @@ levels: [
}
]
}
]
]

View file

@ -131,4 +131,4 @@ levels: [
}
]
}
]
]

View file

@ -1,9 +1,6 @@
# Dit gehele bestand is een Block
# Dit is een entry: key + value
player: { # Value is hier een block
player: {
hp: 50,
inventory: [ # BlockList
inventory: [
{
id: "dagger",
x: 0,
@ -18,7 +15,6 @@ player: { # Value is hier een block
]
}
# Dit is een entry
levels: [
{
layout: {
@ -135,4 +131,4 @@ levels: [
}
]
}
]
]

View file

@ -9,7 +9,7 @@ import RPGEngine.Config ( bgColor, winDimensions, winOffsets )
import RPGEngine.Render ( initWindow, render )
import RPGEngine.Input ( handleAllInput )
import RPGEngine.Input.Playing ( checkPlaying, spawnPlayer )
import RPGEngine.Data (Game (..), State (..), Layout, Level (..), Physical (..))
import RPGEngine.Data (Game (..), State (..), Layout, Level (..), Physical (..), Entity(..), Direction(..), Player(..))
import RPGEngine.Data.Default (defaultLevel, defaultPlayer)
import Graphics.Gloss ( play )
@ -27,22 +27,15 @@ playRPGEngine title fps = do
-- TODO revert this
-- Initialize the game
initGame :: Game
-- initGame = Game {
-- state = Menu{ base = StateBase{
-- renderer = renderMenu,
-- inputHandler = handleInputMenu
-- }}
-- }
initGame = Game{
state = initState
}
where initState = Playing{
levels = [defaultLevel, otherLevel],
count = 0,
level = defaultLevel,
player = spawnPlayer defaultLevel defaultPlayer,
restart = initState
}
initGame = Game { state = Menu }
-- initGame = Game{ state = initState }
-- where initState = Playing{
-- levels = [defaultLevel, otherLevel],
-- count = 0,
-- level = defaultLevel,
-- player = spawnPlayer defaultLevel defaultPlayer,
-- restart = initState
-- }
-- TODO remove this
otherLayout :: Layout
@ -50,6 +43,8 @@ otherLayout = [
[Blocked, Blocked, Blocked],
[Blocked, Entrance, Blocked],
[Blocked, Walkable, Blocked],
[Blocked, Walkable, Blocked],
[Blocked, Walkable, Blocked],
[Blocked, Exit, Blocked],
[Blocked, Blocked, Blocked]
]
@ -69,12 +64,30 @@ otherLevel = Level {
(1, 2, Walkable),
(2, 2, Blocked),
(0, 3, Blocked),
(1, 3, Exit),
(1, 3, Walkable),
(2, 3, Blocked),
(0, 4, Blocked),
(1, 4, Blocked),
(2, 4, Blocked)
(1, 4, Walkable),
(2, 4, Blocked),
(0, 5, Blocked),
(1, 5, Exit),
(2, 5, Blocked),
(0, 6, Blocked),
(1, 6, Blocked),
(2, 6, Blocked)
],
items = [],
entities = []
entities = [
Entity{
entityId = "door",
entityX = 1,
entityY = 3,
entityName = "Epic door",
entityDescription = "epic description",
entityActions = [],
entityValue = Nothing,
entityHp = Nothing,
direction = North
}
]
}

View file

@ -12,7 +12,7 @@ import RPGEngine.Render.Core ( Renderer )
-- A game is the base data container.
data Game = Game {
state :: State
}
} deriving (Eq, Show)
------------------------------- State --------------------------------
@ -33,6 +33,7 @@ data State = Menu
| Win
-- Lost a level
| Lose { restart :: State }
deriving (Eq, Show)
------------------------------- Level --------------------------------

View file

@ -64,9 +64,30 @@ defaultLevel = Level {
defaultPlayer :: Player
defaultPlayer = Player {
playerHp = Prelude.Nothing, -- Compares to infinity
inventory = [],
position = (0, 0)
-- playerHp = Prelude.Nothing, -- Compares to infinity
playerHp = Just 50,
inventory = [ Item{
itemId = "key",
itemX = 0,
itemY = 0,
itemName = "Epic key",
itemDescription = "MyKey",
itemActions = [],
itemValue = Nothing,
useTimes = Nothing
}, Item{
itemId = "dagger",
itemX = 0,
itemY = 0,
itemName = "My dagger",
itemDescription = "dagger",
itemActions = [],
itemValue = Nothing,
useTimes = Nothing
}],
position = (0, 0),
showInventory = False,
showHp = True
}
defaultSelector :: ListSelector

View file

@ -20,7 +20,7 @@ type InputHandler a = Event -> (a -> a)
data ListSelector = ListSelector {
selection :: Int,
selected :: Bool
}
} deriving (Eq, Show)
------------------------------ Exported ------------------------------

View file

@ -2,6 +2,7 @@ module RPGEngine.Input.Playing
( handleInputPlaying
, checkPlaying
, spawnPlayer
, putCoords
) where
import RPGEngine.Input.Core (InputHandler, handle, handleKey, composeInputHandlers)
@ -32,6 +33,8 @@ handleInputPlaying = composeInputHandlers [
handleKey (Char 's') Down $ movePlayer South,
handleKey (Char 'a') Down $ movePlayer West,
handleKey (Char 'r') Down restartGame,
handleKey (Char 'i') Down $ toggleInventoryShown True,
handleKey (Char 'i') Up $ toggleInventoryShown False
]
@ -58,13 +61,15 @@ pauseGame g@Game{ state = playing@Playing{} } = pausedGame
where pausedGame = g{ state = Paused playing }
pauseGame g = g
restartGame :: Game -> Game
restartGame g@Game{ state = playing@Playing{ restart = restarted } } = g{ state = restarted }
-- Go to next level if there is a next level, otherwise, initialize win state.
goToNextLevel :: State -> State
goToNextLevel s@Playing{ levels = levels, level = current, count = count, player = player } = nextState
where -- Either the next level or winState
nextState | (count + 1) < length levels = nextLevelState
where nextState | (count + 1) < length levels = nextLevelState
| otherwise = Win
nextLevelState = s{ level = nextLevel, count = count + 1, player = movedPlayer }
nextLevelState = s{ level = nextLevel, count = count + 1, player = movedPlayer, restart = nextLevelState }
nextLevel = levels !! (count + 1)
movedPlayer = spawnPlayer nextLevel player
goToNextLevel s = s

View file

@ -6,13 +6,11 @@ import RPGEngine.Data ( Game )
import RPGEngine.Parse.StructureToGame ( structureToGame )
import GHC.IO (unsafePerformIO)
import Text.Parsec.String (parseFromFile)
import RPGEngine.Parse.TextToStructure (structure)
import RPGEngine.Parse.TextToStructure ( gameFile )
------------------------------ Exported ------------------------------
parse :: FilePath -> Game
parse filename = structureToGame struct
where (Right struct) = unsafePerformIO io
io = parseFromFile structure filename
tempParse = parseFromFile
io = parseFromFile gameFile filename

View file

@ -10,26 +10,25 @@ import RPGEngine.Data
entityActions, entityValue, entityHp, direction),
Item(itemId, itemX, itemY, itemName, itemDescription, itemValue,
itemActions, useTimes),
Level(layout, items, entities),
Level(layout, items, entities, index),
Game (..), State (..) )
import RPGEngine.Parse.TextToStructure
( Value(Infinite, Action, Layout, String, Direction, Integer),
Key(Tag, ConditionList),
Structure(..) )
import RPGEngine.Data.Default (defaultPlayer, defaultLevel, defaultItem, defaultEntity)
import RPGEngine.Input.Playing (putCoords, spawnPlayer)
------------------------------ Exported ------------------------------
structureToGame :: Structure -> Game
-- structureToGame [Entry(Tag "player") playerBlock, Entry(Tag "levels") levelsBlock] = game
structureToGame (Entry (Tag "player") playerBlock) = game
where game = Game{ state = newState }
newState = Playing{ levels = newLevels, level = currentLevel, player = newPlayer, restart = newState }
-- newLevels = structureToLevels levelsBlock
-- currentLevel = head newLevels
newLevels = [defaultLevel]
currentLevel = defaultLevel
newPlayer = structureToPlayer playerBlock
structureToGame :: [Structure] -> Game
structureToGame [Entry (Tag "player") playerBlock, Entry (Tag "levels") levelsBlock] = game
where game = Game newState
newState = Playing newLevels 0 currentLevel newPlayer newState
newLevels = structureToLevels levelsBlock
currentLevel = head newLevels
newPlayer = spawnPlayer currentLevel $ structureToPlayer playerBlock
structureToGame _ = Game Menu
------------------------------- Player -------------------------------
@ -60,7 +59,9 @@ structureToLevels (Block struct) = structureToLevel <$> struct
structureToLevels _ = [defaultLevel]
structureToLevel :: Structure -> Level
structureToLevel (Block entries) = structureToLevel' entries defaultLevel
structureToLevel (Block entries) = indexIsSet
where indexIsSet = level{ index = putCoords level }
level = structureToLevel' entries defaultLevel
structureToLevel _ = defaultLevel
structureToLevel' :: [Structure] -> Level -> Level

View file

@ -18,9 +18,13 @@ import Text.Parsec
notFollowedBy,
sepBy,
many,
try )
try, spaces, endOfLine )
import qualified Text.Parsec as P ( string )
import Text.Parsec.String ( Parser )
import Text.Parsec.Combinator (lookAhead)
gameFile :: Parser [Structure]
gameFile = try $ do many1 $ ignoreWS structure
-------------------------- StructureElement --------------------------
@ -111,7 +115,7 @@ data Value = String String
----------------------------------------------------------------------
value :: Parser Value
value = choice [layout, string, integer, infinite, action, direction]
value = choice [layout, string, integer, infinite, direction, action]
string :: Parser Value
string = try $ String <$> between (char '\"') (char '\"') reading
@ -149,7 +153,7 @@ direction = try $ do
ignoreWS $ P.string "left",
ignoreWS $ P.string "right"
]
notFollowedBy alphaNum
-- lookAhead $ char ','
return $ Direction $ make value
where make "up" = North
make "right" = East
@ -160,15 +164,12 @@ direction = try $ do
layout :: Parser Value
layout = try $ do
open <- ignoreWS $ oneOf openingBrackets
ignoreWS $ char '|'
list <- ignoreWS $ ignoreWS strip `sepBy` ignoreWS (char '|')
let closing = getMatchingClosingBracket open
ignoreWS $ char closing
return $ Layout list
value <- many1 strip <* ignoreWS (char closing)
return $ Layout value
strip :: Parser Strip
strip = try $ do
physical `sepBy` char ' '
strip = try $ do ignoreWS (char '|') *> ignoreWS (physical `sepBy` char ' ')
physical :: Parser Physical
physical = try $ do

View file

@ -55,7 +55,11 @@ test-suite rpg-engine-test
main-is: Spec.hs
hs-source-dirs: test
default-language: Haskell2010
build-depends: base >=4.7 && <5, hspec <= 2.10.6, hspec-discover, rpg-engine
build-depends:
base >=4.7 && <5,
rpg-engine,
hspec <= 2.10.6, hspec-discover,
parsec >= 3.1.15.1
other-modules:
Parser.GameSpec
Parser.StructureSpec
Parser.StructureSpec

View file

@ -6,24 +6,50 @@ import RPGEngine.Data
import RPGEngine.Parse.Core
import RPGEngine.Parse.TextToStructure
import RPGEngine.Parse.StructureToGame
import RPGEngine.Parse.TextToStructure (gameFile)
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
-- TODO There is a weird bug that caused this to go in an infinite loop. Fix later.
xit "Simple game" $ do
let input = "player: {\n hp: 50,\n inventory: []\n}\n\nlevels: [\n {\n layout: {\n | * * * * * *\n | * s . . e *\n | * * * * * *\n },\n \n items: [],\n\n entities: []\n\n\n }\n]"
correct = Game {
state = Playing {
levels = [],
count = 0,
level = Level {
RPGEngine.Data.layout = [],
index = [],
items = [],
entities = []
},
player = Player {
playerHp = Just 50,
inventory = [],
position = (0, 0),
showHp = True,
showInventory = False
},
restart = Menu
}
}
(Right struct) = parseWith gameFile input
structureToGame struct `shouldBe` correct
it "More complex game" $ do
pendingWith "fix parsing first"
it "Game with multiple levels" $ do
pendingWith "fix parsing first"
describe "Player" $ do
it "cannot die" $ do
let input = "player: { hp: infinite, inventory: [] }"
correct = Player {
playerHp = Prelude.Nothing,
inventory = [],
position = (0, 0)
playerHp = Prelude.Nothing,
inventory = [],
position = (0, 0),
showHp = True,
showInventory = False
}
Right (Entry (Tag "player") struct) = parseWith structure input
structureToPlayer struct `shouldBe` correct
@ -31,9 +57,11 @@ spec = do
it "without inventory" $ do
let input = "player: { hp: 50, inventory: [] }"
correct = Player {
playerHp = Just 50,
inventory = [],
position = (0, 0)
playerHp = Just 50,
inventory = [],
position = (0, 0),
showHp = True,
showInventory = False
}
Right (Entry (Tag "player") struct) = parseWith structure input
structureToPlayer struct `shouldBe` correct
@ -54,14 +82,12 @@ spec = do
useTimes = Prelude.Nothing
}
],
position = (0, 0)
position = (0, 0),
showHp = True,
showInventory = False
}
Right (Entry (Tag "player") struct) = parseWith structure input
structureToPlayer struct `shouldBe` correct
describe "Layout" $ do
it "simple" $ do
pending
describe "Items" $ do
it "simple" $ do
@ -117,23 +143,40 @@ spec = do
structureToActions struct `shouldBe` correct
describe "Entities" $ do
it "TODO: Simple entity" $ do
pending
it "Simple entity" $ do
pendingWith "fix parsing first"
describe "Level" $ do
it "Simple layout" $ do
let input = "{ layout: { | * * * * * * \n| * s . . e *\n| * * * * * * }, items: [], entities: [] }"
let input = "{ layout: { | * * * * * *\n| * s . . e *\n| * * * * * *\n }, items: [], entities: [] }"
correct = Level {
RPGEngine.Data.layout = [
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
[Blocked, Entrance, Walkable, Walkable, Exit, Blocked],
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
],
index = [
(0, 0, Blocked),
(1, 0, Blocked),
(2, 0, Blocked),
(3, 0, Blocked),
(4, 0, Blocked),
(5, 0, Blocked),
(0, 1, Blocked),
(1, 1, Entrance),
(2, 1, Walkable),
(3, 1, Walkable),
(4, 1, Exit),
(5, 1, Blocked),
(0, 2, Blocked),
(1, 2, Blocked),
(2, 2, Blocked),
(3, 2, Blocked),
(4, 2, Blocked),
(5, 2, Blocked)
],
items = [],
entities = []
}
Right struct = parseWith structure input
structureToLevel struct `shouldBe` correct
it "TODO: Complex layout" $ do
pending
structureToLevel struct `shouldBe` correct

View file

@ -5,6 +5,8 @@ import Test.Hspec
import RPGEngine.Data
import RPGEngine.Parse.Core
import RPGEngine.Parse.TextToStructure
import Text.Parsec.String (parseFromFile)
import GHC.IO (unsafePerformIO)
spec :: Spec
spec = do
@ -68,7 +70,7 @@ spec = do
]], "")
parseWithRest structure input `shouldBe` correct
let input = "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() } } ]"
let input = "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() } } ]"
correct = Right (Entry (Tag "entities") $ Block [ Block [
Entry (Tag "id") $ Regular $ String "door",
Entry (Tag "x") $ Regular $ Integer 4,
@ -83,6 +85,17 @@ spec = do
]], "")
parseWithRest structure input `shouldBe` correct
it "combines actions and direction" $ do
let input = "entities: [ { direction: left, actions: { [inventoryContains(key)] useItem(key), [] leave() } } ]"
correct = Right (Entry (Tag "entities") $ Block [ Block [
Entry (Tag "direction") $ Regular $ Direction West,
Entry (Tag "actions") $ Block [
Entry (ConditionList [InventoryContains "key"]) $ Regular $ Action $ UseItem "key",
Entry (ConditionList []) $ Regular $ Action Leave
]
]], "")
parseWithRest structure input `shouldBe` correct
it "can parse entries" $ do
let input = "id: \"dagger\""
correct = Right $ Entry (Tag "id") $ Regular $ String "dagger"
@ -252,22 +265,21 @@ spec = do
parseWith RPGEngine.Parse.TextToStructure.direction input `shouldBe` correct
it "can parse layouts" $ do
let input = "| * * * * * * * *\n| * s . . . . e *\n| * * * * * * * *"
let input = "{ | * * * * * * * *\n | * s . . . . e *\n | * * * * * * * *\n }"
correct = Right $ Layout [
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
[Blocked, Entrance, Walkable, Walkable, Walkable, Walkable, Exit, Blocked],
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
]
parseWith RPGEngine.Parse.TextToStructure.layout input `shouldBe` correct
parseWith value input `shouldBe` correct
let input = "{ |* * * * * * * *|* s . . . . e *|* * * * * * * * }"
-- correct = Right $ Entry (Tag "layout") $ Regular $ Layout [
correct = Right $ Layout [
let input = "layout: { | * * * * * * * *\n | * s . . . . e *\n | * * * * * * * *\n }"
correct = Right $ Entry (Tag "layout") $ Regular $ Layout [
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
[Blocked, Entrance, Walkable, Walkable, Walkable, Walkable, Exit, Blocked],
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
]
parseWith RPGEngine.Parse.TextToStructure.value input `shouldBe` correct
parseWith structure input `shouldBe` correct
describe "Brackets" $ do
it "matches closing <" $ do
@ -289,3 +301,75 @@ spec = do
let input = '['
correct = ']'
getMatchingClosingBracket input `shouldBe` correct
describe "Full game file" $ do
it "single level" $ do
let input = "player: {\n hp: 50,\n inventory: []\n}\n\nlevels: [\n {\n layout: {\n | * * * * * *\n | * s . . e *\n | * * * * * *\n },\n \n items: [],\n\n entities: []\n\n\n }\n]"
correct = Right [
Entry (Tag "player") $ Block [
Entry (Tag "hp") $ Regular $ Integer 50,
Entry (Tag "inventory") $ Block []
],
Entry (Tag "levels") $ Block [ Block [
Entry (Tag "layout") $ Regular $ Layout [
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
[Blocked, Entrance, Walkable, Walkable, Exit, Blocked],
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
],
Entry (Tag "items") $ Block [],
Entry (Tag "entities") $ Block []
]]
]
parseWith gameFile input `shouldBe` correct
it "two levels" $ do
let input = "player: {\n hp: 50,\n inventory: []\n}\n\nlevels: [\n {\n layout: {\n | * * * * * *\n | * s . . e *\n | * * * * * *\n },\n \n items: [],\n\n entities: []\n },\n {\n layout: {\n | * * *\n | * e *\n | * . *\n | * . *\n | * . *\n | * . *\n | * s *\n | * * *\n },\n\n items: [],\n\n entities: []\n }\n]"
correct = Right [
Entry (Tag "player") $ Block [
Entry (Tag "hp") $ Regular $ Integer 50,
Entry (Tag "inventory") $ Block []
],
Entry (Tag "levels") $ Block [
Block [
Entry (Tag "layout") $ Regular $ Layout [
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
[Blocked, Entrance, Walkable, Walkable, Exit, Blocked],
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
],
Entry (Tag "items") $ Block [],
Entry (Tag "entities") $ Block []
], Block [
Entry (Tag "layout") $ Regular $ Layout [
[Blocked,Blocked,Blocked],
[Blocked,Exit,Blocked],
[Blocked,Walkable,Blocked],
[Blocked,Walkable,Blocked],
[Blocked,Walkable,Blocked],
[Blocked,Walkable,Blocked],
[Blocked,Entrance,Blocked],
[Blocked,Blocked,Blocked]
],
Entry (Tag "items") $ Block [],
Entry (Tag "entities") $ Block []
]
]
]
parseWith gameFile input `shouldBe` correct
it "from file" $ do
let correct = Right [
Entry (Tag "player") $ Block [
Entry (Tag "hp") $ Regular $ Integer 50,
Entry (Tag "inventory") $ Block []
],
Entry (Tag "levels") $ Block [ Block [
Entry (Tag "layout") $ Regular $ Layout [
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked],
[Blocked, Entrance, Walkable, Walkable, Exit, Blocked],
[Blocked, Blocked, Blocked, Blocked, Blocked, Blocked]
],
Entry (Tag "items") $ Block [],
Entry (Tag "entities") $ Block []
]]
]
unsafePerformIO (parseFromFile gameFile "levels/level1.txt") `shouldBe` correct

View file

@ -1 +1,18 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
-------------------------- How to use Hspec --------------------------
-- If a test has not yet been written:
-- Use `pending` or `pendingWith`.
-- it "Description" $ do
-- pendingWith "Reason"
-- Temporarily disable running a test:
-- Replace `it` with `xit`
-- xit "Description" $ do ...
-- Temporarily only run a specific test:
-- Put `focus` in front.
-- it "Description" $ do ...
-- becomes
-- focus $ it "Description" $ do ...