first commit
This commit is contained in:
commit
78d26a57bf
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
dist
|
||||
dist-*
|
||||
cabal-dev
|
||||
*.o
|
||||
*.hi
|
||||
*.chi
|
||||
*.chs.h
|
||||
*.dyn_o
|
||||
*.dyn_hi
|
||||
.hpc
|
||||
.hsenv
|
||||
.cabal-sandbox/
|
||||
cabal.sandbox.config
|
||||
*.prof
|
||||
*.aux
|
||||
*.hp
|
||||
*.eventlog
|
||||
.stack-work/
|
||||
cabal.project.local
|
41
.travis.yml
Normal file
41
.travis.yml
Normal file
@ -0,0 +1,41 @@
|
||||
# This is the simple Travis configuration, which is intended for use
|
||||
# on applications which do not require cross-platform and
|
||||
# multiple-GHC-version support. For more information and other
|
||||
# options, see:
|
||||
#
|
||||
# https://docs.haskellstack.org/en/stable/travis_ci/
|
||||
#
|
||||
# Copy these contents into the root directory of your Github project in a file
|
||||
# named .travis.yml
|
||||
|
||||
# Use new container infrastructure to enable caching
|
||||
sudo: false
|
||||
|
||||
# Do not choose a language; we provide our own build tools.
|
||||
language: generic
|
||||
|
||||
# Caching so the next build will be fast too.
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.stack
|
||||
|
||||
# Ensure necessary system libraries are present
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libgmp-dev
|
||||
|
||||
before_install:
|
||||
# Download and unpack the stack executable
|
||||
- mkdir -p ~/.local/bin
|
||||
- export PATH=$HOME/.local/bin:$PATH
|
||||
- travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
|
||||
|
||||
install:
|
||||
# Build dependencies
|
||||
- stack --no-terminal --install-ghc test --only-dependencies
|
||||
|
||||
script:
|
||||
# Build the package, its tests, and its docs and run the tests
|
||||
- stack --no-terminal test --haddock --no-haddock-deps
|
||||
|
30
LICENSE
Normal file
30
LICENSE
Normal file
@ -0,0 +1,30 @@
|
||||
Copyright Author name here (c) 2017
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* Neither the name of Author name here nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
88
README.md
Normal file
88
README.md
Normal file
@ -0,0 +1,88 @@
|
||||
Übungszettel 4
|
||||
==============
|
||||
|
||||
In dieser und den nächsten Übungen werden wir uns mit der (Weiter-)Entwicklung
|
||||
eines Spiels beschäftigen.
|
||||
|
||||
Bisher umfasst das Spiel die folgenden Module:
|
||||
* GameTypes.hs
|
||||
* GameLogic.hs
|
||||
* GameDraw.hs
|
||||
* GameConfig.hs
|
||||
* GameMain.hs
|
||||
|
||||
Sie können es mit `stack exec Game` ausprobieren.
|
||||
|
||||
Wie Sie sehen werden, sind uns bei der Implementation dieses Spiels leider
|
||||
einige Fehler unterlaufen. Außerdem fehlen noch einige wichtige Features,
|
||||
aber zum Glück kennen sich unsere Tutanden gut genug aus, um uns zu helfen!
|
||||
|
||||
Issue 1
|
||||
---------
|
||||
Sie haben in der Vorlesung bereits *Record syntax* kennengelernt. Im Modul
|
||||
`GameLogic.hs` unter "Initialization" findet sich die unschöne Funktion `newWorld`.
|
||||
Geben Sie dieser Funktion eine anschauliche Form, indem Sie Teilausdrücke
|
||||
refaktorisieren (in sinnvollen Funktionen oder `where`-/`let`-Klauseln) und
|
||||
verwenden Sie *Record syntax* um eine default World anzulegen!
|
||||
Bedenken Sie, dass sie zur Zuweisung einzelner *Record fields* genau deren Namen
|
||||
verwenden müssen (nicht die daraus erstellten Lenses).
|
||||
|
||||
Issue 2
|
||||
---------
|
||||
In der Datei `GameDraw.hs` wird bestimmt, wie eine `World` angezeigt wird.
|
||||
1. Ergänzen Sie den Code, so dass ein toter Boss als rotes `'T'`, ein toter Hero
|
||||
als grünes `'T'` dargestellt wird.
|
||||
2. Ergänzen Sie das Spiel um einen visuellen Hinweis, der anzeigt, ob das Spiel
|
||||
pausiert ist.
|
||||
|
||||
Issue 3
|
||||
---------
|
||||
Obwohl die Spielerbewegung einwandfrei funktioniert, schlägt der entsprechende
|
||||
QuickCheck-Test fehl! Korrigieren Sie den QuickCheck-Test.
|
||||
|
||||
Issue 4
|
||||
---------
|
||||
1. Implementieren Sie eine neue `Action`, die die Blickrichtung des Helden ändert,
|
||||
ohne dass ein World-Update stattfindet. Erweitern Sie hierfür entsprechend den
|
||||
`Action`-Typ in `GameTypes.hs`, modifizieren Sie `performAction` in `GameLogic.hs`
|
||||
und schreiben Sie eine Funktion `changeDirection :: Direction -> World -> World`.
|
||||
Erweitern Sie außerdem `getAction` in `GameConfig.hs`, so dass gilt:
|
||||
-- `'h'` führt zu Blick nach `West`
|
||||
-- `'j'` führt zu Blick nach `North`
|
||||
-- `'k'` führt zu Blick nach `South`
|
||||
-- `'l'` führt zu Blick nach `East`.
|
||||
|
||||
2. Eine Kollegin hat begonnen ein neues Feature zu implementieren: Eine neue `Action`
|
||||
soll dem Spieler/der Spielerin das Stellen von Fallen vor dem Helden ermöglichen.
|
||||
Bewegt sich ein Monster auf eine solche Falle, soll diese zuschnappen und Schaden
|
||||
zufügen. Die Kollegin hat bereits einen `Action`-Wert (`SetTrap`) und einen `Entity`-
|
||||
Wert (`Trap`) in `GameTypes.hs` angelegt und `getAction` in `GameConfig.hs` angepasst.
|
||||
Im GameLogic-Modul hat sie die zu modifizierenden Stellen mit einem `-- TODO`-Kommentar
|
||||
markiert. Bringen Sie ihre Arbeit zuende.
|
||||
|
||||
Bonus: Issue 5
|
||||
---------
|
||||
An einigen Stellen im Code fügen Entities Schaden zu.
|
||||
Hard coded finden sich folgende Schadenswerte:
|
||||
-- Angreifender `Hero` fügt Boss 45 Schaden zu
|
||||
-- Explodierende `Bomb` fügt Held 30 bzw. entfernter 15 Schaden zu
|
||||
-- Läuft der Held in ein `Fire` fügt es ihm 15 Schaden zu
|
||||
-- Läuft der Boss in eine `Trap` fügt diese ihm x Schaden zu
|
||||
Implementieren Sie ein sinnvolles Schadensystem, in dem Sie den Typ `Stats` um
|
||||
ein Feld `_damage :: Integer` erweitern. Ein großes Feuer soll mehr Schaden zufügen
|
||||
als ein kleines. Passen Sie die `GameLogic` entsprechend an.
|
||||
Hinweis: Trap hat bisher keine Stats. Wie Sie hier verfahren, ist Ihnen überlassen.
|
||||
|
||||
|
||||
Appendix:
|
||||
--------
|
||||
|
||||
Die wichtigsten Lens operators (**unbedingt anschauen**):
|
||||
https://github.com/ekmett/lens/wiki/Operators
|
||||
|
||||
Auch das hier ist definitv einen Blick Wert:
|
||||
http://intolerable.me/lens-operators-intro/
|
||||
|
||||
Und hier noch ein Link zu
|
||||
Setter: https://hackage.haskell.org/package/lens-4.15.2/docs/Control-Lens-Setter.html
|
||||
|
71
app/GameMain.hs
Normal file
71
app/GameMain.hs
Normal file
@ -0,0 +1,71 @@
|
||||
module Main where
|
||||
|
||||
import GameTypes
|
||||
import GameLogic
|
||||
import GameConfig
|
||||
import GameDraw
|
||||
import Draw
|
||||
import Control.Lens
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import Linear.V2
|
||||
import Control.Exception
|
||||
import System.IO
|
||||
import qualified Data.Map as M
|
||||
|
||||
|
||||
{- ncurses view -}
|
||||
|
||||
main :: IO ()
|
||||
main = play' opts 60 world renderWorld (\_ w -> w) handleEvent
|
||||
where world = newWorldWithBoss (V2 10 1) (V2 10 16) (defaultConfig^.worldSize)
|
||||
opts = defaultConfig^.ncursesOpts
|
||||
|
||||
handleEvent :: Event -> World -> World
|
||||
handleEvent (EventCharacter c) w = fromMaybe w $ performAction w <$> getAction c
|
||||
handleEvent _ w = w
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{- Terminal view -}
|
||||
|
||||
main' :: IO ()
|
||||
main' = gameLoop $ newWorldWithBoss (V2 10 1) (V2 10 16) (20, 32)
|
||||
|
||||
gameLoop :: World -> IO ()
|
||||
gameLoop oldW = do
|
||||
putStrLn ""
|
||||
renderWorld' oldW
|
||||
putStrLn "Use 'w', 'a', 's', 'd' to navigate through the world. Press ' ' to attack, 'p' to pause."
|
||||
input <- getChar
|
||||
let newW = fromMaybe oldW $ performAction oldW <$> getAction input
|
||||
gameLoop newW
|
||||
|
||||
renderWorld' :: World -> IO ()
|
||||
renderWorld' world = do
|
||||
let graphics = map (map (renderEntity)) (layout $ M.toAscList $ world^.entities)
|
||||
layout = groupBy (\(u,_) (v,_) -> u^._y /= v^._y)
|
||||
mapM_ putStrLn graphics
|
||||
|
||||
renderEntity :: (V2 Integer, Entity) -> Char
|
||||
renderEntity (p, Floor) = '-'
|
||||
renderEntity (p, (Hero s)) = 'H'
|
||||
renderEntity (p, (Boss s)) = 'M'
|
||||
renderEntity (p, (Wall)) = '#'
|
||||
renderEntity (p, (Bomb _)) = 'o'
|
||||
renderEntity (p, (Fire s)) | s^.life < 2 = 'x'
|
||||
| otherwise = 'X'
|
||||
renderEntity _ = '?'
|
||||
|
||||
hide :: IO a -> IO a
|
||||
hide action = bracket_
|
||||
(hSetEcho stdin False)
|
||||
(hSetEcho stdin True)
|
||||
action
|
||||
|
||||
|
||||
|
||||
|
191
src/Draw.hs
Normal file
191
src/Draw.hs
Normal file
@ -0,0 +1,191 @@
|
||||
{-# Language DeriveFunctor #-}
|
||||
{-# Language DeriveGeneric #-}
|
||||
{-# Language TemplateHaskell #-}
|
||||
|
||||
module Draw
|
||||
( Options(..)
|
||||
, VisObject(..)
|
||||
, Color(..)
|
||||
, Drawable(..)
|
||||
, display
|
||||
, display'
|
||||
, simulate
|
||||
, simulate'
|
||||
, play
|
||||
, play'
|
||||
, Event(..)
|
||||
) where
|
||||
|
||||
import GHC.Generics ( Generic )
|
||||
import Data.Maybe (fromMaybe, isNothing)
|
||||
import Control.Monad (unless)
|
||||
import UI.NCurses hiding (Color)
|
||||
import System.CPUTime
|
||||
import Control.Monad.IfElse
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Lens
|
||||
import Linear.V2
|
||||
|
||||
data Color = White | Black | Red | Green deriving (Eq,Show)
|
||||
|
||||
data Options =
|
||||
Options
|
||||
{ _optBackgroundColor :: Maybe Color -- ^ optional background color
|
||||
, _optWindowSize :: Maybe (Integer,Integer) -- ^ optional (row,col) window size
|
||||
, _optWindowPosition :: Maybe (Integer,Integer) -- ^ optional (row,col) window origin
|
||||
} deriving Show
|
||||
|
||||
makeLenses (''Options)
|
||||
|
||||
data VisObject a = VisObjects [VisObject a]
|
||||
| Trans (V2 Integer) (VisObject a)
|
||||
| LineV (V2 Integer) a Color
|
||||
| LineH (V2 Integer) a Color
|
||||
| Char (V2 Integer) a Color
|
||||
| Text (V2 Integer) [a] Color
|
||||
deriving (Generic, Functor)
|
||||
|
||||
class Drawable a where
|
||||
c :: a -> Char
|
||||
|
||||
instance Drawable Char where
|
||||
c = id
|
||||
|
||||
type ColorCache = (ColorID, ColorID, ColorID, ColorID)
|
||||
|
||||
initColors :: Curses ColorCache
|
||||
initColors = do
|
||||
cWhite <- newColorID ColorWhite ColorWhite 10
|
||||
cBlack <- newColorID ColorBlack ColorWhite 11
|
||||
cRed <- newColorID ColorRed ColorWhite 12
|
||||
cGreen <- newColorID ColorGreen ColorWhite 13
|
||||
return ( cWhite, cBlack, cRed, cGreen)
|
||||
|
||||
white = _1
|
||||
black = _2
|
||||
red = _3
|
||||
green = _4
|
||||
|
||||
initWindow :: Options -> Curses (ColorCache, Window)
|
||||
initWindow options = do
|
||||
win <- newWindow
|
||||
(fromMaybe 60 $ options^?optWindowSize._Just._1)
|
||||
(fromMaybe 80 $ options^?optWindowSize._Just._2)
|
||||
(fromMaybe 0 $ options^?optWindowPosition._Just._2)
|
||||
(fromMaybe 0 $ options^?optWindowPosition._Just._1)
|
||||
colors <- initColors
|
||||
bgRed <- newColorID ColorWhite ColorRed 14
|
||||
bgGreen <- newColorID ColorWhite ColorGreen 15
|
||||
let bgCol Black = colors^.white
|
||||
bgCol White = colors^.black
|
||||
bgCol Red = bgRed
|
||||
bgCol Green = bgGreen
|
||||
updateWindow win $ setColor $ bgCol
|
||||
(fromMaybe Black $ options^.optBackgroundColor)
|
||||
return (colors, win)
|
||||
|
||||
display :: Drawable a => Options -> VisObject a -> Curses ()
|
||||
display options toDraw = do
|
||||
(colors, win) <- initWindow options
|
||||
updateWindow win $ clear >> rawDraw colors toDraw
|
||||
render
|
||||
waitKeyEvent win
|
||||
|
||||
display' options toDraw = runCurses $ display options toDraw
|
||||
|
||||
waitKeyEvent :: Window -> Curses ()
|
||||
waitKeyEvent win = do
|
||||
let isKeyEvent (Just (EventCharacter _)) = True
|
||||
isKeyEvent (Just (EventSpecialKey _)) = True
|
||||
isKeyEvent _ = False
|
||||
event <- getEvent win Nothing
|
||||
unless (isKeyEvent event) $ waitKeyEvent win
|
||||
|
||||
waitQKey :: Window -> Integer -> Curses Bool
|
||||
waitQKey win timeout = do
|
||||
start <- liftIO getCPUTime
|
||||
let isQKey (Just (EventCharacter 'q')) = True
|
||||
isQKey _ = False
|
||||
event <- getEvent win $ Just timeout
|
||||
if isNothing event
|
||||
then return False
|
||||
else if isQKey event
|
||||
then return True
|
||||
else do
|
||||
now <- liftIO getCPUTime
|
||||
waitQKey win $ timeout - round
|
||||
((fromIntegral (now - start)::Double) / (10^9::Double))
|
||||
|
||||
simulate' options sampleRate model drawFunction simulateFunction = runCurses $ simulate options sampleRate model drawFunction simulateFunction
|
||||
simulate :: (Drawable a) => Options -> Integer -> world -> (world -> VisObject a) -> (Double -> world -> world) -> Curses ()
|
||||
simulate options sampleRate model drawFunction simulateFunction = do
|
||||
(colors, win) <- initWindow options
|
||||
let simulateLoop model = do
|
||||
start <- liftIO getCPUTime
|
||||
updateWindow win $ clear >> rawDraw colors (drawFunction model)
|
||||
render
|
||||
isQKey <- waitQKey win sampleRate
|
||||
unless isQKey $ do
|
||||
now <- liftIO getCPUTime
|
||||
simulateLoop $ simulateFunction ((fromIntegral (now - start)::Double) / 10^9) model
|
||||
|
||||
simulateLoop model
|
||||
|
||||
play' options sampleRate model drawFunction simulateFunction eventHamdler =
|
||||
runCurses $ play options sampleRate model drawFunction simulateFunction eventHamdler
|
||||
play :: (Drawable a)
|
||||
=> Options
|
||||
-> Integer
|
||||
-> world
|
||||
-> (world -> VisObject a)
|
||||
-> (Double -> world -> world)
|
||||
-> (Event -> world -> world)
|
||||
-> Curses ()
|
||||
play options sampleRate model drawFunction simulateFunction eventHamdler = do
|
||||
(colors, win) <- initWindow options
|
||||
let playLoop model = do
|
||||
start <- liftIO getCPUTime
|
||||
updateWindow win $ clear >> rawDraw colors (drawFunction model)
|
||||
render
|
||||
event <- getEvent win $ Just sampleRate
|
||||
now <- liftIO getCPUTime
|
||||
let model' = simulateFunction ((fromIntegral (now - start)::Double) / 10^9) model
|
||||
model'' = case event of
|
||||
(Just event') -> eventHamdler event' model'
|
||||
_ -> model'
|
||||
playLoop model''
|
||||
playLoop model
|
||||
|
||||
|
||||
rawDraw :: Drawable a => ColorCache -> VisObject a -> Update ()
|
||||
rawDraw colors (VisObjects a) = sequence_ $ rawDraw colors <$> a
|
||||
rawDraw colors (Trans t d) = rawDraw colors $ translate t d
|
||||
rawDraw colors (Text pos t col) = do
|
||||
moveCursor (pos^._x) (pos^._y)
|
||||
sequence_ $ drawC <$> t
|
||||
where drawC a = drawGlyph $ Glyph (a^.to c) [col2Attr colors col]
|
||||
rawDraw colors (Char pos a color) = do
|
||||
moveCursor (pos^._x) (pos^._y)
|
||||
drawGlyph $ Glyph (a^.to c) [color^.to attrColor]
|
||||
where attrColor = col2Attr colors
|
||||
rawDraw colors (LineH pos a color) = do
|
||||
moveCursor (pos^._x) 0
|
||||
drawLineH (Just $ Glyph (a^.to c) [col2Attr colors color]) 20
|
||||
rawDraw colors (LineV pos a color) = do
|
||||
moveCursor 0 (pos^._y)
|
||||
drawLineV (Just $ Glyph (a^.to c) [col2Attr colors color]) 20
|
||||
|
||||
col2Attr :: ColorCache -> Color -> Attribute
|
||||
col2Attr colors White = AttributeColor $ colors^.white
|
||||
col2Attr colors Black = AttributeColor $ colors^.black
|
||||
col2Attr colors Green = AttributeColor $ colors^.green
|
||||
col2Attr colors Red = AttributeColor $ colors^.red
|
||||
|
||||
translate :: V2 Integer -> VisObject a -> VisObject a
|
||||
translate t (VisObjects a) = VisObjects $ translate t <$> a
|
||||
translate t (Trans t' d) = translate (t+t') d
|
||||
translate t (Text pos a c) = Text (pos+t) a c
|
||||
translate t (LineV pos a c) = LineV (pos+t & _x .~ 0) a c
|
||||
translate t (LineH pos a c) = LineH (pos+t & _y .~ 0) a c
|
||||
translate t (Char pos a c) = Char (pos+t) a c
|
||||
|
47
src/GameConfig.hs
Normal file
47
src/GameConfig.hs
Normal file
@ -0,0 +1,47 @@
|
||||
{-# Language TemplateHaskell #-}
|
||||
module GameConfig
|
||||
( GameConfig(..)
|
||||
, defaultConfig
|
||||
, getAction
|
||||
, worldSize
|
||||
, ncursesOpts
|
||||
, keyMap
|
||||
) where
|
||||
|
||||
import Draw
|
||||
import Control.Lens
|
||||
import GameTypes
|
||||
import Linear
|
||||
|
||||
-- | Holds the basic configurations of the game.
|
||||
data GameConfig = GameConfig
|
||||
{ _worldSize :: (Integer, Integer)
|
||||
, _ncursesOpts :: Options
|
||||
, _keyMap :: Char -> Maybe Action
|
||||
}
|
||||
|
||||
-- | Specifies a game configuration for a given size, background color and key mapping.
|
||||
makeConfig :: (Integer, Integer) -> Maybe Color -> (Char -> Maybe Action) -> GameConfig
|
||||
makeConfig size optBackgroundColor keyMapping = GameConfig size opts keyMapping
|
||||
where opts = Options optBackgroundColor (Just $ size & _1 +~ 5 & _2 +~ 5) Nothing
|
||||
|
||||
-- | Default game configuration.
|
||||
defaultConfig :: GameConfig
|
||||
defaultConfig = makeConfig (20, 32) (Just Red) getAction
|
||||
|
||||
-- | Default key mapping.
|
||||
getAction :: Char -> Maybe Action
|
||||
getAction 'w' = Just $ Move North
|
||||
getAction 'a' = Just $ Move West
|
||||
getAction 's' = Just $ Move South
|
||||
getAction 'd' = Just $ Move East
|
||||
getAction ' ' = Just Attack
|
||||
getAction 'p' = Just Pause
|
||||
getAction 't' = Just SetTrap
|
||||
getAction _ = Nothing
|
||||
|
||||
|
||||
-- * Lenses for the GameConfig type
|
||||
|
||||
$(makeLenses ''GameConfig)
|
||||
|
40
src/GameDraw.hs
Normal file
40
src/GameDraw.hs
Normal file
@ -0,0 +1,40 @@
|
||||
module GameDraw
|
||||
( toVisObject
|
||||
, renderWorld
|
||||
) where
|
||||
|
||||
import Draw
|
||||
import GameTypes
|
||||
import Linear
|
||||
import Control.Lens
|
||||
import qualified Data.Map as M
|
||||
|
||||
toVisObject :: (V2 Integer, Entity) -> VisObject Char
|
||||
toVisObject (p, Hero s) =
|
||||
VisObjects [ Text (V2 1 0) ("Life: " ++ s^.life.to show ++ " ") Green
|
||||
, case () of
|
||||
_ | s^.orient == North -> Char p 'N' Green
|
||||
| s^.orient == South -> Char p 'S' Green
|
||||
| s^.orient == East -> Char p 'E' Green
|
||||
| s^.orient == West -> Char p 'W' Green
|
||||
]
|
||||
toVisObject (p, Boss s) =
|
||||
VisObjects [ Text (V2 0 0) ("Bosslife: " ++ s^.life.to show ++ " ") Red
|
||||
, Char p 'M' Black
|
||||
]
|
||||
toVisObject (p, Floor) = Char p '-' White
|
||||
toVisObject (p, Wall) = Char p '#' Black
|
||||
toVisObject (p, Bomb _) = Char p '💣' Black
|
||||
toVisObject (p, Fire s) | s^.life < 2 = Char p '🔥' Red
|
||||
| otherwise = Char p '🔥' Red
|
||||
|
||||
renderWorld :: World -> VisObject Char
|
||||
renderWorld w =
|
||||
let map = toVisObject . (_1._x +~ (2::Integer)) <$> (w^.entities.to M.toList)
|
||||
wonMsg = Text (V2 1 12) " WON! " Green
|
||||
lostMsg = Text (V2 1 12) " Lost " Red
|
||||
heroIsDead = w^.entities.at (w^.heroPos)^?_Just._Hero.life.to (<=0) & and
|
||||
in case w^.mode of
|
||||
Ended | heroIsDead -> VisObjects $ lostMsg : map
|
||||
| otherwise -> VisObjects $ wonMsg : map
|
||||
_ -> VisObjects map
|
188
src/GameLogic.hs
Normal file
188
src/GameLogic.hs
Normal file
@ -0,0 +1,188 @@
|
||||
module GameLogic where
|
||||
|
||||
import GameTypes
|
||||
import Control.Lens
|
||||
import Linear
|
||||
import Data.Maybe
|
||||
import Data.Char
|
||||
import qualified Data.Map as M
|
||||
|
||||
|
||||
{- * Initialization -}
|
||||
|
||||
-- | Generates a world with the hero and an endboss at a specific positions
|
||||
-- in a dungeon with a specific size.
|
||||
newWorldWithBoss :: V2 Integer -> V2 Integer -> (Integer, Integer) -> World -- XXX: ??! This looks very ugly. What does it even do? Some refactoring should be done. Also, use record syntax to make things clear..
|
||||
newWorldWithBoss hPos mPos size = entities.at mPos._Just .~ Boss (Stats West 500)
|
||||
$ entities.at hPos ?~ Hero (Stats East 100)
|
||||
$ World Active 0
|
||||
(walls (atBorder size) $ M.fromList $ map (flip (,) Floor) $ V2 <$> [0..(size^._1)] <*> [0..(size^._2)])
|
||||
hPos
|
||||
|
||||
-- | Creates a new bomb with a certain life time and orientation.
|
||||
newBomb :: Integer -> Direction -> Entity
|
||||
newBomb l d = Bomb (Stats d l)
|
||||
|
||||
-- | Creates a new small fire
|
||||
fire :: Entity
|
||||
fire = Fire (Stats North 1)
|
||||
|
||||
-- | Creates a new big fire.
|
||||
bigFire :: Entity
|
||||
bigFire = Fire (Stats North 3)
|
||||
|
||||
-- | Tests whether a position is on the edge of the map.
|
||||
atBorder :: (Integer,Integer) -> Position -> Bool
|
||||
atBorder (bx,by) (V2 x y) = x `elem` [bx,0] || y `elem` [by,0]
|
||||
|
||||
-- | Places walls on the edge of the dungeon map.
|
||||
walls :: (V2 Integer -> Bool) -> Dungeon -> Dungeon
|
||||
walls test = M.mapWithKey (\pos c -> if test pos then Wall else c)
|
||||
|
||||
|
||||
{- * Game logic -}
|
||||
|
||||
-- | Performs an action on the game state (world).
|
||||
performAction :: World -> Action -> World
|
||||
performAction w a =
|
||||
case (w^.mode,a) of
|
||||
(Active,Move dir) -> updateWorld.moveHero (toVector dir) $ w
|
||||
(Active,Attack) -> updateWorld.attack $ w
|
||||
(Active,SetTrap) -> w -- TODO setTrap...
|
||||
(Active,Pause) -> mode.~Paused $ w
|
||||
(Paused,Pause) -> mode.~Active $ w
|
||||
_ -> w
|
||||
|
||||
-- | Updates the all the entities in the world by calling the function updateDungeon
|
||||
-- and registers a game turn.
|
||||
updateWorld :: World -> World
|
||||
updateWorld = updateDungeon.count
|
||||
where count = turn +~ 1
|
||||
|
||||
-- | Updates all the entities in the dungeon.
|
||||
updateDungeon :: World -> World
|
||||
updateDungeon w = M.foldrWithKey updateEnt w (w^.entities)
|
||||
where updateEnt pos ent =
|
||||
case ent of
|
||||
Bomb s | s^.life.to (<=0) -> explodeBomb pos
|
||||
| otherwise -> moveBomb pos (s^.orient.to toVector)
|
||||
Fire s | s^.life.to (<=0) -> entities %~ M.insert pos Floor
|
||||
| otherwise -> entities.at pos._Just._Fire.life -~ 1
|
||||
Boss s | s^.life.to (<=0) -> (mode .~ Ended) . (entities %~ M.insert pos (Boss s ))
|
||||
| otherwise -> updateBoss pos
|
||||
Hero s | s^.life.to (<=0) -> (mode .~ Ended) . (entities %~ M.insert pos (Hero s ))
|
||||
| otherwise -> id
|
||||
_ -> id
|
||||
|
||||
-- | Updates the boss at a given position. The bosses behavior depends on the turn counter.
|
||||
-- Generally the boss tries to approach the hero and attack him by throwing bombs at him.
|
||||
updateBoss :: Position -> World -> World
|
||||
updateBoss pos w =
|
||||
let atHero = badAiming $ w^.heroPos - pos
|
||||
spawnPos = pos + atHero
|
||||
withRandomTimer = w^.turn.to (\t -> 2 + (1 + mod t 5)*(1 + mod t 3))
|
||||
in case w^.turn.to (`mod` 16) of
|
||||
0 -> moveBoss pos (V2 1 0) w
|
||||
1 -> moveBoss pos atHero w
|
||||
2 -> throwBomb spawnPos withRandomTimer atHero w
|
||||
3 -> w
|
||||
4 -> moveBoss pos (negate atHero) w
|
||||
5 -> moveBoss pos (V2 (-1) 0) w
|
||||
6 -> moveBoss pos (V2 0 (-1)) w
|
||||
7 -> throwBomb spawnPos withRandomTimer atHero w
|
||||
8 -> moveBoss pos (negate atHero) w
|
||||
9 -> moveBoss pos atHero w
|
||||
10 -> moveBoss pos (V2 0 1) w
|
||||
11 -> moveBoss pos atHero w
|
||||
12 -> moveBoss pos atHero w
|
||||
13 -> moveBoss pos atHero w
|
||||
14 -> moveBoss pos atHero w
|
||||
15 -> throwBomb spawnPos withRandomTimer atHero w
|
||||
_ -> w
|
||||
|
||||
-- | Calculates the approximate direction of the hero.
|
||||
badAiming :: DirVector -> DirVector
|
||||
badAiming relPos =
|
||||
case abs <$> relPos of
|
||||
p | p^._x < p^._y -> signum relPos & _x .~ 0
|
||||
| otherwise -> signum relPos & _y .~ 0
|
||||
|
||||
-- | If possible, this function moves the hero into a given direction by calling the function moveFrom.
|
||||
-- Otherwise, only the orientation is changed towards the intended direction of movement.
|
||||
moveHero :: DirVector -> World -> World
|
||||
moveHero dir w =
|
||||
let pos = w^.heroPos
|
||||
aim = pos + dir
|
||||
in case w^.entities.at aim of
|
||||
Just Floor -> w & heroPos .~ aim & entities %~ moveFrom pos dir
|
||||
Just (Fire _) -> w & heroPos .~ aim & entities.at pos._Just._Hero.life -~ 10 & entities %~ moveFrom pos dir -- XXX magic number.. issue: implement damage system
|
||||
_ -> w & entities.at pos._Just.stats.orient .~ (fromVector dir)
|
||||
|
||||
-- | If possible, this function moves the boss into a given direction by calling the function moveFrom.
|
||||
-- Otherwise, only the orientation is changed towards the intended direction of movement.
|
||||
moveBoss :: Position -> DirVector -> World -> World
|
||||
moveBoss pos dir w =
|
||||
let aim = pos + dir
|
||||
in case w^.entities.at aim of
|
||||
Just Floor -> w & entities %~ moveFrom pos dir
|
||||
Just Trap -> w -- TODO setTrap...
|
||||
_ -> w & entities.at pos._Just.stats.orient .~ (fromVector dir)
|
||||
|
||||
-- | If possible, this function moves a bomb at a given position into a given direction
|
||||
-- by calling the function moveFrom. Otherwise, the bomb explodes.
|
||||
moveBomb :: Position -> DirVector -> World -> World
|
||||
moveBomb pos dir w =
|
||||
let aim = pos + dir
|
||||
in case w^.entities.at aim of
|
||||
Just Floor -> w & entities %~ moveFrom pos dir
|
||||
Just (Fire s) | s^.life < 2 -> w & entities %~ moveFrom pos dir
|
||||
| otherwise -> w & explodeBomb pos
|
||||
_ -> w & explodeBomb pos
|
||||
|
||||
-- | Moves any movable entity with at a given position into a given direction.
|
||||
moveFrom :: Position -> DirVector -> Dungeon -> Dungeon
|
||||
moveFrom p d dungeon =
|
||||
case dungeon^.at p of
|
||||
Just (Hero s) -> M.insert (p+d) (Hero (s & orient .~ fromVector d)) $ M.insert p Floor dungeon
|
||||
Just (Bomb s) -> M.insert (p+d) (Bomb (s & life -~ 1)) $ M.insert p Floor dungeon
|
||||
Just (Boss s) -> M.insert (p+d) (Boss (s & orient .~ fromVector d)) $ M.insert p Floor dungeon
|
||||
_ -> dungeon
|
||||
|
||||
-- | Attacks the location in front of the hero. Erases fires, harms enemies.
|
||||
attack :: World -> World
|
||||
attack w =
|
||||
let pos = w^.heroPos
|
||||
maybeAim = w^.entities.at pos^?_Just.stats.orient.to ((pos +).toVector)
|
||||
in case maybeAim >>= \aim -> (,) aim <$> w^.entities.at aim of
|
||||
Just (aim,Boss s) -> w & entities.at aim._Just._Boss.life -~ 45 -- XXX magic number.. issue: implement damage system
|
||||
Just (aim,Fire _) -> w & entities %~ M.insert aim Floor
|
||||
_ -> w
|
||||
|
||||
-- | Sets a trap in front of the hero, that harms enemies if stepped on.
|
||||
setTrap :: World -> World
|
||||
setTrap w = w -- TODO setTrap...
|
||||
|
||||
-- | Creates a bomb at a given position with specific detonation delay and orientation.
|
||||
throwBomb :: Position -> Integer -> DirVector -> World -> World
|
||||
throwBomb pos delay dir w =
|
||||
case w^.entities.at pos of
|
||||
Just Floor -> w & entities %~ M.insert pos (newBomb delay $ fromVector dir)
|
||||
_ -> w & explodeBomb pos
|
||||
|
||||
-- | Creates a big fire at a given position and eight small fires adjacent to that position.
|
||||
-- If the hero is at one of these positions he gets damaged.
|
||||
explodeBomb :: Position -> World -> World
|
||||
explodeBomb p w = foldr maybeSetFire w (around p)
|
||||
where
|
||||
maybeSetFire pos w' =
|
||||
case w'^.entities.at pos of
|
||||
Just (Bomb _) | pos==p -> w' & entities %~ M.insert pos bigFire
|
||||
| otherwise -> w' & entities %~ M.insert pos bigFire & explodeBomb pos
|
||||
Just Floor -> w' & entities %~ M.insert pos fire
|
||||
Just (Hero s) | pos==p -> w' & entities %~ M.insert pos (Hero (s & life -~ 30)) -- XXX magic number.. issue: implement damage system
|
||||
| otherwise -> w' & entities %~ M.insert pos (Hero (s & life -~ 10)) -- XXX magic number.. issue: implement damage system
|
||||
_ -> w'
|
||||
|
||||
-- | Calculates the adjacent positions of a given position
|
||||
around :: Position -> [Position]
|
||||
around pos = (+pos) <$> (V2 <$> [-1..1] <*> [-1..1])
|
124
src/GameTypes.hs
Normal file
124
src/GameTypes.hs
Normal file
@ -0,0 +1,124 @@
|
||||
{-# Language TemplateHaskell #-}
|
||||
module GameTypes where
|
||||
|
||||
import Control.Lens
|
||||
import Linear
|
||||
import qualified Data.Map as M
|
||||
|
||||
{- * Types-}
|
||||
|
||||
-- | World represents the current game state with a game mode,
|
||||
-- a turn counter, a map of entitites and the hero's position.
|
||||
data World
|
||||
= World
|
||||
{ _mode :: Mode
|
||||
, _turn :: Integer
|
||||
, _entities :: Dungeon
|
||||
, _heroPos :: Position
|
||||
} deriving (Show,Eq)
|
||||
|
||||
-- | Mode can be one of three things:
|
||||
--
|
||||
-- - Active – the game is currently running; player can control hero
|
||||
-- - Ended – the game has ended; hero or boss is dead
|
||||
-- - Paused – the game is paused; player can only unpause or restart the game
|
||||
data Mode
|
||||
= Active
|
||||
| Ended
|
||||
| Paused
|
||||
deriving (Show,Eq)
|
||||
|
||||
-- | A lazy Data.Map which associates all the entities in the
|
||||
-- dungeon with its coordinates (Position)
|
||||
type Dungeon = M.Map Position Entity
|
||||
|
||||
-- | Position of an entity in the world
|
||||
type Position = V2 Integer
|
||||
|
||||
-- | Entity can be one of six things:
|
||||
--
|
||||
-- - Hero – under the control of the player
|
||||
-- - Boss – controlled by the computer; fights the hero with bombs
|
||||
-- - Floor – empty tile in the dungeon
|
||||
-- - Wall – cannot be passed
|
||||
-- - Bomb – thrown by enemies; eventually exploding
|
||||
-- - Fire – burning tile emerging from explosions
|
||||
data Entity
|
||||
= Hero { _stats :: Stats }
|
||||
| Boss { _stats :: Stats }
|
||||
| Bomb { _stats :: Stats }
|
||||
| Fire { _stats :: Stats }
|
||||
| Trap
|
||||
| Floor
|
||||
| Wall
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Stats represents two things:
|
||||
--
|
||||
-- - Orientation – the direction, in which an entity is oriented
|
||||
-- - Life – the life energy or (!) life time of an entity
|
||||
data Stats
|
||||
= Stats
|
||||
{ _orient :: Direction
|
||||
, _life :: Integer
|
||||
} deriving (Show,Eq)
|
||||
|
||||
-- | Direction vector
|
||||
type DirVector = V2 Integer
|
||||
|
||||
-- | Direction of an entity in the world
|
||||
data Direction
|
||||
= North
|
||||
| South
|
||||
| West
|
||||
| East
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Converts to a vector (V2 Integer)
|
||||
toVector :: Direction -> DirVector
|
||||
toVector North = V2 (-1) 0
|
||||
toVector South = V2 1 0
|
||||
toVector West = V2 0 (-1)
|
||||
toVector East = V2 0 1
|
||||
|
||||
-- | Gets a Direction from a vector (V2 Integer)
|
||||
fromVector :: DirVector -> Direction
|
||||
fromVector vec =
|
||||
case abs <$> vec of
|
||||
v | v^._x > v^._y && vec^._x > 0 -> South
|
||||
| v^._x > v^._y && vec^._x <= 0 -> North
|
||||
| v^._x <= v^._y && vec^._y > 0 -> East
|
||||
| otherwise -> West
|
||||
|
||||
-- | Action can be one of three things
|
||||
--
|
||||
-- - Move – the player moves the hero in a specified direction
|
||||
-- - Attack – the player attacks the location in front of the hero
|
||||
-- - Pause – the player pauses the game
|
||||
-- - SetTrap – the player sets a trap in front of the hero
|
||||
data Action
|
||||
= Move { _movDir :: Direction }
|
||||
| Attack
|
||||
| Pause
|
||||
| SetTrap
|
||||
deriving (Show,Eq)
|
||||
|
||||
{- * Lenses for the World type -}
|
||||
$(makeLenses ''World)
|
||||
|
||||
{- * Prisms for the Entity type -}
|
||||
$(makePrisms ''Entity)
|
||||
|
||||
{- * Traversals for the Entity type -}
|
||||
$(makeLenses ''Entity)
|
||||
|
||||
{- * Lenses for the Stats type -}
|
||||
$(makeLenses ''Stats)
|
||||
|
||||
{- * Prisms for the Action type -}
|
||||
$(makePrisms ''Action)
|
||||
|
||||
{- * Traversals for the Action type -}
|
||||
$(makeLenses ''Action)
|
||||
|
||||
|
11
src/Lib.hs
Normal file
11
src/Lib.hs
Normal file
@ -0,0 +1,11 @@
|
||||
module Lib
|
||||
( someFunc
|
||||
) where
|
||||
|
||||
import GameTypes
|
||||
import GameLogic
|
||||
import GameDraw
|
||||
import GameConfig
|
||||
|
||||
someFunc :: IO ()
|
||||
someFunc = putStrLn "someFunc"
|
7
stack.yaml
Normal file
7
stack.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
flags: {}
|
||||
extra-package-dbs: []
|
||||
packages:
|
||||
- '.'
|
||||
extra-deps:
|
||||
- ncurses-0.2.16
|
||||
resolver: lts-8.12
|
40
test/Game-Spec.hs
Normal file
40
test/Game-Spec.hs
Normal file
@ -0,0 +1,40 @@
|
||||
{-# Language TypeSynonymInstances #-}
|
||||
{-# Language FlexibleInstances #-}
|
||||
import GameLogic
|
||||
import GameTypes
|
||||
import Test.QuickCheck
|
||||
import Test.Framework.Providers.QuickCheck2 (testProperty)
|
||||
import Test.Framework.Runners.Console (defaultMain)
|
||||
import Linear
|
||||
import Control.Monad
|
||||
import Data.Map
|
||||
import Control.Lens hiding (elements)
|
||||
import Data.Maybe
|
||||
|
||||
instance Arbitrary World where
|
||||
arbitrary = do
|
||||
dx <- choose (4, 100)
|
||||
dy <- choose (4, 100)
|
||||
my <- choose (1, dy-1)
|
||||
mx <- choose (1, dx-1)
|
||||
hy <- choose (1, dy-1)
|
||||
hx <- choose (1, dx-1) `suchThat` (\hx -> mx /= hx || my /= hy)
|
||||
return $ newWorld (V2 hx hy) (V2 mx my) (dx, dy)
|
||||
|
||||
type Step = V2 Integer
|
||||
|
||||
instance Arbitrary Step where
|
||||
arbitrary = elements (V2 <$> [-1,0,1] <*> [-1,0,1]) `suchThat` ((==1).quadrance)
|
||||
|
||||
bewegungProp1 :: World -> Step -> Property
|
||||
bewegungProp1 world d =
|
||||
and ((world^.entities.to findMax._1.to (subtract d) - world^.heroPos)^..each.to (> 0)) &&
|
||||
(world^.entities.at (world^.heroPos + d)^?_Just._Floor)^.to isJust ==>
|
||||
world^.to (performAction (Just $ Move d)).heroPos == (world^.heroPos)
|
||||
|
||||
bewegungTest1 = testProperty "Bewegung Test 1" bewegungProp1 -- FIXME: I think I'm broken. What's wrong with me?
|
||||
|
||||
tests = [bewegungTest1]
|
||||
|
||||
main :: IO ()
|
||||
main = defaultMain tests
|
65
zettel4.cabal
Normal file
65
zettel4.cabal
Normal file
@ -0,0 +1,65 @@
|
||||
name: zettel4
|
||||
version: 0.1.0.0
|
||||
synopsis: First Assignment of FFPiHaskell 2017
|
||||
description: Zettel 4
|
||||
homepage: https://github.com/FFPiHaskell/zettel4-skeleton#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: FFPiHaskell Tutors
|
||||
maintainer: sdressel@techfak.uni-bielefeld.de
|
||||
copyright: 2017 FFPiHaskell Tutors
|
||||
category: cli
|
||||
build-type: Simple
|
||||
extra-source-files: README.md
|
||||
cabal-version: >=1.10
|
||||
|
||||
|
||||
-- library for all things common in all exercises/not neccessary for students
|
||||
-- to solve assignments
|
||||
library
|
||||
hs-source-dirs: src
|
||||
exposed-modules: Lib
|
||||
, GameLogic
|
||||
, GameTypes
|
||||
, GameConfig
|
||||
, GameDraw
|
||||
, Draw
|
||||
build-depends: base >= 4.7 && < 5
|
||||
, ncurses
|
||||
, IfElse
|
||||
, lens
|
||||
, linear
|
||||
, containers
|
||||
default-language: Haskell2010
|
||||
|
||||
executable Game
|
||||
hs-source-dirs: app
|
||||
main-is: GameMain.hs
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||
build-depends: base
|
||||
, zettel4
|
||||
, IfElse
|
||||
, lens
|
||||
, linear
|
||||
, containers
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite Game-tests
|
||||
type: exitcode-stdio-1.0
|
||||
hs-source-dirs: test
|
||||
main-is: Game-Spec.hs
|
||||
build-depends: base
|
||||
, zettel4
|
||||
, test-framework
|
||||
, test-framework-quickcheck2
|
||||
, QuickCheck
|
||||
, linear
|
||||
, containers
|
||||
, lens
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||
default-language: Haskell2010
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/FFPiHaskell/zettel4-skeleton
|
||||
|
Loading…
Reference in New Issue
Block a user