4.3. Sakārtojām spēles kodu

Moduļu veidošana

Iepriekšējā sadaļā mēs pabeidzām spēles koda rakstīšanu, bet tas palika nesakārtots. Šoreiz mēs iemācīsimies veidot savus moduļus un importēt tos. Ir pieņemts veidot direktorijas moduļiem un likt moduļus tajās. Ir definēti noteikumi moduļu veidošanai: moduļu nosaukumam vajag sākties ar lielo burtu, kā arī, ar lielu burtu vajag sākties arī visām mapēm.

Izveidosim mapi Roulette un tajā izveidosim failu Helpers.hs. Tajā ieliksim mūsu palīgfunkcijas:

module Roulette.Helpers where

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

-- 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"]

Savukārt, galvenā failā vajag pievienot rindu:

import Roulette.Helpers

Vēl pārnesīsim funkcijas game, processInput, generateNumber, end, quit un visus datus modulī Roulette.Game:

module Roulette.Game where

import Data.List
import Data.Tuple.Utils

import System.Random

import Roulette.Helpers

-- 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]

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

Pievērsiet uzmanību, ka neskatoties uz to, ka abi moduļi atrodas vienā mapē, modulis Roulette.Game importē moduli Roulette.Helpers, izmantojot pilno ceļu. Tas ir saistīts ar to, ka par atskaites punktu tiek izmantota direktorija, kurā atrodas galvenais fails.

Mūsu galvenā faila saturs pēc visām izmaiņām izskatīsies šādi:

import Roulette.Helpers
import Roulette.Game

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

Nedaudz par moduļu veidošanas iespējām - Jūs varat definēt funkcijas un datus, kas nav pieejami no ārpuses. Koncepts ir līdzīgs private īpašībām OOP valodās. Lai to īstenotu, mēs pēc moduļa nosaukuma (pirms atslēgvārda where), iekavās sarakstam funkcijas, kuras ir nepieciešams importēt:

module Foo.Mymodule
(foo, bar)
where

Dotajā piemērā, funkcijas foo un bar būs pieejamas no ārpuses, bet ja modulī ir kaut kādas citas funkcijas, tas ir pieejamas tikai no iekšienes.


Mēs esam pabeiguši rakstīt mūsu pirmo spēli Haskell valodā. Tagad Jūs varat papētīt šo kodu, paeksperimentēt ar to. Jūs varat novērtēt, ka veidot strādājošu kodu Haskell valodā ir interesanti un nav grūti. Šajā piemērā mēs izskatījām lietotāja ievadu, moduļu veidošanu un kā pareizi veidot funkcijas. Turpinājumā mēs pievērsīsim uzmanību tādām lietām, kā stāvoklis (ar State monādi), datubāzes, darbs ar tīklu, paralēla un konkurenta izpilde.