: bekezdést jelöl, tehát ennek a segítségével tudjuk a szöveget tagolni, hogy átláthatóbb, olvashatóbb legyen. Így a nyitó és záró jelölő közé főként szöveg kerül, de lehetnek további jelölők is.
Hamarosan egy újabb ajtóhoz érsz az alagútban. A kilincs enged, de az ajtót keresztben bedeszkázták. Ha le akarod feszíteni a deszkákat a kardoddal -
jelölőben található a történet szövege. Végül az oldal befejezését jelző Dobj két kockával! A szökőkút hideg vize felfrissít, de furcsa mellékhatása van. Nyersz 1 ÉLETERŐ pontot,
attribútumban megadtam az ügyesség jellemzőt, és az
2.2. Fejlesztői dokumentáció Tehát a nagyprogramomban két részt lehet élesen elkülöníteni. Az egyik a motor, mely beolvassa az xml fájlt, átfordítja saját nyelvre, műveleteket elvégzi és ad egy kimenetet. Ez a kimenet minden információt tartalmaz a kinézettel kapcsolatban, hogy mikor érünk új oldalra, mikor van csata, hová lehet menni az adott oldalról,. . . stb.
2.2.1.
Motor
A nagyprogramomnak tárolni kell különböző információkat. Például ha kaland során felveszünk egy tárgyat, azt valahol meg kell jegyeznünk, mert a későbbiekben fel akarjuk használni. Tehát szükségünk van valamilyen állapotra, mely folyamatosan módosul, és melyet lekérdezhetünk időről időre.
2.2.2.
Játék állapota
Ezt az állapotot egy algebrai adattípussal írtam le, mely az alábbi: data GameState = GS { player_state :: PlayerState , fight_state :: Maybe FightState , page_state :: Map Die Int } deriving (Show, Read) 19
Így a játék állapotában szükségünk van a játékos állapotára, melyet lejjebb fogok részletezni. A player state írja le ezt. Mivel csaták is lehetnek a kaland során, amikor egy különös lénnyel találkozunk, ezért kell egy csata állapota is. Az implementáció miatt ez opcionális vagyis vagy van ilyen fight state vagy nincs, attól függően, hogy épp hacolunk-e vagy csak kalandozunk. A page state a kockadobások eredményeit tárolja. Amikor dobunk egyet a kockával, később le akarjuk kérdezni az eredményét, mert ez befolyásolja kalandunk további kimenetelét az adott oldalon.
2.2.3.
A játékos állapota
Kaland írásánál, kitalálásánál, hogy a történet minél színesebb, fordulatosabb legyen, sok információt kell eltárolnunk. Ilyen információ, hogy milyen eszközöket viszünk magunkkal kezdetben, vagy miket találtunk eddig az út során. Ezt tárolja a player carries nevű mező az állapotban. Vannak olyan események, melyeket nem lehet egyszerűen azzal leírni, hogy eltesszük, mint egy tárgyat. Például, hogy jártunk egy könyvtárban, ahol egy könyvben olvastunk kalandunk hátteréről. Itt kiderülhet egy nagyon fontos részlet. Ha majd sikeresen elérkezünk kalandunk végkimeneteléhez, akkor például egy adott folyosón semmiképpen se jobbra, hanem balra menjünk, mert jobbra egy csapda vár ránk. Ezt fel kell jegyeznünk magunknak. Ehhez hasonló információkat tárol a player flags. Ebben a kokrét kalandban, amit feldolgoztam, az volt a feladat, hogy összegyűjtsünk gyűrűket. Ezt számon kellett tartani, így szükség volt egy olyan adatszerkezetre, mely tárolja a gyűjtött tárgy nevét és hogy eddig mennyit találtunk belőle. Ez kezdetben 0, majd minden esetben, ahol egy újabb gyűrűt találtunk, csak a gyűrűk számát növeltem eggyel. Erre való a player counters nevű mező. Játékos „létrehozásánál” az első lépés, hogy meghatározzuk az életerő, ügyességi és szerencse pontjait. (A papíros játékban is ezzel kezdődött a kaland és itt a programban is ez az első lépés, amikor elkezdünk játszani.) A játék szabálya azt mondja, hogy ezen értékeket folyamatosan befolyásolja a kalandok sokasága, de nem csökkenhet 0 alá (mert az azt jelenti, hogy a kalador elbukta küldetését), és nem nőheti túl a kezdeti értéket. Ezért az aktuális értékek eltárolása mellett szükségünk van arra is, hogy kezdetben mennyi volt ez az érték. Ezért van player stats mező, mely minden tulajdonsághoz tárolja az aktuális és kezdeti értékeket. További fontos információ, melyet lépésről lépésre változtatunk, lekérdezünk, hogy hányadik oldalon tartunk éppen a küldetésünkben. Erre szolgál a player page. Ezek alapján kaptam tehát a játékos állapotát: 20
data PlayerState = PS { player_carries :: Set Item , player_flags :: Set Flag , player_counters :: Map Counter Int , player_stats :: Map Stat (Int, Int) , player_page :: PageNum } deriving (Show, Read)
2.2.4.
Harc/csata állapota
Egy csata folyamán előfordulhat, hogy csak egy lénnyel kell megküzdenünk, de lehet az is, hogy többel. Ezért itt el kell tárolnunk az összes ilyen mesebeli szörnyet, akik arra várnak, hogy megtámadhassanak. A szabályok szerint egyszerre csak lénnyel kell megküzdenünk, tehát egymás után következnek. A szörnyek tárolása a fight enemies listában történik. Az implementáció miatt volt szükség feljegyeznem, hogy mi volt az előző forduló, ezt a fight last round teszi meg. A könyv formája olyan, hogy az oldal elején leírja a történetet, itt találkozunk egy (vagy több) ellenféllel, majd az oldal alján van a megjegyzés, hogy ha győztes kimenetelű számunkra csata, akkor merre menjünk, ellenkező esetben vége a játékunknak. A programom írásánál úgy gondoltam, hogy nem szeretném, ha a kimenetben rögtön megjelenne, hogy merre van a továbbvezető út, mert ekkor még nem releváns számunkra. Úgy gondoltam, hogy akkor szeretném ezt megjeleníteni, ha már legyőztük ellenségeinket, különben egyáltalán ne jelenjen meg. Így a fight cont nevű mezőben tárolom a csata utáni folytatást, ami az oldal folyatását reprezentálja. data FightState = FS { fight_enemies :: [Enemy] , fight_last_round :: Maybe FightRound , fight_cont :: [PageItem] } deriving (Show, Read)
2.2.5.
Monádok a háttérben
Egy CyoaT nevű monád van a háttérben, mely egy monád-interfészt húz az állapot kezelése fölé. Ez minden olyan műveletet nyújt, amivel meg lehet változtatni az állapotot. Tehát a CyoaT monádom egy monádtranszformátor, melynek külseje egy Error monád, a belseje pedig egy RWST monádtranszformátor, ami az IO monád fölött van. A Reader monádban tárolom magukat az oldalakat, és amikor elérkezünk egy új oldalra, akkor csak le 21
kell kérdezni az aktuálisat. A Writer monádba kerül a kimenet, amely egy további algebrai adattípus. A State monád pedig egy játék állapotot ír le. Az IO monádra a véletlenszám generálásakor van szükség. A kockadobásnál generálunk egy számot 1 és 6 között. Az Error monádot a kaland elbukásának kezelésére használom, mert az bármikor megtörténhet, hogy egy oldal kiértékelése közben meghal a kalandorunk akár csatában, akár azért mert olyan oldalra került, ahonnan nincs tovább. newtype CyoaT m a = CyoaT { unCyoaT :: ErrorT GameEvent (RWST (Array PageNum Page, [PageItem]) Output GameState m) a } deriving (Monad, Functor, MonadIO, MonadError GameEvent, MonadState GameState, MonadReader (Array PageNum Page, [PageItem]), MonadWriter Output, Applicative)
2.2.6.
A monád műveletei
modifyPlayerState :: (Monad m) => (PlayerState -> PlayerState) -> CyoaT m () modifyPlayerState f = modify $ \gs -> gs{ player_state = f (player_state gs) } Tehát a modifyPlayerState módosítja a játék állapotát. A paraméterül kapott PlayerState -> PlayerState függvény mondja meg, hogy mi a módosítás. modifyFightState :: (Monad m) => (FightState -> FightState) -> CyoaT m () modifyFightState f = modify $ \gs -> gs{ fight_state = f' (fight_state gs) } where f' = Just . f. fromJust Hasonlóan az előzőhöz a csata állapotát is tudnunk kell módosítani, hogy éppen mi történik. Ki támad kit, kinek sikerült csapást bevinnie. takeItem :: (Monad m) => Item -> CyoaT m () takeItem item = modifyPlayerState $ \ps -> ps { player_carries = Set.insert item (player_carries ps) } Tágy felvételénél a játékos állapotában tároljuk, hogy eddig mikre tett szert. Ha egy új tárgyat talál, akkor azt megjegyezzük, módosítva az állapotunkat azzal, hogy egy új elem bekerült a halmazunkba. 22
dropItem :: (Monad m) => Item -> CyoaT m () dropItem item = modifyPlayerState $ \ps -> ps { player_carries = Set.delete item (player_carries ps) } Akkor is a játékos állapota változik, ha eldob, vagy felhasznál egy tárgyat. carries :: (Monad m) => Item -> CyoaT m Bool carries item = gets $ Set.member item . player_carries . player_state Szükségünk lehet lekédezni, hogy egy adott elemmel rendelkezünk-e. Ennek segítségére van ez a carries függvény. flagSet :: (Monad m) => Flag -> CyoaT m Bool flagSet flag = gets $ Set.member flag . player_flags . player_state setFlag :: (Monad m) => Flag -> CyoaT m () setFlag flag = modifyPlayerState $ \ps -> ps { player_flags = Set.insert flag (player_flags ps) } resetFlag :: (Monad m) => Flag -> CyoaT m () resetFlag flag = modifyPlayerState $ \ps -> ps { player_flags = Set.delete flag (player_flags ps) } Egy különleges helyzet megjegyzésére szolgál. (Amely nem egy tárgyfelvétel hanem, hogy például jártunk-e egy adott helyen vagy megettünk-e egy ételt, amit kalandunk közben találtunk.) Ezt az információt meg tudjuk jegyezni, le tudjuk kérdezni és lehet, hogy egy olyan információról van szó, melyet fel tudunk használni. Ezen információk leírására készültek ezek a függvények. getCounter :: (Monad m) => Counter -> CyoaT m Int getCounter counter = do lookup <- gets $ Map.lookup counter . player_counters . player_state return $ 0 `fromMaybe` lookup modifyCounter :: (Monad m) => (Int -> Int) -> Counter -> CyoaT m () modifyCounter f counter = do modifyPlayerState $ \ps -> ps { player_counters = Map.alter f' counter (player_counters ps) } where f' mx = Just $ f $ 0 `fromMaybe` mx 23
Ebben a könyvben, melyet feldolgoztam gyűrűket kell gyűjteni. Amikor találunk egy újabbat, akkor változik a játékos állapota úgy, hogy fel kell jegyeznünk, hogy még egy ilyen gyűjtött tárgyat találtunk. A modifyCounter vátoztatja meg az állatpotot. A paraméterül kapott Int -> Int függvény mondja meg, hogy mennyi az új érték a régi függvényében, ezután ennyi darab lesz a tarsolyunkban. A getCounter pedig lekérdezi az állapotból. getStat :: (Monad m) => Stat -> CyoaT m Int getStat a = gets $ fst . fromJust . Map.lookup a . player_stats . player_state A getStat függvénnyel a paraméterül kapott jellemzőt (életerő, ügyesség, szerencse) kérdezhetjük le. modifyStat :: (Monad m) => (Int -> Int) -> Stat -> CyoaT m () modifyStat f stat = do modifyPlayerState $ \ps -> ps { player_stats = Map.alter (Just . f' . fromJust) stat (player_stats ps) } when (stat == Health) $ do health <- getStat Health when (health == 0) $ do emit [outText "Életerő pontjaid elfogytak, kalandod itt véget ér."] die where f' (current, initial) = (new, initial) where new = clamp (0, initial) (f current) clamp (low, high) x | x < low = low | x > high = high | otherwise = x Hasonló, mint az előbb, ez is a játékos jellemzőivel foglalkozik. Míg ott lekérdeztük, itt módosítjuk a jellemzőket. A kaland során találkozhatunk olyannal, hogy megsérülünk, ekkor az életerőnk csökken. Ennek módosítása ezzel a függvénnyel történik. A játékszabályokban az is benne van, hogy ezek a pontok nem csökkenhetnek 0 alá. Ilyen esetben a kimenetbe elküldjük, hogy „Életerő pontjaid elfogytak, kalandod itt véget ér.”. Azonban a kezdeti érték fölé sem mehetnek gyógyulás alkalmával. Erre is fel van készítve ez a függvény. getDice :: (Monad m) => Die -> CyoaT m Int getDice d = gets (fromJust . Map.lookup d . page_state)
24
addDice :: (Monad m) => Die -> Int -> CyoaT m () addDice d value = modify $ \gs -> gs{ page_state = Map.insert d value (page_state gs) }
clearDice :: (Monad m) => CyoaT m () clearDice = modify $ \gs -> gs{ page_state = Map.empty } Ezek a függvények a kockadobás miatti állapotváltozásokat, állapotlekérdezéseket kezelik. A getDice művelettel egy konkrét kockának az értékét kérdezzük le. Az addDice egy új kockát definiál egy konkrét étrékkel. A clearDice kitörli az eddigi kockadobásokat. Engine.hs Ebben a modulban kifejezéseket, feltételeket értékelünk ki, és előállítjuk azt a kimenetet, amelyet majd a későbbiekben fel lehet dolgozni. Maga a kimenet egy algebrai adattípus, amelyben azt fogalmaztam meg, hogy mikre kell felkészülnünk kimenet készítésekor. data Output = OutputClear String [OutputItem] | OutputContinue [OutputItem] data OutputAttr = Good | Bad data OutputItem = | | | | |
OutText (Maybe OutputAttr) String OutDie Int OutLink Link String OutEnemies [Enemy] OutBreak OutImage ImageId
Az Output algebrai adattípus mutatja, hogy milyen kimeneteink lehetnek. Az egyik az OutputClear, ami azt jelenti, hogy egy új oldalra értünk, tehát az előzőt törölni kell és az újat kiírni. Az OutputContinue megkapja a hátralévő oldalt. Ezt csatánál használom. Csata esetén nem egy új oldalt kezdünk, hanem mindig csak hozzáírunk az előzőhöz, úgy hogy például közben felhasznaló interakció is történik (lásd kockadobás vagy annak eldöntése, hogy szeretnénk-e próbára tenni a szerencsénket).
25
Az OutputItem-ben írtam le, hogy miket akarok közölni a kimenettel kapcsolatban. Az OutText egy szöveg kiírására szolgál. Van egy Maybe OutputAttr attribútuma, ami annyit jelent, hogy jó vagy rossz dologot szeretnék közölni a játékos szempontjából. Rossz, ha őt sebezték, jó, ha ő sebzett egy csatában. Ez az attribútum opcionális. Az OutDie segítségével írok ki a kimenetre egy kockadobást. Az Int tartalmazza az értéket. Az OutLink segítségével teszem ki az oldalra azokat a linkeket, amellyel a továbbhaladást akarom közölni, vagy azt, hogy éppen egy csatához értünk, vagy a csatát folytatjuk. Az OutEnemies mondja meg, hogy milyen ellenfelekkel kell megküzdenie a hősnek, tehát kiírjuk az ellenfelek tulajdonságait. A paramétere egy lista, amely az ellenfeleket tartalmazza, mert lehet egy vagy több ellenfél is egyszerre. Az OutBreak sortörésre közlésére szolgál. Az OutImage segítségével lehet megmondani, hogy ennek az oldalnak van image attribútuma, így majd változtatni kell a képen. Alapértelmezetten a hősünket látjuk, de amint egy csatába belekerülünk, akkor az adott szörny képe jelenik meg. Miután megnéztük mik a lehetséges kimenetek, áttérhetünk a kiértékelő és kimenet előállító függvénykre. evalExpr :: (Monad m, Functor m) => Expr -> CyoaT m Int evalExpr (ELiteral n) = return n evalExpr (e :+: f) = (+) <$> evalExpr e <*> evalExpr f evalExpr (e :-: f) = (-) <$> evalExpr e <*> evalExpr f evalExpr (e :*: f) = (*) <$> evalExpr e <*> evalExpr f evalExpr (e :\%: f) = mod <$> evalExpr e <*> evalExpr f evalExpr (DieRef die) = getDice die evalExpr (CounterRef counter) = getCounter counter evalExpr (StatQuery a) = getStat a evalExpr (ECond cond thn els) = do b <- evalCond cond evalExpr (if b then thn else els) Kifejezések kiértékelésére van az evalExpr függvény. Kifejezéseink: +, -, *, lekédezése, jellemző lekérdezése, if. evalCond :: (Monad m, Functor m) => Cond -> CyoaT m Bool evalCond (CLiteral b) = return b evalCond (c :||: d) = (||) <$> evalCond c <*> evalCond d 26
evalCond evalCond evalCond evalCond evalCond evalCond evalCond evalCond evalCond
(c :&&: d) = (&&) <$> evalCond (e :<: f) = (<) <$> evalExpr e (e :<=: f) = (<=) <$> evalExpr (e :>: f) = (>) <$> evalExpr e (e :>=: f) = (>=) <$> evalExpr (e :==: f) = (==) <$> evalExpr (CNot c) = not <$> evalCond c (Carry item) = carries item (FlagSet flag) = flagSet flag
c <*> evalCond <*> evalExpr f e <*> evalExpr <*> evalExpr f e <*> evalExpr e <*> evalExpr
d f f f
Feltételek kiértékelésére van az evalCond függvény. Feltételek: bool literál, és, vagy, kisebb, kisebb vagy egyenlő, nagyobb, nagyobb vagy egyenlő, egyenlő-e, van-e nálunk egy bizonyos tárgy, megcsináltunk-e egy bizonyos dolgot. evalPage :: (Functor m, MonadIO m) => CyoaT m () evalPage = do fightState <- gets fight_state case fightState of Nothing -> do pageNum <- gets $ player_page . player_state (Page _ image pageType is) <- asks $ (!pageNum) . fst tell $ OutputClear (show pageNum ++ ".") [] case image of Just image -> emit [OutImage image] Nothing -> return () evalPageItems is case pageType of WinPage -> throwError WinEvent DeathPage -> throwError DeathEvent NormalPage -> return () Just fs -> do fight Egy oldal kiértékelésénél az az első kérdés, hogy csatában vagyunk-e, mert ettől függnek a továbbiak. A csatánál azt szerettem volna, ha az egész egy nagy oldal lenne és folytatólagosan írná ki a program, hogy mi történik. Hagyományos oldal esetén pedig új oldalt kezdünk. Ezért kell szétválasztani. Tehát csata esetén a fight függvénnyel folytatódik, viszont új oldal esetén le kell kérdeznünk az oldal számát. Ezt el is küldjük a kimenetre. Majd megnézzük, hogy van-e új kép az adott oldalhoz. Ha van, akkor azt is tudatjuk a 27
kimeneten. Majd kiértékeljük az oldal belsejét. Az oldalnak olyan tulajdonsága is lehet, hogy ezen az oldalon a hős elbukik, vagy megnyeri. Ezekben a különleges helyzetekben egy kivételt váltunk ki. evalPageItem :: (Functor m, MonadIO m) => PageItem -> CyoaT m () evalPageItem (Paragraph is) = do mapM_ evalPageItem is emit [OutBreak] Egy bekezdés kiértékelése úgy történik, hogy benne levő oldalrészleteket rekurzívan egymás után kiértékeljük, és kiírjuk a kimenetre, és ha elértünk a bekezdés végére, akkor következik egy sortörés. evalPageItem (TextLit s) = emit [outText s] Szöveg kiértékelésénél csak a szöveget kitesszük a kimenetre. evalPageItem (If c thn els) = do b <- evalCond c mapM_ evalPageItem (if b then thn else els) Elágazás esetén előbb kiértékeljük a feltételt. Amennyiben igaz, akkor a then ágra megyünk, ellenkező esetben az else ágra. evalPageItem (Goto capitalize pageNum) = do emit [OutLink (PageLink pageNum) $ unwords [if capitalize then "Lapozz" else "lapozz", if the then "a" else "az", show pageNum ++ ".", "oldalra"]] where the | pageNum `elem` [1, 5] = False | pageNum `elem` [2, 3, 4, 6, 7, 8, 9] = True | pageNum < 50 = True | pageNum < 60 = False | otherwise = True Ennek a kiértékelésnek a hatására a kimenetbe kerül, hogy melyik oldalon folytatódik a történet, hova lapozhat az olvasó. evalPageItem (GotoLucky refYes refNo) = do emit [outText "Tedd próbára SZERENCSÉDET!"] d1 <- roll d2 <- roll luck <- getStat Luck 28
let page' | d1 + d2 <= luck = refYes | otherwise = refNo modifyStat pred Luck evalPageItem (Goto True page') A Tedd próbára a szerencsédet utasítás. Először kiírjuk a kimenetre, hogy ”Tedd próbára SZERENCSÉDET!”. Majd két kockadobás következik (a játék szabályai alapján). Összehasonlítjuk a szerencse pontkainkkal. Ha kockadobások összege kisebb vagy egyenlő, mint a szerencse pontjaink, akkor szerencsések vagyunk, különben nem. Majd a szerencsepontjainkból levonunk egyet. Végül kiírjuk, hogy honnan folytatódik a történet. evalPageItem (Inc counter) = modifyCounter succ counter evalPageItem (Dec counter) = modifyCounter pred counter evalPageItem (Clear counter) = modifyCounter (const 0) counter evalPageItem (Take item) = takeItem item evalPageItem (Drop item) = dropItem item evalPageItem (Damage stat expr) = do value <- evalExpr expr modifyStat (\x -> x - value) stat evalPageItem (Heal stat expr) = do value <- evalExpr expr modifyStat (+value) stat evalPageItem (Set flag) = setFlag flag Ezek az oldal részletek a monád állapotát változtatják. evalPageItem (DieDef name) = do n <- roll addDice name n A kockadobás kiértékelésénél előbb dobunk, mellyel kapunk egy véletlen számot. Majd a monád állapotába elmentjük ezt az értéket. evalPageItem (Fight enemies) = do is <- asks snd let fs = FS { fight_enemies = enemies, fight_last_round = Nothing, fight_cont = is } modify $ \gs -> gs{ fight_state = Just fs } emit [OutLink StartFightLink "Harcolj!", OutBreak] throwError FightEvent 29
Csata kezdetén egy új csata állapotot hozunk létre és állítunk be a játék állapotában, ahol az ellenségeket most kaptuk meg, és a folytatás pedig a csata utáni rész. Ezt azért csináltam így, mert amikor egy ellenséggel találkozik a hősünk, akkor nem akarom, hogy tudja, hogy az adott oldal hogyan folytatódik, mert például ne tudjon harc nélkül a következő oldalra menni. Csak akkor jelennek meg az oldal további részei, amikor már legyőzte a szörnyet, vagy szörnyeket. Így a folytatást félretesszük az állatpotba és majd akkor folytatjuk, ha a harc véget ért pozitív kimenettel a hős számára.
2.2.7.
Megjelenítés
GTK A Gtk2hs nevű csomag segítségével készítettem ezt a grafikus felhasználói felületet. Majdnem minden függvény meg van írva Haskell nyelven, ami a GTK-ban szerepel. Így csak ezeket a functionális nyelven megírt függvényeket kellett ugyanúgy használnom, mintha GTK-t használnék. Pl: • a main függvény ellenőrzi le, hogy a parancssori paramétereket adtunke a programnak (az xml fájlt) pages <- case args of [filename] -> parsePages filename otherwise -> do self <- getProgName hPutStrLn stderr $ unwords ["Usage:", self, "
30
• egy görgetősávot tettem függőlegesen (harcoknál vagy hosszabb szövegeknél szükség van rá), scrollwin <- scrolledWindowNew Nothing Nothing scrolledWindowSetPolicy scrollwin PolicyNever PolicyAutomatic containerAdd scrollwin tview • alulra került egy állapotjelző (a játékos jellemzői ide kerülnek, mely egy kép és mellette az érteke). statusbar <- statusbarNew let statusLabel icon = do hbox <- hBoxNew False 0 path <- getDataFileName icon containerAdd hbox =<< imageNewFromFile path label <- labelNew Nothing containerAdd hbox label boxPackStart statusbar hbox PackNatural 0 return label labelHealth <- statusLabel "heart_icon.png" labelAgility <- statusLabel "sword_icon.png" labelLuck <- statusLabel "luck_icon.png" A display függvény dolgozza fel azt a kimenetet, amelyet a motorban előállítottam. Yesod A Yesod nevű csomag segítégével írtam meg a webes felületet. Mivel webes környezetben nincs állapot, ebben a programban pedig számon tartunk néhány dolgot, melyet állapotként fogalmaztunk meg, ezért sütik kezelésével oldottam ezt meg. A sütiben a játék állapotát adom oldalról oldalra. A routing táblában 3 út van. Az első a root, a második a start (melynek segítségével bármikor új játékot lehet kezdeni, mert ez egy új, üres állapotot állít elő), a harmadik a goto amelynek egy oldalszám a paramétere, hogy hanyadik oldalon tartunk vagy csata esetén a folyatást tartalmazza. A root út arra szolgál, hogy elrejti azt az információt, hogy melyik oldalon vagyunk, tehát, tényleg csak a sütiben van információ az állapotról. Minden új oldalra kerülés után átirányítjuk a játékost a root-ra, melynek nincs oldalszám paramétere. Különben könnyen csalni lehetne, hogy melyik oldalra menjunk, mert csak beírnánk az oldalszámot. 31
mkYesod "CyoaWeb" [$parseRoutes| / PRoot GET /start PStart GET /goto/#Link PGoto GET |] Az addCassius függvénnyel tudok css-t hozzáadni, melyben például a linkek színét adtam meg. A hamletToRepHtml függvény a html leírására szolgál. A toHamlet függvénnyel állítottam elő azt a html kódot, amely megmondja, hogy mit kell tenni a motor által előállított kimenettel. Sőt javascriptre is szükségem volt, hogy mikor a játékos a csatában választott már, hogy probára teszi-e a szerencséjét, akkor később ezek a linkek már ne legyenek elérhetőek. Csak egyszer engedjük meg neki, hogy válasszon. Ezért aztán javascript segítségével eltüntetem az onClick() eseményt.
2.2.8.
Továbbfejlesztési lehetőségek
• A programot tovább lehet színesíteni, csinosítani. A webes felületre is ki lehetne írni azokat az információkat, mint a grafikus felhasználói felületre. • Ki lehetne egészíteni, hogy a játékos megválaszthassa, hogy milyen módban szeretne játszani. Lehetne egy könnyű mód, ami azt jelenti, hogy ha a szerencsére van bízva a kalandjának folytatása, akkor mindig a legkedvezőbb út jöjjön ki továbbhaladási lehetőségként. Esetleg a csatában is nagy százalékban a játékos sebezzen az ellenfelén. Lehetne egy normál mód, amikor a szerencsét nem tudjuk befolyásolni, de lehessen visszaféle is menni. Ha olyan helyre értünk, ami számunkra kedvezőtlen, akkor legyen egy vissza gomb, melynek megnyomásával oda kerülnénk vissza, ahonnan ide jutottunk. A nehéz mód az a mostani maradhatna. • A bemenő fájlra lehetne egy ellernőrző programot írni, hogy a kezdeti oldalról valóban el lehet jutni minden más oldalra. Egy gráfként lehetne ábrázolni és azt vizsgálni, hogy az 1. oldalról, azaz a gyökérből vezete út minden más oldalra. Továbbá, hogy létezik-e nyerő út, azaz el lehet-e jutni a 400. oldalra a gyökérből. • Készíthetnénk belőle egy olyan játékot, mely elrugaszkodik a könyvolvasás élményétől, kicsit modernebb lenne.
32