4.2. Rakstām spēli (turpinājums)

Ievades apstrāde

Iepriekšējā sadaļā mēs esam izdarījuši ļoti daudz darba - mēs izveidojām funkciju, kas apkalpo galveno spēles ciklu, izstrādājām likmes, izveidojām palīgfunkcijas likmju apstrādei. Palika pavisam maz - izveidot funkcijas, kas uzsāk spēli, apstrādā likmi, ģenerē un apstrādā rezultātu. Kad mēs to pabeigsim, mums būs strādājoša spēle, kuru mēs nedaudz sakārtosim.

Sāksim ar funkciju, kas apstrādā lietotāja ievadi. Tai ir jāparedz visas darbības, ko lietotājs var izdarīt, un jāparedz kļūdas. Iepriekšējā sadaļā mēs šo funkciju nosaucām par processInput un noteicām, ka tai ir trīs argumenti - likmes izmērs, ievadīta rinda, un naudas daudzums. Funkcijas nosaukums ar argumentiem izskatīsies šādi:

processInput betSize bet money

Visas darbības apstrādāsim ar guard (|) simboliem. Pirmā iespējama darbība būs iziešana no spēles. Mēs izveidosim atsevišķu funkciju pārbaudei, jo mēs to varēsim izmantot citur:

quit s = any (`isPrefixOf` s) [":q", "q", "exit"]

Pieliksim šo pārbaudi processInput funkcijā:

    | quit input = end "quit."

Lai nomainītu likmi, mēs izmantosim atslēgvārdu "change". Ja lietotājs ievadīs "change 3", viņa likme tiks samainīta uz 3 un paliks tāda, kamēr viņš nenomainīs likmi vēlreiz, vai kamēr viņa nauda nebūs mazāk par 3. Pieliekam pārbaudi:

    | isPrefixOf "change" input = changeValue

Funkciju changeValue izveidosim zem where atslēgvārda, jo mēs neplānosim to izmantot kaut kur citur:

    where
        changeValue
            | validValue = game newValNum money
            | otherwise  = wrongInput
            where
                newVal     = dropWhile (== ' ') $ dropWhile (/= ' ') input
                newValNum  = read newVal :: Int
                validValue = isNumber newVal && newValNum >= 1 && newValNum <= money

Konstrukcija dropWhile (== ' ') $ dropWhile (/= ' ') input paņem rindu, piemēram "change 3", izdzēš visus simbolus līdz atstarpei un tad noņem visas atstarpes. Paliks tikai 3. Pēc tam, mēs pārbaudām, vai tas, kas palika, ir skaitlis, vai tas skaitlis ir lielāks par nulli un ir mazāk vai vienāds ar naudas summu. Ja viss ir kārtībā, tad mēs izpildām galveno spēles funkciju ar jauno likmes izmēru. Ja ir notikusi kļūda, tad izpildām funkciju wrongInput, kuru aprakstīsim turpat zem where:

        wrongInput = do
            putStrLn "Wrong input!"
            game betSize money

Nākošais noteikums mūsu processInput funkcijā izpildīsies, ja ir ievadīta pareiza likme. Mums vajag izveidot divas papildfunkcijas zem tā paša where, kuras pārbaudīs, vai likmes ir pareizas. Pie tam, mums ir svarīgi atdalīt šīs funkcijas, jo no likmes ir atkarīgs uzvaras vai zaudējuma koeficients. Funkcijas izskatīsies šādi:

        simpleBet = inList input bets
        numberBet = checkNumbers input && checkList input numberBets

Mēs izmantojam palīgfunkcijas no iepriekšajas sadaļas. Ko dara šīs divas funkcijas saprast nav grūti. Funkcija simpleBet atgriež true, ja likme ir starp definētām likmēm. Savukārt, numbersBet pārbauda, vai ievads atbilst formātam "skaitļi sadalīti ar komatu", ka skaitļu skaits ir pareizs un visi skaitļi ir starp 0 un 36. Ja viena no pārbaudēm tiks izieta, mums vajadzēs izsaukt funkciju, kas ģenerēs skaitli un apstrādās rezultātu. Sākumā mums vajag izdomāt, kādi argumenti būs vajadzīgi šai funkcijai.

Pirmais arguments var būt likmes izmērs - tas ir vajadzīgs uzvaras vai zaudējuma aprēķinos. Otrais arguments ir mūsu nauda. Kā trešo argumentu izmantosim sarakstu ar skaitļiem, kuri uzvar ar esošo likmi. Definēto likmju gadījumā šis saraksts ir bet saraksta pāru otrais elements, bet skaitļu likmju gadījumā tie ir paši skaitļi. Noteiksim to zem where:

       winners
            | simpleBet = tripleLookup input bets snd3 []
            | numberBet = toNumbers input

Pievērsiet uzmanību, kā mēs padodam snd3 funkciju, lai iegūtu otro pāra elementu.

Vēl mums vajag padot likmes koeficientu, kurš definētām likmēm ir bets sarakstā pāru trešais elements, savukārt, skaitļu likmēm tas vienmēr ir 36. Noteiksim to turpat zem where:

        quotient
            | simpleBet = tripleLookup input bets thd3 0
            | otherwise = 36

Un pēdējais argiments ir reizinātājs, ar kuru mēs zināsim, cik daudz ir uzlikts, tas ir, cik ir jāatņem no spēlētāja naudas pirms rēķināt rezultātu. Definētam likmēm tas vienmēr ir viens, bet skaitļu likmēm tas ir tik daudz, uz cik skaitļiem ir uzlikts:

        multiplier
            | simpleBet = 1
            | otherwise = length $ toNumbers input

Tagad mēs varam pabeigt mūsu processInput funkciju. Tā sanāks diezgan liela, tāpēc mēs izmantosim dažas atkāpes, lai uzlabotu lasāmību. Pie reizes izveidosim dažus mainīgus izteiksmēm, kas atkārtojas, piemēram toNumbers input. Kods sanāks šāds:

processInput betSize input money
    | quit input             = end "quit."
    | change                 = changeValue
    | simpleBet || numberBet = generateNumber betSize money winners quotient multiplier
    | otherwise              = wrongInput
    where
        change    = isPrefixOf "change" input

        changeValue
            | validValue = game newValNum money
            | otherwise  = wrongInput
            where
                newVal     = dropWhile (== ' ') $ dropWhile (/= ' ') input
                newValNum  = read newVal :: Int
                validValue = isNumber newVal && newValNum >= 1 && newValNum <= money

        wrongInput = do
            putStrLn "Wrong input!"
            game betSize money

        simpleBet = inList input bets
        numberBet = checkNumbers input && checkList input numberBets

        numbers  = toNumbers input
        winners
            | simpleBet = tripleLookup input bets snd3 []
            | numberBet = numbers

        quotient
            | simpleBet = tripleLookup input bets thd3 0
            | otherwise = 36

        multiplier
            | simpleBet = 1
            | otherwise = length numbers

Skaitļa ģenerācija un rezultāta apstrāde

Tagad mums vajag uzrakstīt funkciju, kas ģenerē skaitli un apstrādā rezultātu. Mēs jau zinām, kā funkcija sauksies un kādi tai būs argumenti. Pāriesim pie paša skaitļa ģenerēšanas. Lai saģenerētu gadījuma skaitli, Haskell valodā ir vairākas iespējas, bet mēs izmantosim funkciju randomIO no bibliotēkas System.Random. Tā funkcija ģenerē skaitli, kuru ierobežo tipa intervāls un kā sākotnēja vērtība tiek izmantots globālais sistēmas ģenerators.

randomIO :: Random a => IO a

Kā redzat, rezultāts ir IO kontekstā. Mēs gribām dabūt skaitli, kas nav lielāks par 36, tāpēc mums vajag atrast atlikumu no saģenerēta skaitļa sadalīta ar 37. Izmantosim funkciju mod, un tā, ka skaitlis ir IO konteksta, mums vajag izmantot fmap:

generateNumber betSize money winners quotient multiplier = do
    gen <- fmap (`mod` 37) randomIO
    putStr $ "Number won: " ++ (show gen) ++ " "

Vēl attēlosim nedaudz informācijas par skaitli - krāsa un paritāte:

    if gen == 0
        then putStrLn ""
        else do
            if elem gen reds then putStr "red" else putStr "black"
            if odd gen then putStrLn " odd" else putStrLn " even"

Tagad atlicis tikai noteikt, vai spēlētājs ir uzvarējis, un aprēķināt rezultātu. Tās darbības ir vienkāršās, tāpēc mēs uzreiz apskatīsimies visu funkciju:

generateNumber betSize money winners quotient multiplier = do
    gen <- fmap (`mod` 37) randomIO
    putStr $ "Number won: " ++ (show gen) ++ " "

    if gen == 0
        then putStrLn ""
        else do
            if elem gen reds then putStr "red" else putStr "black"
            if odd gen then putStrLn " odd" else putStrLn " even"

    if elem gen winners
        then do
            let w = (quotient - multiplier) * betSize
            putStrLn $ "You won " ++ (show w)
            game betSize $ money + w
        else do
            let l = betSize * multiplier
            putStrLn "You lost "
            game betSize $ money - l

Mēs redzam kaut ko, ko mēs esam izmantojuši GHCi, bet neesam izmantojuši kodā - let atslēgvārdu. Atcerēsimies, ko tas dara - tas ļauj veidot definīcijas izteiksmēs. Var saprast, ka katra rinda do blokā ir izteiksme, kurai ir noteikts rezultāts, tāpēc tajās nevar definēt funkcijas vai datus, bez let izteiksmes.


Spēles kods

Mums vajag vēl izveidot funkciju, kas uzsāk spēli, pieprasa no spēlētāja ievadīt naudas summu un izsauc game funkciju, vai kļūdas gadījumā attēlos paziņojumu un izsauks pati sevi vēlreiz. Funkcija sauksies gameStart:

-- Main cycle
gameStart = do
    putStrLn "Enter amount"
    m <- getLine
    if quit m
        then putStrLn "Bye!"
        else do
            if isNumber m
                then game 1 $ read m
                else do
                    putStrLn "Wrong input!"
                    gameStart

Izdarīsim vēl vienu sīkumu, lai mūsu spēle būtu nedaudz lietojamāka - pieliksim krāsas dažiem paziņojumiem. Lai konsolē attēlotu simbolus ar krāsu, tos vajag ieslēgt speciālos simbolos, piemēram, kods putStrLn $ "\x1b[31m" ++ "You lost " ++ "\x1b[0m" attēlos rindu You lost sarkanajā krāsā. Uztaisīsim vienu funkciju, kas attēlos krāsainas rindas, un vēl trīs papildfunkcijas sarkanai, zaļai un rozā krāsām:

colorLine color line = "\x1b[" ++ color ++ "m" ++ line ++ "\x1b[0m"

redLine   = colorLine "31"
greenLine = colorLine "32"
pinkLine  = colorLine "35"

Jautājums, vai var kaut kā vēl optimizēt, lai nebūtu jāatkārto funkcija colorLine trīs reizes. Var aizvietot tās trīs rindas ar vienu:

[redLine, greenLine, pinkLine] = map colorLine ["31", "32", "35"]

Pamēģiniet šo koda gabalu saprast paši.

Saliksim mūsu funkcijas spēlē. Funkcijā gameStart:

putStrLn $ redLine "Wrong input!"

Funkcijā game:

end $ redLine "You lost!"
...
putStrLn $ pinkLine $ "Money too low, bet value changed to " ++ (show money) ++ "!"
...
putStrLn $ pinkLine $ "Money: " ++ (show money) ++ ". Bet value: " ++ (show betSize)

Funkcijā processInput:

putStrLn $ redLine "Wrong input!"

Funkcijā generateNumber:

if elem gen reds then putStr (redLine "red") else putStr "black"
...
putStrLn $ greenLine $ "You won " ++ (show w)
...
putStrLn $ redLine "You lost"

Mūsu spēles kods ir gatavs. Varam to apskatīties pilnībā:

import Data.Char hiding (isNumber)
import Data.List
import Data.Tuple.Utils

import System.Random

main = gameStart

-- Main cycle
gameStart = do
    putStrLn "Enter amount"
    m <- getLine
    if quit m
        then putStrLn "Bye!"
        else do
            if isNumber m
                then game 1 $ read m
                else do
                    putStrLn $ redLine "Wrong input!"
                    gameStart

end s = putStrLn $ "Game ended: " ++ s

quit s = any (`isPrefixOf` s) [":q", "q", "exit"]

game betSize money
    | money <= 0      = end $ redLine "You lost!"
    | betSize > money = do
        putStrLn $ pinkLine $ "Money too low, bet value changed to " ++ (show money) ++ "!"
        game money money
    | otherwise       = do
        putStrLn $ pinkLine $ "Money: " ++ (show money) ++ ". Bet value: " ++ (show betSize)
        putStrLn "Enter your bet (to change amount of bet enter 'change <number>')"
        input <- getLine
        processInput betSize input money


processInput betSize input money
    | quit input             = end "quit."
    | change                 = changeValue
    | simpleBet || numberBet = generateNumber betSize money winners quotient multiplier
    | otherwise              = wrongInput
    where
        change    = isPrefixOf "change" input

        changeValue
            | validValue = game newValNum money
            | otherwise  = wrongInput
            where
                newVal     = dropWhile (== ' ') $ dropWhile (/= ' ') input
                newValNum  = read newVal :: Int
                validValue = isNumber newVal && newValNum >= 1 && newValNum <= money

        wrongInput = do
            putStrLn $ redLine "Wrong input!"
            game betSize money

        simpleBet = inList input bets
        numberBet = checkNumbers input && checkList input numberBets

        numbers  = toNumbers input
        winners
            | simpleBet = tripleLookup input bets snd3 []
            | numberBet = numbers

        quotient
            | simpleBet = tripleLookup input bets thd3 0
            | otherwise = 36

        multiplier
            | simpleBet = 1
            | otherwise = length numbers


generateNumber betSize money winners quotient multiplier = do
    gen <- fmap (`mod` 37) randomIO
    putStr $ "Number won: " ++ (show gen) ++ " "

    if gen == 0
        then putStrLn ""
        else do
            if elem gen reds then putStr (redLine "red") else putStr "black"
            if odd gen then putStrLn " odd" else putStrLn " even"

    if elem gen winners
        then do
            let w = (quotient - multiplier) * betSize
            putStrLn $ greenLine $ "You won " ++ (show w)
            game betSize $ money + w
        else do
            let l = betSize * multiplier
            putStrLn $ redLine "You lost"
            game betSize $ money - l


-- Helpers

-- Check if all symbols in string are digits
isNumber n = n /= "" && all isDigit n

-- Check if string is numbers separated by comma
checkNumbers = all isNumber . splitBy ','

-- Split string by comma and convert to Int
toNumbers = nub . map (\x -> read x :: Int) . splitBy ','

-- Split string by delimiter
splitBy delimiter = foldr checkDelimiter [[]]
    where
        checkDelimiter c l@(x:xs)
            | c == delimiter = [] : l
            | otherwise      = (c:x) : xs

-- Check if number count is within allowed lengths and all numbers are in [0..36]
checkList l lengths = (length parsedList) `elem` lengths && all (`elem` [0..36]) parsedList
    where parsedList = toNumbers l

-- Check if argument is index in list of triples
inList :: Eq a => a -> [(a, b, c)] -> Bool
inList index = any (\ x -> index == fst3 x)

-- Find index in list of triples and execute function on that triple
tripleLookup _ [] _ def = def
tripleLookup index (l:ls) fn def
    | fst3 l == index = fn l
    | otherwise       = tripleLookup index ls fn def

colorLine color line = "\x1b[" ++ color ++ "m" ++ line ++ "\x1b[0m"

[redLine, greenLine, pinkLine] = map colorLine ["31", "32", "35"]

-- Bets
reds   = [1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32,34,36]
blacks = [1..36] \\ reds

bets = [
        ("odd",    [1,3..35],  2),
        ("even",   [2, 4..36], 2),
        ("red",    reds,       2),
        ("black",  blacks,     2),
        ("1..18",  [1..18],    2),
        ("19..36", [19..36],   2),
        ("1..12",  [1..12],    3),
        ("13..24", [13..24],   3),
        ("25..36", [25..36],   3)
    ]

numberBets = [1,3,6]

Šo kodu var nokompilēt, un tas strādās pilnvērtīgi. Koda trūkums ir ka viss ir samests vienā failā. Nākamajā sadaļā mēs mēģināsim atrisināt šo problēmu.



>> 4.3. Sakārtojām spēles kodu