Lecture 21
Stephen Brookes
today
2-person games, continued
sequences
signature SEQ =
sig
type 'a seq
exception Range
val nth : int -> 'a seq -> 'a
val length : 'a seq -> int
val tabulate : (int -> 'a) -> int -> 'a seq
val empty : unit -> 'a seq
val null : 'a seq -> bool
val map : ('a -> 'b) -> ('a seq -> 'b seq)
val reduce : ('a * 'a -> 'a) -> 'a -> 'a seq -> 'a
val reduce1 : ('a * 'a -> 'a) -> 'a seq -> 'a
val mapreduce : ('a -> 'b) -> 'b -> ('b * 'b -> 'b) -> 'a seq -> 'b
end
context
minimax
fun F s = !
F
calls
G,
let!
uses max
val M = moves s!
in!
if (null M) then score s else !
reduce1 Int.max (map (fn m => G(step(s, m))) M)!
end!
and!
G s =!
G calls F,
let !
uses
min
val M = moves s!
in !
if (null M) then ~(score s) else !
reduce1 Int.min (map (fn m => F(step(s, m))) M)!
end
duration
duration(s) = longest move sequence from s
fun duration (s : state) : int =
let
spec?
val M = moves s
in
if (null M) then 0 else
1 + reduce1 Int.max
(map (fn m => duration(step(s,m))) M)
end
terminating games
No infinite sequence of
legal moves
Definition
Game : GAME is terminating
iff duration : state -> int is total
iff for all s:state,
duration(s) evaluates to a non-negative integer
(We also assume that
for all terminal states s, score(s) terminates!)
F and G
How can we reason about the behavior of
mutually recursive functions?
corollary
We could have defined F by itself, recursively
fun F s = !
let!
val M = moves s!
in!
if (null M) then score s else !
reduce1 Int.max (map (fn m => ~ F(step(s, m))) M)!
end!
properties
The enumeration order of moves(s)
does not affect the value of F(s)
Proof?
cost analysis
Suppose at most k moves are possible,
from any state
reachable from s
why?
cost analysis
assuming score, moves
are constant-time
F(s) = score s
when (moves s) is empty
F(s) = reduce1 Int.max (map (fn m => G(step(s, m))) M)
when (moves s) is not empty
WF(0) = c
WF(d) k*WF(d-1) + c
for d > 0
SF(0) = c
SF(d) SF(d-1) + log k + c
for d > 0
WF(d) is O(kd)
SF(d) is O(d log k)
reflection
For Nim, the branching factor k is 3
Starting from 15 sticks, the duration is 15
So for Nim, the work for F 15 is bounded by
315 = 14348907
n
and the work for F n is O(3 )
reflection
The order in which moves are enumerated
does not affect the extensional behavior
of F and G
picking moves
For Me, a best move is one that leads to the
maximum outcome
PLAYER
signature PLAYER =!
sig!
structure Game : GAME!
val player : Game.state -> Game.move!
end
MaxiMe
functor MaxiMe(Game : GAME) : PLAYER =!
struct!
structure Game = Game!
open Game!
!
fun F p = ...!
and G p = ...!
!
type edge = ...!
fun maxmove ...!
fun maxbest ...!
!
fun player p = !
let!
val M = moves p!
val (m,_) = maxbest(map (fn m => (m, G(step(p,m)))) M)!
in!
m!
end!
end
MiniMe
functor MiniMe(Game : GAME) : PLAYER =!
struct!
structure Game = Game!
open Game!
!
fun F p = ...!
and G p = ...!
!
type edge = ...!
fun minmove ...!
fun minbest ...!
!
fun player p = !
let!
val M = moves p!
val (m,_) = minbest(map (fn m => (m, F(step(p,m)))) M)!
in!
m!
end!
end
- MaxiNim.player 15;
val it = 2 : Nim.move
!
- MaxiNim.player 25;
val it = 2 : Nim.move
!
- MaxiNim.player 30;
val it = 1 : Nim.move
!
- MaxiNim.player 35;
(* takes a long time! *)
problem
Full minimax players explore the entire
game tree, which may take too long.
a solution
Bounded minimax:
minimax
unbounded
fun F s = !
F, G : state -> int
let!
val M = moves s!
in!
if (null M) then score s else !
reduce1 Int.max (map (fn m => G(step(s, m))) M)!
end!
and!
G s =!
let !
val M = moves s!
in !
if (null M) then ~(score s) else !
reduce1 Int.min (map (fn m => F(step(s, m))) M)!
end
minimax
bounded
guessing games
signature EST_GAME =
sig
structure Game : GAME
val estimate : Game.state -> int
end
bounds
signature BOUND =
sig
val depth : int
end
bounded minimax
functor BoundedMaxPlayer(structure E : EST_GAME and B : BOUND) : PLAYER =!
struct!
structure Game = E.Game!
open Game!
!
!
fun F (p, d) =
!
let!
val M = moves p!
in!
if (null M) then (score p) else !
if d=0 then (E.estimate p) else!
reduce1 Int.max (map (fn m => G(step(p,m), d-1)) M)!
end!
and!
G (p, d) =!
let !
val M = moves p!
in !
if (null M) then ~(score p) else!
if d=0 then ~(E.estimate p) else!
reduce1 Int.min (map (fn m => F(step(p,m), d-1)) M)!
end!
!
fun maxmove ((m1,v1),(m2,v2)) = if v1 < v2 then (m2,v2) else (m1,v1)!
fun maxbest [ ] = raise Error!
| maxbest ((m,v)::L) = foldr maxmove (m,v) L!
fun player p = !
let!
val M = moves p!
val (m,_) = maxbest(map (fn m => (m, G(step(p,m), B.depth))) M)!
in!
m!
end!
end
guessing Nim
For Nim there is a genius heuristic
For k sticks, player p to go next,
p will lose if k mod 4 = 1,
p will win otherwise
Why?
Nim theorem
Let F and G be the Nim-based functions, so
F p = reduce1 Int.max <G(p-3), G(p-2), G(p-1)>
for p>2
F(4n) = 1
F(4n+1) = ~1
F(4n+2) = 1
F(4n+3) = 1
- structure GeniusNim =
BoundedMaxPlayer(structure E = SmartNim
and B = struct val depth = 1 end);
!
- GeniusNim.player 15;
val it = 2 : Nim.move
!
- GeniusNim.player 25;
val it = 2 : Nim.move
!
- GeniusNim.player 30;
val it = 1 : Nim.move
!
- GeniusNim.player 35;
val it = 2 : Nim.move
(* FAST!!! *)
properties
(sanity checks!)
referee
A referee takes 2 players for the same game
and alternates them from a start state,
until the game is over
reflection
We used a very simple GAME signature
Could have added extra features
print states and moves
signature REFEREE =
sig
structure E : ARENA
val run : E.Game.state -> E.Game.state list
end
functor FairReferee(E : ARENA) : REFEREE =!
struct!
structure E = E!
val moves = E.Game.moves!
val step = E.Game.step!
fun loop (player1, player2) s = !
if null(moves s) then [s] else!
let !
val m = player1 s!
val s1 = step (s, m)!
in!
s :: loop(player2, player1) s1!
end!
fun run s = loop (E.Player1, E.Player2) s!
end
transparent Nim
structure Nim : GAME =!
struct!
exception Fail of string!
type state = int!
type move = int!
fun moves 0 = [ ]!
| moves 1 = [1]!
| moves 2 = [1,2]!
| moves p = [1,2,3]!
fun score p = ...!
fun step(p,m) = ... !
end
- open Nim;
opening Nim
exception Fail of string
type state = int
val score : state -> int
type move = int
val moves : state -> move list
val step : state * move -> state
!
- moves 15;
val it = [1,2,3] : move list
opaque Nim
structure Nim :> GAME =!
struct!
exception Fail of string!
type state = int!
type move = int!
fun moves 0 = [ ]!
| moves 1 = [1]!
| moves 2 = [1,2]!
| moves p = [1,2,3]!
fun score p = ...!
fun step(p,m) = ... !
end
- open Nim;
opening Nim
exception Error
type state
val score : state -> int
type move
val moves : state -> move list
val step : state * move -> state
!
- moves 15;
Error: operator and operand don't agree
operator domain: state
operand:
int
in expression:
moves 15
What happens?
so far
Games, players
Minimax (unbounded, bounded)
Efficiency is a concern
even bounded minimax may waste time
searching fruitlessly...