Я создал игру по угадыванию чисел для двух игроков на Haskell. Моя главная цель заключалась в том, чтобы попрактиковаться в обращении с «состоянием» на чисто функциональном языке (например, с оценками игроков, их ходом и т. Д.).
Вот правила:
Геймплей:
Два игрока по очереди угадывают случайное число от 1 до 10. Ответы будут введены в командную строку.
Подсчет очков:
- Если игрок угадает номер правильно, ему будет начислено 5 очков.
- Если игрок находится в пределах двух (включительно) от ответа, ему будет начислено 3 очка.
- Если игрок находится в пределах трех (включительно) от ответа, ему будет начислено 1 очко.
- Если у игрока меньше 7 очков, он теряет одно очко. Оценка не может быть отрицательной.
- Все остальные смещения дают нулевые баллы.
- Игра будет продолжаться до тех пор, пока один из игроков не наберет 10 очков.
Предостережения:
- Это не было разработано, чтобы быть упражнением в приятном игровом дизайне — очевидно, что оптимальное решение — всегда выбирать пять, что не вызывает большого волнения. : D
- Я знаю что
Control.Monad.Stateсуществует, но я хочу попрактиковаться в отслеживании состояния без него. - Я знаю, что «взаимную рекурсию» сложно уследить. Я хотел бы получить несколько предложений по избавлению от того, что не связано с вложением
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 ответ
Я бы утверждал, что 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
