Anda di halaman 1dari 38

15-150 Fall 2014

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

Given a structure Game : GAME



For simplicity, assume weve opened Game

type state

type move

val step : state * move -> state

val moves : state -> move Seq.seq

val score : state -> int

We defined mutually recursive functions

F : state -> int


G : state -> int
that compute the best possible outcome

from the viewpoint of Player1 and Player2
assuming each tries for best results

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?

Use induction as ever!


Theorem
Let Game : GAME be terminating.

Then F and G are total functions,

and for all s : state, (F s) = ~(G s).
Proof? By induction on duration(s).
Int.min ~v1,,~vn = ~ Int.max v1,,vn

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?

Assume that for all s:state,



moves1(s) is a permutation of moves2(s).

Let F1,G1 use moves1 and F2,G2 use moves2.

Show that for all s:state F1(s) = F2(s)



and G1(s) = G2(s)
Int.max, Int.min

are associative and commutative

cost analysis
Suppose at most k moves are possible,
from any state
reachable from s

Let W (d) be the work of F(s)


F

and SF(d) be the span of F(s)


when s is a state with duration d
WF(d) = WG(d)

SF(d) = SG(d)

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 )

Standard ML of New Jersey



runs sequentially,

so thats why F 30 takes so long!

reflection
The order in which moves are enumerated
does not affect the extensional behavior
of F and G

But may have drastic effect on efficiency


(we will see this later!)

picking moves
For Me, a best move is one that leads to the
maximum outcome

type edge = move * int!


!
fun max_edge ((m1,v1),(m2,v2)) =!
if v1 < v2 then (m2,v2) else (m1,v1)!
!
fun maxbest (M : edge Seq.seq) = reduce1 max_edge M!
!
fun Player1 s = !
let!
val M = moves s!
val (m,_) = maxbest (map (fn m => (m, G(step(s, m)))) M)!
in!
m!
end!

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

- structure MaxiNim = MaxiMe(Nim);



structure MaxiNim : PLAYER

!

- 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:

use minimax up to depth d



use heuristic function to guess outcome
at depth > d

estimate : state -> int

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

fun F (s: state, d:int) : int = !


let!
F, G : state * int -> int
val M = moves s!
in!
if (null M) then (score s) else !
if d=0 then (estimate s) else!
reduce1 Int.max (map (fn m => G(step(s, m), d-1)) M)!
end!
and!
G (s, d) =!
let !
val M = moves s!
in !
if (null M) then ~(score s) else !
if d=0 then ~(estimate s) else!
reduce1 Int.min (map (fn m => F(step(s, m), d-1)) M)!
end

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

Prove that for all n0,

F(4n) = 1

F(4n+1) = ~1

F(4n+2) = 1

F(4n+3) = 1

structure SmartNim : EST_GAME =



struct
structure Game = Nim

fun estimate k =

if (k mod 4 = 1) then ~1 else 1

end

- structure GeniusNim =

BoundedMaxPlayer(structure E = SmartNim

and B = struct val depth = 1 end);

!

structure GeniusNim : PLAYER



!

- 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!)

If the depth bound is large enough,


bounded minimax is perfect

If d duration(s), then F(s, d) = F(s)

If the estimation function is perfect,

bounded minimax to any depth is perfect


If F = estimate, then F(s, d) = F(s)

referee
A referee takes 2 players for the same game
and alternates them from a start state,
until the game is over

Produces a list of the game states



Prints a string summarizing the outcome
1. e4 e5!
2. Qh5?! Nc6!
3. Bc4 Nf6??!
4. Qxf7# 10!
Deep Blue wins!!!

reflection
We used a very simple GAME signature

Could have added extra features

print states and moves

Could have allowed for more general kind


of outcome or estimation, e.g.

score, estimate : state -> real

Could have used opaque ascription to limit


visibility and prevent cheating players...

Could design referee to handle exceptions...

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

Too much is hidden!

functor Generous(E : EVENT) : 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 handle _ => [ ]!
end!
fun run s = loop (E.Player1, E.Player2) s!
end

What happens?

so far
Games, players

Minimax (unbounded, bounded)

Efficiency is a concern

even bounded minimax may waste time
searching fruitlessly...

Anda mungkin juga menyukai