mixed-strategies-0.1.0.0: Data/MixedStrategies.hs
-- Copyright © 2011 Bart Massey
-- [This program is licensed under the "MIT License"]
-- Please see the file COPYING in the source
-- distribution of this software for license terms.
-- | Calculate an optimal mixed strategy given the payoff
-- matrix of a two-player zero-sum single-round iterated
-- simultaneous game. Follows the method of Chapter 6 of
-- J.D. Williams' \"The Compleat Strategyst\" (McGraw-Hill
-- 1954). Throughout, the player playing the rows (strategies
-- on the left) is assumed to be the \"maximizer\" and the
-- player playing the columns (strategies on top) is assumed
-- to be the \"minimizer\".
module Data.MixedStrategies (
Schema(..),
Edge(..),
readSchema,
pivot,
Solution(..),
extractSolution,
solved,
solve
) where
import Prelude hiding (Left, Right, lookup)
import Data.Array
import Data.Function (on)
import Data.List hiding (lookup)
import Data.Map (lookup, fromList)
import Data.Ord (comparing)
import Text.Printf
import Text.SimpleTabular
dim :: Array Int t -> Int
dim a = let (1, ix) = bounds a in ix
dims :: Array (Int, Int) t -> (Int, Int)
dims a = let ((1, 1), ixs) = bounds a in ixs
-- | An 'Edge' of a 'Schema' contains annotations of the Schema.
data Edge = Left | Top | Right | Bottom deriving (Ord, Eq, Ix)
-- | Schema describing a two-player hidden information game.
data Schema = Schema {
-- | Score offset to make the game fair.
offset :: Double,
-- | \"Determinant\".
d :: Double,
-- | Strategy \"names\", given as labels along an edge of the
-- payoff matrix.
names :: Array Edge (Array Int (Maybe Int)),
-- | Payoff matrix.
payoffs :: Array (Int, Int) Double
}
-- | Show a 'Schema' in tabular format.
instance Show Schema where
show s =
let tab =
["" : printNameVec Top] ++
map printRow [1 .. nr - 1] ++
["" : printRowM nr] ++
["" : printNameVec Bottom]
in
printf "O = %.2f, D = %.2f\n" (offset s) (d s) ++
tabular (replicate (nc + 1) 'r' ++ "l") tab
where
(nr, nc) = dims $ payoffs s
printRowM i =
printVec $ ixmap (1, nc) (\j -> (i, j)) (payoffs s)
where
printVec a =
map (printf "%.2f" . (a !)) [1 .. dim a]
printName e i =
case names s ! e ! i of
Just v -> printf "%d" v
Nothing -> ""
printNameVec e =
map (printName e) [1 .. dim (names s ! e)]
printRow i =
[printName Left i] ++
printRowM i ++
[printName Right i]
-- | Read a 'Schema' (actually a payoff matrix) from standard input.
readSchema :: IO Schema
readSchema =
construct `fmap` getContents
where
construct s =
let o = minimum $ elems ps in
let core = map (\(c, x) -> (c, x - o)) $ assocs ps
augr = zip (zip (repeat (nr + 1)) [1..nc]) (repeat (-1))
augc = zip (zip [1..nr] (repeat (nc + 1))) (repeat 1)
augv = ((nr + 1, nc + 1), 0)
bounds' = ((1, 1), (nr + 1, nc + 1)) in
let ps' = array bounds' $ core ++ augr ++ augc ++ [augv] in
Schema {
offset = o,
d = 1,
names = mkNames,
payoffs = ps' }
where
ps = mkPayoffs $ map (map read . words) $ lines s
(nr, nc) = dims ps
mkNames =
array (Left, Bottom) [namesLeft, namesTop, namesRight, namesBottom]
where
namesLeft = (Left, listArray (1, nr) [Just n | n <- [1..nr]])
namesTop = (Top, listArray (1, nc) [Just n | n <- [1..nc]])
namesRight = (Right, listArray (1, nr) (replicate nr Nothing))
namesBottom = (Bottom, listArray (1, nc) (replicate nc Nothing))
mkPayoffs :: [[Double]] -> Array (Int, Int) Double
mkPayoffs l@(e : es)
| all ((== length e) . length) es =
listArray ((1, 1), (length l, length e)) $ concat l
mkPayoffs _ = error "bad payoff matrix format"
-- | Execute one pivot step in the 'Schema' reduction.
pivot :: Schema -> Schema
pivot s =
Schema {
offset = offset s,
d = pv,
names = updateNames,
payoffs = updatePayoffs }
where
ps = payoffs s
(nr, nc) = dims ps
((pr, pc), pv) =
maximumBy pivotCompare $
map (minimumBy pivotCompare) $
groupBy ((==) `on` (snd . fst)) $
sortBy (comparing (snd . fst)) potPivots
where
potPivots =
filter okPivot $ assocs ps
where
okPivot ((_, c), v) = v > 0 && (ps ! (nr, c)) < 0
pivotCompare =
comparing pivotCriteria
where
pivotCriteria ((r, c), p) =
- (ps ! (nr, c)) * (ps ! (r, nc)) / p
updateNames =
array (Left, Bottom) [
updateName Left Bottom pr pc,
updateName Bottom Left pc pr,
updateName Right Top pr pc,
updateName Top Right pc pr ]
where
updateName e e' i i' =
(e, (names s ! e) // [(i, names s ! e' ! i')])
updatePayoffs =
listArray (bounds ps) $ map updatePayoff $ assocs $ ps
where
updatePayoff ((r, c), n)
| r == pr && c == pc = d s
| r == pr = n
| c == pc = -n
| otherwise = (n * pv - (ps ! (pr, c)) * (ps ! (r, pc))) / d s
-- | A game solution, given as the value of the game and
-- an optimal mixed strategy for each player.
data Solution = Solution {
value :: Double,
leftStrategy, topStrategy :: [Double] }
instance Show Solution where
show soln =
printf "value = %.2f\n" (value soln) ++
printf "leftmax = %s\n" (showStrategy leftStrategy) ++
printf "topmin = %s" (showStrategy topStrategy)
where
showStrategy strat =
unwords $ map (uncurry (printf "%d:%.2f")) $
zip [(1::Int)..] $ strat soln
-- | Given a solved 'Schema', extract the 'Solution'
-- implied by that 'Schema'.
extractSolution :: Schema -> Solution
extractSolution s | solved s = Solution {
value = offset s + d s / (ps ! ds),
leftStrategy = strat Bottom $ ixmap (1, nc - 1) (\j -> (nr, j)) ps,
topStrategy = strat Right $ ixmap (1, nr - 1) (\j -> (j, nc)) ps }
where
ps = payoffs s
ds@(nr, nc) = dims ps
strat e odds =
let strats = zip (elems (names s ! e)) (elems odds) in
let mStrats = fromList [(nm, pr) | (Just nm, pr) <- strats] in
let vStrats = map (zeroInactive mStrats) [1 .. dim odds] in
[o / sum vStrats | o <- vStrats]
where
zeroInactive m nm =
case lookup nm m of
Just o -> o
Nothing -> 0
extractSolution _ = error "refusing to extract solution from unsolved schema"
-- | Returns 'True' if the given 'Schema' is fully reduced (\"solved\");
-- 'False' otherwise.
solved :: Schema -> Bool
solved s =
let ps = payoffs s in
let ((1,1), (nr, nc)) = bounds ps in
all (>= 0) [v | ((r, c), v) <- assocs ps, r == nr || c == nc]
-- | Keep reducing the given 'Schema' until a fixed
-- point is reached, and then return the corresponding
-- 'Solution'.
solve :: Schema -> Solution
solve s
| solved s = extractSolution s
| otherwise = solve (pivot s)