Reputation: 53077
A simple form of a turn-based game can be abstracted in a functional language as:
data Player
= PlayerA
| PlayerB
deriving Show
data Game state move = Game {
start :: state,
turn :: (move, move)
-> state
-> Either Player state}
play :: [(m, m)] -> Game s m -> Maybe Player
play moves game
= either Just (const Nothing)
$ foldr tick (Right (start game)) moves where
tick move (Right state) = turn game move state
tick move p = p
In that setting, a game has an initial state, a type of valid moves, and a function that computes the next state (or a winner) based on the move picked by each player on that turn. This definition is enough to create any kind of turn-based game - from something as simple as rock-paper-scissors, to a fully featured battle RPG. Here is a simple duel game:
data DuelMove = Punch | Guard | Rest
data DuelState = DuelState {
p1hp :: Int,
p2hp :: Int}
deriving Show
duel :: Game DuelState DuelMove
duel = Game start turn where
start = DuelState 4 4
turn (ma,mb) (DuelState p1 p2)
| p1 <= 0 = Left PlayerB
| p2 <= 0 = Left PlayerA
| otherwise = attack ma mb where
attack Punch Punch = Right (DuelState (p1-1) (p2-1))
attack Punch Guard = Right (DuelState (p1-2) (p2+0))
attack Punch Rest = Right (DuelState (p1+0) (p2-2))
attack Guard Punch = Right (DuelState (p1+0) (p2-2))
attack Guard Guard = Right (DuelState (p1+0) (p2+0))
attack Guard Rest = Right (DuelState (p1+0) (p2+2))
attack Rest Punch = Right (DuelState (p1-2) (p2+0))
attack Rest Guard = Right (DuelState (p1+0) (p2+2))
attack Rest Rest = Right (DuelState (p1+2) (p2+2))
main :: IO ()
main = print $ play moves duel where
moves = [
(Punch, Punch),
(Punch, Guard),
(Guard, Punch),
(Rest, Rest),
(Punch, Guard),
(Guard, Punch),
(Punch, Rest)]
There is a problem with that abstraction, though: adding new moves require editing the definition of the the types and, consequently, the source-code of turn
. This is not sufficient if you want to allow your users to define their own moves. Is there any abstraction for similar turn-based games, which elegantly allow for new moves to be added without modifying the original code?
Upvotes: 3
Views: 162
Reputation: 153342
A simple trick is to parameterize the turn
function. Thus:
type DuelMovePlus = Either DuelMove
turn :: (a -> DuelMove -> Either Player DuelState)
-> (DuelMove -> a -> Either Player DuelState)
-> (a -> a -> Either Player DuelState)
-> DuelMovePlus a -> DuelMovePlus a -> Either Player DuelState
turn userL userR userLR = \case
(Left l, Left r) -> {- same code as before -}
(Left l, Right r) -> userR l r
(Right l, Left r) -> userL l r
(Right l, Right r) -> userLR l r
Upvotes: 1