2012-06-18 5 views
7

Als Übung versuche ich eine Simulation für das Casino-Spiel "Krieg" in Haskell zu schreiben.Wie machen Sie dieses Stück Haskell-Code prägnanter?

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

Es ist ein sehr einfaches Spiel mit ein paar Regeln. Es wäre ein ansonsten sehr einfaches Problem, in irgendeiner der imperativen Sprachen zu schreiben, die ich kenne, aber ich habe Mühe, es in Haskell zu schreiben.

Der Code, den ich bisher habe:

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

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

Ich verstehe, warum zusätzliche Arbeit zur Schaffung von Zufallszahlen gehen, aber ich bin ziemlich sicher, dass ich einige grundlegende Konstrukt oder Konzept fehle. Es sollte nicht so schwierig sein, eine Sammlung von Zuständen zu behalten und eine Verzweigungslogik über eine Liste von Eingaben zu führen. Ich konnte nicht einmal eine gute Methode finden, um die Logik für den Fall zu schreiben, wo es Krawatten auf dem Tisch gibt.

Ich frage nicht nach Komplettlösungen. Es wäre wirklich schön, wenn jemand darauf hinweisen könnte, was ich falsch mache, oder einige gute Lesestoffe, die relevant sind.

Vielen Dank im Voraus.

+1

Sie sollten in die ['StateT'] (http://hackage.haskell.org/packages/archive/mtl/latest/doc/html/Control-Monad-State-Lazy.html#v:StateT) schauen und ['RandT'] (http://hackage.haskell.org/packages/archive/MonadRandom/0.1.6/doc/html/Control-Monad-Random.html#t:RandT) Monadetransformatoren. –

Antwort

6

Ein nützliches Entwurfsmuster zum Aufrechterhalten des Anwendungszustands ist die sogenannte Zustands-Monade. Sie können eine Beschreibung und einige einführende Beispiele finden here. Außerdem möchten Sie vielleicht mit dem Namen Felder anstelle eines Tupels für GameState, beispielsweise mit einem Datentyp zu berücksichtigen:

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

Dadurch wird es einfacher/update einzelne Felder zuzugreifen mit record syntax.

+1

Record-Syntax kann auch Ihren Code leichter verständlich machen, da die Felder beschreibende Namen anstelle von Tupeln haben können, die nur Typen sind, und ein Tupel von '(Int, Int, Int)' ist nicht sehr nützlich, wenn Sie nicht können Erinnere dich, für was 'Int' ist. +1 für Staat Monad auch, es spart eine Menge von manuellen Rohrleitungen. –

2

Es kam mir in den Sinn, dass die Empfehlung "use StateT" ein wenig undurchsichtig sein könnte, also übersetzte ich ein wenig in diesen Jargon und hoffte, dass Sie sehen könnten, wie man von dort weitergeht. Es ist vielleicht am besten, den Status des Decks in den Spielstatus aufzunehmen. gameround unten wiederholt nur Ihre Funktion in StateT-Jargon. Die vorherige Definition, game, verwendet das deck Feld des Spielstatus, wird kontinuierlich reduziert und enthält das gesamte Spiel. Ich führe IO-Aktionen ein, nur um zu zeigen, wie es gemacht wird, und so können Sie die Abfolge von Zuständen sehen, wenn Sie main in ghci aufrufen. Sie heben IO-Aktionen in die StateT-Maschinerie auf, um sie auf eine Ebene mit den Gets und Puts zu bringen. Beachten Sie, dass wir in mose-Subcases den neuen Zustand setzen und dann die Aktion wiederholen, damit der do-Block die vollständige rekursive Operation enthält. (Tie und ein leeres Deck beenden das Spiel sofort.) Dann in der letzten Zeile von main wir runStateT auf dieser selbst aktualisierenden game ergibt eine Funktion GameState -> IO (GameState,()); dann füttern wir dies mit einem bestimmten Anfangszustand einschließlich des zufällig bestimmten Decks, um die IO-Aktion zu erhalten, die das Hauptgeschäft ist. (Ich verstehe nicht, wie das Spiel funktionieren soll, wurde aber mechanisch Dinge in Bewegung um die Idee zu vermitteln.)

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

Um den Code besser lesbar zu machen, sollten Sie die Struktur des Spiels brechen in sinnvolle Komponenten und reorganisieren Sie Ihren Code entsprechend. Was Sie getan haben, ist, den gesamten Spielstatus in eine Datenstruktur zu bringen. Das Ergebnis ist, dass Sie ständig mit allen Spieldetails umgehen müssen.

Das Spiel verfolgt die Punktzahlen für jeden Spieler und den Dealer. Manchmal addiert es 1 oder subtrahiert 1 von einer Punktzahl. Punkte werden für nichts anderes verwendet.Trennen Sie die Punkteverwaltung vom anderen Code:

Die ausgeteilten Karten sind auch mit Spielern und dem Dealer verbunden. Das Gewinnen oder Verlieren einer Runde basiert nur auf den Kartenwerten. Trennen Sie die Partitur Berechnung aus dem anderen Code aus:

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 

Ich würde sagen, ein Spielrunde aller Schritte besteht Automatisierung ein Outcome zu produzieren. Reorganisieren Sie den Code entsprechend:

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) 

Dies deckt die meisten von dem, was im ursprünglichen Code war. Sie können sich dem Rest auf ähnliche Weise nähern.


Leitgedanke Sie bekommen sollte, ist, dass Haskell es leicht zu decompose Programme in kleine Stücke macht, die auf ihren eigenen sinnvoll sind. Das macht es einfacher, mit Code zu arbeiten.

Statt alles in GameState des Setzens, habe ich Score, Outcome, Dealt und Deck. Einige dieser Datentypen stammen aus dem ursprünglichen GameState. Andere waren überhaupt nicht im ursprünglichen Code; sie waren implizit in der Art, wie komplizierte Schleifen organisiert wurden. Anstatt das gesamte Spiel in gameRound zu setzen, erstellte ich updateScore, scoreRound, deal und andere Funktionen. Jeder von diesen interagiert mit nur wenigen Daten.

Verwandte Themen