2012-06-18 10 views
7

Como práctica, estoy tratando de escribir una simulación para la "guerra" del juego de casino en Haskell.¿Cómo es que esta parte del código Haskell es más conciso?

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

Es un juego muy simple, con algunas reglas. Sería un problema por lo demás muy simple escribir en cualquiera de los idiomas imperativos que conozco, sin embargo, estoy luchando por escribirlo en Haskell.

El código que tengo hasta ahora:

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

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

entiendo por qué trabajo adicional tiene que ir hacia la creación de números aleatorios, pero estoy bastante seguro de que me falta algo de construcción básica o concepto. No debería ser tan incómodo mantener una colección de estados y ejecutar una lógica de bifurcación sobre una lista de entrada. Ni siquiera pude encontrar una buena manera de escribir la lógica para el caso donde hay vínculos sobre la mesa.

No estoy pidiendo soluciones completas. Sería realmente agradable si alguien pudiera señalar lo que estoy haciendo mal, o algunos buenos materiales de lectura que sean relevantes.

Gracias de antemano.

+1

Debería consultar el ['StateT'] (http://hackage.haskell.org/packages/archive/mtl/latest/doc/html/Control-Monad-State-Lazy.html#v:StateT) y ['RandT'] (http://hackage.haskell.org/packages/archive/MonadRandom/0.1.6/doc/html/Control-Monad-Random.html#t:RandT) transformadores de mónadas. –

Respuesta

6

Un patrón de diseño útil para mantener el estado de la aplicación es la denominada mónada de estado. Puede encontrar una descripción y algunos ejemplos introductorios here. Además, es posible que desee considerar el uso de un tipo de datos con los campos nombre, en lugar de una tupla para GameState, por ejemplo:

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

Esto hará que sea más fácil acceder a/actualización de los campos individuales utilizando record syntax.

+1

La sintaxis de registros también puede hacer que su código sea más fácil de comprender, ya que los campos pueden tener nombres descriptivos en lugar de tuplas que son solo tipos, y una tupla de '(Int, Int, Int)' no es muy útil si no puede recuerda qué 'Int' es para qué. +1 para mónada de estado también, ahorra una gran cantidad de plomería manual. –

2

Se me ocurrió que la recomendación "usar StateT" podría ser un poco opaca, así que traduje un poco a esa jerga, con la esperanza de que pudieras ver cómo ir desde allí. Puede ser mejor incluir el estado del mazo en el estado del juego. gameround a continuación solo reafirma su función en StateT lingo. La definición anterior, game usa el campo deck del estado del juego, continuamente reducido, y contiene todo el juego. Presento acciones IO, solo para mostrar cómo se hace, y para que pueda ver la sucesión de estados si llama a main en ghci. Usted "levanta" las acciones de E/S en la maquinaria de StateT, para ponerlos al mismo nivel que los get y puts. Tenga en cuenta que en las subcategorías mose, ponemos el nuevo estado y luego pedimos que se repita la acción, de modo que el bloque do contenga la operación recursiva completa. (Tie y un mazo vacío terminan el juego de inmediato.) Luego en la última línea de main tenemos runStateT en esta actualización automática de game dando como resultado una función GameState -> IO (GameState,()); luego alimentamos esto con un cierto estado inicial que incluye el mazo determinado aleatoriamente para obtener la acción IO que es el negocio principal. (No sigo cómo el juego se supone que funciona, pero se movía mecánicamente cosas para transmitir la idea de.)

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

Para que el código sea más legible, debe romper la estructura del juego en componentes significativos, y reorganizando tu código en consecuencia. Lo que has hecho es poner todo el estado del juego en una estructura de datos. El resultado es que debes lidiar con todos los detalles del juego todo el tiempo.

El juego realiza un seguimiento de las puntuaciones de cada jugador y el distribuidor. A veces agrega 1 o resta 1 de una puntuación. Las puntuaciones no se usan para nada más.Diferenciar entre la gestión de la puntuación de otro código:

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

Las cartas repartidas también están asociados con los jugadores y el crupier. Ganar o perder una ronda se basa solo en los valores de la tarjeta. Separar el cálculo de la puntuación de otro código:

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 

diría una ronda de juego se compone de todos los pasos necesarios para producir una sola Outcome. Reorganice el código en consecuencia:

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) 

Esto cubre la mayor parte de lo que estaba en el código original. Puedes acercarte al resto de una manera similar.


La idea principal que debe obtener es que Haskell hace que sea fácil de programas se descomponen en pequeños pedazos que son significativos por sí mismos. Eso es lo que hace que el código sea más fácil de usar.

En lugar de poner todo en GameState, he creado Score, Outcome, Dealt, y Deck. Algunos de estos tipos de datos provienen del GameState original. Otros no estaban en el código original en absoluto; estaban implícitos en la forma en que se organizaron los bucles complicados. En lugar de poner todo el juego en gameRound, creé updateScore, scoreRound, deal y otras funciones. Cada uno de estos interactúa con solo unos pocos datos.

Cuestiones relacionadas