Justin Pombrio

Chopsticks

Chopsticks is a silly game you play with your fingers and someone else’s fingers. The number of possible hand positions (5 * 5 * 5 * 5) is small enough that it can easily be brute-force solved with a few tricks. First, we will need memoization. This memoization module handles only unary functions, but at least it hides it’s implementation :-). (That is to say it’s impossible to obtain the memoization table from a memoized function.)

module Memoize (Memoized, recall, forget) where

import Prelude hiding (lookup)
import Data.Map
import Control.Monad.State

newtype Memoize a b c = Memoize {
    memoize :: Map a b -> (c, Map a b)
}

type Memoized a b = a -> Memoize a b b

forget :: (Memoized a b) -> a -> b
forget func arg = fst (memoize (func arg) empty)

recall :: Ord a => (a -> Memoize a b b) -> a -> Memoize a b b
recall func arg = Memoize recall' where
    recall' memory = case lookup arg memory of
        Just val -> (val, memory)
        Nothing -> remember memory
    remember memory =
        let (val, memory') = memoize (func arg) memory in
        (val, insert arg val memory')

instance Monad (Memoize a b) where
    return val = Memoize (\memory -> (val, memory))
    m >>= func = Memoize (\memory ->
        let (val, memory') = memoize m memory in
        memoize (func val) memory')

And it’s use, for our old friend the Fibonacci sequence,

import Memoize

fib :: Int -> Integer
fib = forget memFib

memFib :: Memoized Int Integer
memFib 0 = return 1
memFib 1 = return 1
memFib n = do
  x <- recall memFib (n - 1)
  y <- recall memFib (n - 2)
  return (x + y)

Now we can write an algorithm to solve games. The following module implementats the Minimax algorithm, except that

module Minimax (Color, Game, solve) where

import Memoize

data Color = Red    -- Loss
           | Green  -- Win
           | Yellow -- Draw
    deriving (Eq, Ord, Show)

class Ord board => Game board where
    moves :: board -> Either Color [board]

solve :: Game board => board -> Color
solve board = forget (explore []) board

explore :: Game board => [board] -> Memoized board Color
explore path board =
  if elem board path then return Yellow -- Cycles are draws
  else case moves board of
    Left color -> return color -- End of the game
    Right nextBoards -> do
      colors <- mapM (recall (explore (board : path))) nextBoards
      -- If you can move such that your opponent loses, then you can win
      if any (== Red) colors then return Green
      -- If you can move such that your opponent draws, then you can draw
      else if any (== Yellow) colors then return Yellow
      -- If your opponent can win regardless of your move, then you lose
      else return Red

And finally the definition of the ChopSticks game. I tried to follow the rules on Wikipedia, but it’s kind of messy. There’s probably a mistake. Anyhow, running solve on Board 1 1 1 1 yields Yellow (a draw).

module ChopSticks where

import Data.List
import Debug.Trace
import qualified Data.Map as Map


type Fingers = Int

fingersOnHand = 5

fingerPlus :: Fingers -> Fingers -> Fingers
fingerPlus x y = if x + y >= fingersOnHand then 0 else x + y

fingerTransfer :: Fingers -> Fingers -> [Fingers]
fingerTransfer x y =
    let cap = min (x - 1) (fingersOnHand - y - 1) in
    if x > y then delete (x - y) [1 .. cap]
    else [1 .. cap]


data Board = Board Fingers Fingers Fingers Fingers
    deriving (Eq, Ord, Show)


instance Game Board where
    moves (Board 0 0 _ _) = Left Red
    moves (Board _ _ 0 0) = Left Green
    moves (Board 0 a c d) = Right $ nub $ [
        Board (c `fingerPlus` a) d 0 a,
        Board c (d `fingerPlus` a) 0 a]
        ++ [Board c d x (a - x) | x <- fingerTransfer a 0]
    moves (Board a 0 c d) = Right $ nub $ [
        Board (c `fingerPlus` a) d a 0,
        Board c (d `fingerPlus` a) a 0]
        ++ [Board c d (a - x) x | x <- fingerTransfer a 0]
    moves (Board a b c d) = Right $ nub $ [
        Board (c `fingerPlus` a) d a b,
        Board (c `fingerPlus` b) d a b,
        Board c (d `fingerPlus` a) a b,
        Board c (d `fingerPlus` b) a b]
        ++ [Board c d (a + x) (b - x)
            | x <- fingerTransfer b a]
        ++ [Board c d (a - x) (b + x)
            | x <- fingerTransfer a b]


instance Game Int where
    moves 0 = Left Yellow
    moves 1 = Left Red
    moves 2 = Right [0, 1]
    moves 3 = Right [2, 3]

test board = putStrLn $ show $ solve (board :: Board)