Игра для двух игроков в угадывание случайных чисел

Я создал игру по угадыванию чисел для двух игроков на Haskell. Моя главная цель заключалась в том, чтобы попрактиковаться в обращении с «состоянием» на чисто функциональном языке (например, с оценками игроков, их ходом и т. Д.).

Вот правила:

Геймплей:

Два игрока по очереди угадывают случайное число от 1 до 10. Ответы будут введены в командную строку.

Подсчет очков:

  • Если игрок угадает номер правильно, ему будет начислено 5 очков.
  • Если игрок находится в пределах двух (включительно) от ответа, ему будет начислено 3 очка.
  • Если игрок находится в пределах трех (включительно) от ответа, ему будет начислено 1 очко.
  • Если у игрока меньше 7 очков, он теряет одно очко. Оценка не может быть отрицательной.
  • Все остальные смещения дают нулевые баллы.
  • Игра будет продолжаться до тех пор, пока один из игроков не наберет 10 очков.

Предостережения:

  1. Это не было разработано, чтобы быть упражнением в приятном игровом дизайне — очевидно, что оптимальное решение — всегда выбирать пять, что не вызывает большого волнения. : D
  2. Я знаю что Control.Monad.State существует, но я хочу попрактиковаться в отслеживании состояния без него.
  3. Я знаю, что «взаимную рекурсию» сложно уследить. Я хотел бы получить несколько предложений по избавлению от того, что не связано с вложением if заявления.
import Data.Char
import System.Random

main = do
    stdGen <- getStdGen
    play 0 0 P1 stdGen

play :: Int -> Int -> Player -> StdGen -> IO ()
play p1Score p2Score player stdGen
    | p1Score < 10 && p2Score < 10 = continueGame p1Score p2Score player stdGen
    | otherwise = putStrLn $ show (determineWinner p1Score p2Score) ++ " wins!"

continueGame :: Int -> Int -> Player -> StdGen -> IO ()
continueGame p1Score p2Score player stdGen = do
    putStr $ show player ++ "'s turn. Pick a number between 1 and 10: "
    chosenNumber <- getLine
    if isInteger chosenNumber
        then do
            let (randomNumber, newGen) = randomR (1, 10) stdGen :: (Int, StdGen)
            putStrLn $ "The answer is " ++ show randomNumber

            let pointsEarned = calcPointsEarned randomNumber (read chosenNumber)
            let newP1Score = min (max (p1Score + calcPointsEarnedForPlayer player P1 pointsEarned) 0) 10
            let newP2Score = min (max (p2Score + calcPointsEarnedForPlayer player P2 pointsEarned) 0) 10

            putStrLn $ "P1 Score: " ++ show newP1Score
            putStrLn $ "P2 Score: " ++ show newP2Score

            play newP1Score newP2Score (changeTurn player) newGen
        else do
            putStrLn "The input must be an integer"
            play p1Score p2Score player stdGen

data Player = P1 | P2 deriving (Show, Eq)

isInteger :: String -> Bool
isInteger = and . map isNumber

changeTurn :: Player -> Player
changeTurn player 
    | player == P1 = P2
    | otherwise = P1

calcPointsEarned :: Int -> Int -> Int
calcPointsEarned actualAnswer chosenAnswer
    | offset == 0 = 5
    | offset <= 2 = 3
    | offset <= 3 = 1
    | offset >= 7 = (-1)
    | otherwise = 0
    where offset = abs $ chosenAnswer - actualAnswer

calcPointsEarnedForPlayer :: Player -> Player -> Int -> Int
calcPointsEarnedForPlayer actualTurn player pointsEarned 
    | actualTurn == player = pointsEarned
    | otherwise = 0

determineWinner :: Int -> Int -> Player
determineWinner p1Score p2Score
    | p1Score > p2Score = P1
    | otherwise = P2
```

1 ответ
1

Я бы утверждал, что State чистый и функциональный, но я думаю, что перевод вашего текущего кода для использования State это отличное упражнение, так что оставлю это на ваше усмотрение.

Первое, о чем я хотел бы поговорить, — это заставить ваших типажей больше заниматься бухгалтерским учетом. Хорошо спроектированные типы позволяют найти правильные решения в зависимости от конструкции.

data Player = P1 | P2 deriving Show

data Game = Game { turn :: Player, p1 :: Int, p2 :: Int } deriving Show

Предпочитайте сопоставление с образцом проверке на равенство, оно часто бывает более кратким. Уменьшение линейного шума часто означает повышенную читаемость.

changeTurn :: Player -> Player
changeTurn P1 = P2
changeTurn P2 = P1

Ваш determineWinner функция имеет (в настоящее время недостижимую) логическую ошибку. Если очки обоих игроков равны, он предпочитает отдать победу второму игроку. Это может не иметь значения в вашем написанном коде, но если ваш код изменится, или вы начнете тестирование свойств, или произойдет какое-либо другое непредвиденное событие в будущем, это может неожиданно начать иметь значение. Работа со связями — это «морально» правильный поступок.

Кроме того, по правилам игры он не определяет победителя, а определяет только тот игрок, у которого больше очков.

winner :: Game -> Maybe Player
winner (Game _ p1 p2) =
    case (max p1 p2 >= 10, p1 > p2, p2 > p1) of
      (True, True, False) -> Just P1
      (True, False, True) -> Just P2
      (_, _ , _)          -> Nothing

Не проверяйте, а затем анализируйте, анализируйте и допускайте сбой. Если ваш код проверки отделен от кода синтаксического анализа, вы рискуете рассинхронизировать их и вызвать ошибки. В этом случае используйте Text.Read.readMaybe из base и оставь свой isInteger функционируют полностью.

Рекомендуется отделить как можно больше чистой игровой логики от действий ввода-вывода. Его легче протестировать, проще понять и вы сможете повторно использовать функции, которые иначе вы не смогли бы сделать.

updateRound :: Int -> Game -> Game
updateRound n (Game P1 p1 p2) = Game P2 (boundScore $ p1 + n) p2
updateRound n (Game P2 p1 p2) = Game P1 p1 (boundScore $ p2 + n)

clamp :: Ord a => a -> a -> a -> a
clamp lo val hi = lo `max` val `min` hi

boundScore :: Int -> Int
boundScore n = clamp 0 n 10

Это также устраняет необходимость в changeTurn.

Также обычно удобно отделить логику отображения от логики управления, даже если обе IO действия. Это может быть полезно для вас, если вы используете REPL во время разработки, ваши типы часто не должны включать несколько копий одной и той же информации (например, два числа и их различие), так как это несет в себе риск рассинхронизации значений. . Эти производные значения могут быть всем, что вы хотите видеть во время работы.

displayGame :: Game -> IO ()
displayGame (Game _ p1 p2) = do
    putStrLn $ "P1 Score: " ++ show p1
    putStrLn $ "P2 Score: " ++ show p2

Тем не менее, хорошо также отделить логику управления от источников ввода и вывода. Это делает вашу программу тестируемой без необходимости возиться с stdin и stdout. Также есть очень элегантное преобразование, когда вы решите начать использовать State, но я оставлю это вам, чтобы разобраться.

gameRound :: Int -> Int -> Game -> (Maybe Player, Game)
gameRound guess answer game =
  let
    score = calcPointsEarned guess answer
    nextGame = updateRound score game
  in
    (winner nextGame, nextGame)

receiveGuess :: Player -> IO Int
receiveGuess player = do
    putStr $ show player ++ "'s turn. Pick a number between 1 and 10: "
    input <- getLine
    case readMaybe input of
      Nothing -> do
        putStrLn "The input must be an integer"
        receiveGuess player
      Just guess -> pure guess

play :: Game -> IO ()
play game = do
    answer <- randomRIO (1, 10)
    guess <- receiveGuess (turn game)
    putStrLn $ "The answer is " ++ show answer
    let (mPlayer, nextGame) = gameRound guess answer game
    displayGame nextGame
    case mPlayer of
      Just player -> putStrLn $ show player ++ " wins!"
      Nothing -> play nextGame

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *