MaiaVictor
MaiaVictor

Reputation: 53077

General approach for user-extensible move effects on turn-based games?

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

Answers (1)

Daniel Wagner
Daniel Wagner

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

Related Questions