2012-06-18 20 views
7

Come pratica, sto provando a scrivere una simulazione per il gioco del casinò "guerra" in Haskell.Come rendere questo pezzo di codice Haskell più conciso?

http://en.wikipedia.org/wiki/Casino_war

Si tratta di un gioco molto semplice, con poche regole. Sarebbe un problema altrimenti molto semplice scrivere in una qualsiasi delle lingue imperative che conosco, tuttavia sto lottando per scriverlo in Haskell.

Il codice che ho finora:

-- Simulation for the Casino War 

import System.Random 
import Data.Map 

------------------------------------------------------------------------------- 
-- stolen from the internet 

fisherYatesStep :: RandomGen g => (Map Int a, g) -> (Int, a) -> (Map Int a, g) 
fisherYatesStep (m, gen) (i, x) = ((insert j x . insert i (m ! j)) m, gen') 
    where 
     (j, gen') = randomR (0, i) gen 

fisherYates :: RandomGen g => g -> [a] -> ([a], g) 
fisherYates gen [] = ([], gen) 
fisherYates gen l = toElems $ Prelude.foldl 
     fisherYatesStep (initial (head l) gen) (numerate (tail l)) 
    where 
     toElems (x, y) = (elems x, y) 
     numerate = zip [1..] 
     initial x gen = (singleton 0 x, gen) 

------------------------------------------------------------------------------- 

data State = Deal | Tie deriving Show 

-- state: game state 
-- # cards to deal 
-- # cards to burn 
-- cards on the table 
-- indices for tied players 
-- # players 
-- players winning 
-- dealer's winning 
type GameState = (State, Int, Int, [Int], [Int], Int, [Int], Int) 

gameRound :: GameState -> Int -> GameState 
gameRound (Deal, toDeal, toBurn, inPlay, tied, numPlayers, pWins, dWins) card 
    | toDeal > 0 = 
     -- not enough card, deal a card 
     (Deal, toDeal - 1, 0, card:inPlay, tied, numPlayers, pWins, dWins) 
    | toDeal == 0 = 
     -- enough cards in play now 
     -- here should detemine whether or not there is any ties on the table, 
     -- and go to the tie state 
     let 
      dealerCard = head inPlay 
      p = zipWith (+) pWins $ (tail inPlay) >>= 
       (\x -> if x < dealerCard then return (-1) else return 1) 
      d = if dealerCard == (maximum inPlay) then dWins + 1 else dWins - 1 
     in 
      (Deal, numPlayers + 1, 0, [], tied, numPlayers, p, d) 
gameRound (Tie, toDeal, toBurn, inPlay, tied, numPlayers, pWins, dWins) card 
    -- i have no idea how to write the logic for the tie state AKA the "war" state 
    | otherwise = (Tie, toDeal, toBurn, inPlay, tied, numPlayers, pWins, dWins) 

------------------------------------------------------------------------------- 

main = do 
    rand <- newStdGen 
    -- create the shuffled deck 
    (deck, _) <- return $ fisherYates rand $ [2 .. 14] >>= (replicate 6) 
    -- fold the state updating function over the deck 
    putStrLn $ show $ Prelude.foldl gameRound 
     (Deal, 7, 0, [], [], 6, [0 ..], 0) deck 

------------------------------------------------------------------------------- 

capisco perché lavoro extra deve andare verso la creazione di numeri casuali, ma sono abbastanza sicuro che mi manca un po 'di costrutto di base o concetto. Non dovrebbe essere così imbarazzante mantenere una serie di stati ed eseguire una logica di ramificazione su un elenco di input. Non riuscivo nemmeno a capire un buon modo di scrivere la logica per il caso in cui ci sono legami sul tavolo.

Non sto chiedendo soluzioni complete. Sarebbe davvero bello se qualcuno potesse indicare cosa sto facendo male, o qualche buon materiale di lettura che sia rilevante.

Grazie in anticipo.

+1

Si dovrebbe esaminare ['StateT'] (http://hackage.haskell.org/packages/archive/mtl/latest/doc/html/Control-Monad-State-Lazy.html#v:StateT) e ['RandT'] (http://hackage.haskell.org/packages/archive/MonadRandom/0.1.6/doc/html/Control-Monad-Random.html#t:RandT) trasformatori monad. –

risposta

6

Un modello di progettazione utile per il mantenimento dello stato dell'applicazione è la cosiddetta monade di stato. È possibile trovare una descrizione e alcuni esempi introduttivi here. Inoltre, si potrebbe voler considerare l'utilizzo di un tipo di dati con campi chiamati al posto di una tupla per GameState, ad esempio:

data GameState = GameState { state :: State, 
          toDeal :: Int 
          -- and so on 
          } 

questo modo sarà più facile l'accesso/aggiornamento campi individuali utilizzando record syntax.

+1

La sintassi dei record può anche rendere il tuo codice più facile da comprendere, poiché i campi possono avere nomi descrittivi invece di tuple che sono solo tipi, e una tupla di '(Int, Int, Int)' non è molto utile se non puoi ricorda quale 'Int' è per cosa. +1 anche per la monade statale, risparmia un sacco di tubature manuali. –

2

Mi è venuto in mente che la raccomandazione "usa StateT" potrebbe essere un po 'opaca quindi ho tradotto un po' in quel gergo, sperando che tu potessi vedere come andare da lì. Potrebbe essere meglio includere lo stato del mazzo nello stato di gioco. gameround sotto ripristina semplicemente la tua funzione nel gergo StateT. La precedente definizione, game utilizza il campo deck dello stato del gioco, continuamente ridotto e contiene l'intero gioco. Presento azioni IO, solo per mostrare come è fatto, e così puoi vedere la successione degli stati se chiami main in ghci. Si "sollevano" le azioni IO nella macchina StateT, per metterle a un livello con i get e le put. Si noti che nelle sottosequelle di mose, inseriamo il nuovo stato e quindi chiediamo che l'azione venga ripetuta, in modo che il blocco do contenga l'operazione ricorsiva completa. (Tie e un mazzo vuoto terminano immediatamente il gioco.) Quindi nell'ultima riga di main abbiamo runStateT su questo autoaggiornamento game che produce una funzione GameState -> IO (GameState,()); quindi alimentiamo questo con un certo stato iniziale compreso il mazzo determinato casualmente per ottenere l'azione IO che è l'attività principale. (Non seguo come il gioco dovrebbe funzionare, ma è stato meccanicamente in movimento le cose intorno per ottenere l'idea attraverso.)

import Control.Monad.Trans.State 
import Control.Monad.Trans 
import System.Random 
import Data.Map 

data Stage = Deal | Tie deriving Show 
data GameState = 
    GameState { stage  :: Stage 
       , toDeal  :: Int 
       , toBurn  :: Int 
       , inPlay  :: [Int] 
       , tied  :: [Int] 
       , numPlayers :: Int 
       , pWins  :: [Int] 
       , dWins  :: Int 
       , deck  :: [Int]} deriving Show 
       -- deck field is added for the `game` example 
type GameRound m a = StateT GameState m a 

main = do 
    rand <- newStdGen 
    let deck = fst $ fisherYates rand $ concatMap (replicate 6) [2 .. 14] 
    let startState = GameState Deal 7 0 [] [] 6 [0 ..100] 0 deck 
    runStateT game startState 

game :: GameRound IO() 
game = do 
    st <- get 
    lift $ putStrLn "Playing: " >> print st 
    case deck st of 
    []   -> lift $ print "no cards" 
    (card:cards) -> 
     case (toDeal st, stage st) of 
     (0, Deal) -> do put (first_case_update st card cards) 
         game -- <-- recursive call with smaller deck 
     (_, Deal) -> do put (second_case_update st card cards) 
         game 
     (_, Tie) -> do lift $ putStrLn "This is a tie" 
         lift $ print st 

where -- state updates: 
      -- I separate these out hoping this will make the needed sort 
      -- of 'logic' above clearer. 
    first_case_update s card cards= 
    s { numPlayers = numPlayers s + 1 
     , pWins = [if x < dealerCard then -1 else 1 | 
        x <- zipWith (+) (pWins s) (tail (inPlay s)) ] 
     , dWins = if dealerCard == maximum (inPlay s) 
        then dWins s + 1 
        else dWins s - 1 
     , deck = cards } 
      where dealerCard = head (inPlay s) 

    second_case_update s card cards = 
    s { toDeal = toDeal s - 1 
     , toBurn = 0 
     , inPlay = card : inPlay s 
     , deck = cards} 

-- a StateTified formulation of your gameRound 
gameround :: Monad m => Int -> GameRound m() 
gameround card = do 
    s <- get 
    case (toDeal s, stage s) of 
    (0, Deal) -> 
     put $ s { toDeal = numPlayers s + 1 
       , pWins = [if x < dealerCard then -1 else 1 | 
          x <- zipWith (+) (pWins s) (tail (inPlay s)) ] 
       , dWins = if dealerCard == maximum (inPlay s) 
           then dWins s + 1 
           else dWins s - 1} 
        where dealerCard = head (inPlay s) 
    (_, Deal) -> 
     put $ s { toDeal = toDeal s - 1 
       , toBurn = 0 
       , inPlay = card : inPlay s} 
    (_, Tie) -> return() 


fisherYatesStep :: RandomGen g => (Map Int a, g) -> (Int, a) -> (Map Int a, g) 
fisherYatesStep (m, gen) (i, x) = ((insert j x . insert i (m ! j)) m, gen') 
    where 
     (j, gen') = randomR (0, i) gen 

fisherYates :: RandomGen g => g -> [a] -> ([a], g) 
fisherYates gen [] = ([], gen) 
fisherYates gen l = toElems $ Prelude.foldl 
     fisherYatesStep (initial (head l) gen) (numerate (tail l)) 
    where 
     toElems (x, y) = (elems x, y) 
     numerate = zip [1..] 
     initial x gen = (singleton 0 x, gen)  
3

Per rendere il codice più leggibile, è necessario rompere la struttura del gioco in componenti significativi e riorganizzando di conseguenza il codice. Quello che hai fatto è mettere tutto lo stato del gioco in una struttura dati. Il risultato è che devi gestire tutti i dettagli del gioco tutto il tempo.

Il gioco tiene traccia dei punteggi per ciascun giocatore e il banco. A volte aggiunge 1 o sottrae 1 da un punteggio. I punteggi non sono usati per nient'altro.Separare la gestione dei punteggi dall'altro codice:

-- Scores for each player and the dealer 
data Score = Score [Int] Int 

-- Outcome for each player and the dealer. 'True' means a round was won. 
data Outcome = Outcome [Bool] Bool 

startingScore :: Int -> Score 
startingScore n = Score (replicate n 0) 0 

updateScore :: Outcome -> Score -> Score 
updateScore (Outcome ps d) (Score pss ds) = Score (zipWith upd pss pos) (update ds d) 
    where upd s True = s+1 
     upd s False = s-1 

Le carte distribuite sono anche associate ai giocatori e al banco. Vincere o perdere un round si basa solo sui valori della carta. Separare il punteggio calcolo dalla altro codice:

type Card = Int 
data Dealt = Dealt [Card] Card 

scoreRound :: Dealt -> Outcome 
scoreRound (Dealt ps dealerCard) = Outcome (map scorePlayer ps) (dealerCard == maximumCard) 
    where 
    maximumCard = maximum (dealerCard : ps) 
    scorePlayer p = p >= dealerCard 

direi una partita consiste di tutti i passi necessari per produrre una singola Outcome. Riorganizzare il codice di conseguenza:

type Deck = [Card] 

deal :: Int -> Deck -> (Dealt, Deck) 
deal n d = (Dealt (take n d) (head $ drop n d), drop (n+1) d) -- Should check whether deck has enough cards 

-- The 'input-only' parts of GameState 
type GameConfig = 
    GameConfig {nPlayers :: Int} 

gameRound :: GameConfig -> Deck -> (Deck, Outcome) 
gameRound config deck = let 
    (dealt, deck') = deal (nPlayers config) deck 
    outcome  = scoreRound dealt 
    in (deck', outcome) 

Questa copre la maggior parte di quello che era nel codice originale. Puoi avvicinare il resto in un modo simile.


L'idea principale che si dovrebbe ottenere è che Haskell rende facile programmi scomporre in piccoli pezzi che sono significative per conto proprio. Questo è ciò che rende più facile lavorare con il codice.

Invece di mettere tutto in GameState, ho creato Score, Outcome, Dealt e Deck. Alcuni di questi tipi di dati provenivano dall'originale GameState. Altri non erano affatto nel codice originale; erano impliciti nel modo in cui erano organizzati circuiti complicati. Invece di mettere tutto il gioco in gameRound, ho creato updateScore, scoreRound, deal, e altre funzioni. Ognuno di questi interagisce con solo pochi dati.

Problemi correlati