2012-06-18 23 views
7

Khi thực hành, tôi đang cố gắng viết một mô phỏng cho trò chơi casino "chiến tranh" trong Haskell.Làm cách nào để đoạn mã Haskell này ngắn gọn hơn?

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

Đây là trò chơi rất đơn giản với một vài quy tắc. Nó sẽ là một vấn đề khác rất đơn giản để viết trong bất kỳ ngôn ngữ mệnh lệnh tôi biết, tuy nhiên tôi đang đấu tranh để viết nó trong Haskell.

Code tôi có cho đến nay:

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

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

Tôi hiểu tại sao việc làm thêm có để đi theo hướng tạo ra các số ngẫu nhiên, nhưng tôi khá chắc chắn tôi đang thiếu một số cấu trúc cơ bản hay khái niệm. Nó không phải là điều này vụng về để giữ một bộ sưu tập của các tiểu bang, và chạy một logic phân nhánh trên một danh sách các đầu vào. Tôi thậm chí không thể tìm ra cách tốt để viết logic cho trường hợp có các mối quan hệ trên bàn.

Tôi không yêu cầu giải pháp hoàn chỉnh. Sẽ thật tuyệt nếu ai đó có thể chỉ ra những gì tôi đang làm sai, hoặc một số tài liệu đọc tốt có liên quan.

Xin cảm ơn trước.

+1

Bạn nên xem [StateT'] (http://hackage.haskell.org/packages/archive/mtl/latest/doc/html/Control-Monad-State-Lazy.html#v:StateT) và ['RandT'] (http://hackage.haskell.org/packages/archive/MonadRandom/0.1.6/doc/html/Control-Monad-Random.html#t:RandT) máy biến áp đơn nguyên. –

Trả lời

6

Mẫu thiết kế hữu ích để duy trì trạng thái ứng dụng là đơn vị trạng thái được gọi. Bạn có thể tìm thấy mô tả và một số ví dụ giới thiệu here. Ngoài ra, bạn có thể muốn xem xét sử dụng một kiểu dữ liệu với các lĩnh vực tên thay vì một tuple cho GameState, ví dụ:

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

Điều này sẽ làm cho nó dễ dàng hơn để truy cập vào/cập nhật các lĩnh vực cá nhân sử dụng record syntax.

+1

Cú pháp ghi cũng có thể làm cho mã của bạn dễ hiểu hơn, vì các trường có thể có các tên mô tả thay vì các bộ dữ liệu chỉ là các kiểu và một bộ '(Int, Int, Int)' không hữu ích nếu bạn không thể hãy nhớ cái 'Int' nào cho cái gì. 1 cho nhà nước monad là tốt, nó giúp tiết kiệm rất nhiều ống nước thủ công. –

2

Tôi nhận thấy rằng đề xuất 'sử dụng StateT' có thể hơi mờ đục vì vậy tôi đã dịch một chút vào thuật ngữ đó, hy vọng bạn có thể thấy cách đi từ đó. Nó có thể là tốt nhất để bao gồm trạng thái của boong trong trạng thái trò chơi. gameround dưới đây chỉ cần nghỉ ngơi chức năng của bạn trong lingo StateT. Định nghĩa trước, game sử dụng trường deck của trạng thái trò chơi, giảm liên tục và chứa toàn bộ trò chơi. Tôi giới thiệu các hành động IO, chỉ để cho thấy cách nó được thực hiện, và vì vậy bạn có thể thấy sự kế thừa của các trạng thái nếu bạn gọi chính trong ghci. Bạn 'nâng' hành động IO vào máy móc StateT, để đặt chúng trên một mức độ với các nhận được và đặt. Lưu ý rằng trong các cặp con mose, chúng ta đặt trạng thái mới và sau đó gọi hành động được lặp lại, để khối làm chứa hoạt động đệ quy hoàn chỉnh. (Tie và một boong trống kết thúc trò chơi ngay lập tức.) Sau đó, trong dòng cuối cùng của main chúng tôi runStateT trên tự cập nhật này game năng suất một GameState -> IO (GameState,()); sau đó chúng tôi cho ăn loại này với một trạng thái khởi đầu nhất định bao gồm sàn được xác định ngẫu nhiên để có được hành động IO là hoạt động kinh doanh chính. (Tôi không làm theo thế trận có nghĩa vụ phải làm việc, nhưng đã bị một cách máy móc di chuyển những thứ xung quanh để có được những ý tưởng trên.)

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

Để làm cho mã dễ đọc hơn, bạn nên phá vỡ các cấu trúc của trò chơi thành các thành phần có ý nghĩa và sắp xếp lại mã của bạn cho phù hợp. Những gì bạn đã làm là đặt tất cả trạng thái của trò chơi vào một cấu trúc dữ liệu. Kết quả là bạn phải đối phó với tất cả các chi tiết trò chơi mọi lúc.

Trò chơi theo dõi điểm số cho mỗi người chơi và người chia bài. Đôi khi nó thêm 1 hoặc trừ 1 từ điểm. Điểm không được sử dụng cho bất cứ điều gì khác.Tách biệt quản lý điểm số với mã khác:

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

Thẻ được xử lý cũng được liên kết với người chơi và đại lý. Chiến thắng hoặc thua một vòng chỉ dựa trên các giá trị thẻ. Tách ra tính toán điểm số từ mã khác:

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 

Tôi muốn nói một vòng trò chơi bao gồm tất cả các bước cần thiết để tạo ra một Outcome. Sắp xếp lại mã cho phù hợp:

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) 

Điều này bao gồm hầu hết những gì có trong mã gốc. Bạn có thể tiếp cận phần còn lại theo cách tương tự.


Ý tưởng chính bạn sẽ nhận được là Haskell làm cho nó dễ dàng để chương trình phân hủy thành từng miếng nhỏ mà có ý nghĩa riêng của họ. Đó là điều làm cho mã dễ làm việc hơn.

Thay vì đặt tất cả mọi thứ vào GameState, tôi đã tạo Score, Outcome, Dealt, và Deck. Một số loại dữ liệu này xuất phát từ số GameState gốc. Những người khác không có mã gốc; chúng được ngầm định trong cách các vòng phức tạp được tổ chức. Thay vì đưa toàn bộ trò chơi vào gameRound, tôi đã tạo updateScore, scoreRound, deal và các chức năng khác. Mỗi trong số này tương tác với chỉ một vài mẩu dữ liệu.

Các vấn đề liên quan