gnugo-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[gnugo-devel] GTP Tcl integration (incl toy global alpha-beta)


From: Douglas Ridgway
Subject: [gnugo-devel] GTP Tcl integration (incl toy global alpha-beta)
Date: Thu, 26 Aug 2004 14:02:26 -0600 (MDT)

GTP looks enough like Tcl ("command arg arg..." returns result) that an
integration might be useful. The Tcl script gtp.tcl (included below) does
this. After including it in a tclsh, and attaching a GTP engine with
gtp_init (the engine does all the work), you can intermix GTP and Tcl
almost freely.  It should be straightforward to work with other Tcl
packages (eg Tk for building a UI, network stuff, gluing to other systems
with Tcl interfaces, etc.) Applications include debugging, regression, and
building metamachines. A nice feature is that anything built this way
could work with any GTP compatible engine, although I've only tried it
with Gnu Go.

Some usage examples (I hope line wrapping isn't too messed up):

Here's trivial metamachine, useful for adding Tcl capabilities inside of
GTP clients like GoGui:

----
#!/usr/bin/tclsh

source gtp.tcl

gtp_init   # optional arg is path to engine, defaults to "gnugo --gtp"
start_loop # begins an interactive loop, necessary if the script is to run 
           # independently

----

Every GTP command becomes a Tcl command, and returns its result in the
usual way. See how the GTP commands all_legal and play get used in this
script, a random player metamachine: 

---- 
#!/usr/bin/tclsh

source gtp.tcl

proc genmove {color} {
    set ml [all_legal $color]
    if {[llength $ml] == 0} {
        set move PASS
    } else {
        set move [lindex $ml [expr {int(rand()*[llength $ml])}]]
    }
    play $color $move
    return $move
}

gtp_init
start_loop
----

With a scripting language, we could do complex logic in regression 
testing, like this example that checks the randomness of the move 
distribution:

----

boardsize 9
expr {srand([clock clicks])}
foreach x {E5 F5 G5 F6 G6 G7} {
    set move($x) 0
}
for {set i 0} {$i<100} {incr i} {
    set m [gg_genmove B [expr int(rand()*(1<<30))]]
    incr move($m)
}
if {$move(E5) < 10 || $move(E5) > 50} {
    puts "E5 distribution bad: $move(E5) / 100"
}
----

Here's a minimally featured twogtp function, showing how to attach and
work with multiple engines simultaneously:

----
proc twogtp {blackcmd whitecmd numgames {boardsize 19}} {
    set bw {}
    set ww {}
    set nr {}
    for {set i 0} {$i<$numgames} {incr i} {
        set black [gtp_init $blackcmd]
        set white [gtp_init $whitecmd]
        gtp_put $black boardsize $boardsize
        gtp_put $white boardsize $boardsize
        for {set bm {}} {$bm != "PASS" || $wm != "PASS"} {} {
            set bm [gtp_put $black genmove b]
            gtp_put $white play b $bm
            set wm [gtp_put $white genmove w]
            gtp_put $black play w $wm
        }
        set score [gtp_put $white score final]
        if {$score > 0} {
            lappend bw $sc
        } elseif {$score < 0} {
            lappend ww $sc
        } else {
            lappend nr $sc
        }
        catch {gtp_put $black quit}
        catch {gtp_put $white quit}
    }
    return [list $bw $ww $nr]
}

----

Here's a function which scores all intermediate positions in a game:

proc scoresgf {sgf} {
    if {[regexp {SZ\[([0-9]+)\]} $sgf {} sz]} {
        boardsize $sz
    } else {
        boardsize 19
    }
    while {[regexp {;[^;]*} $sgf node]} {
        regsub {;[^;]*} $sgf {} sgf
        if {[regexp {(B|W)\[([a-t][a-t])\]} $node {} color coord]} {
            play $color [sgf2coord $coord]
            lappend s [score $color]
        }
    }
    return $s
}

And finally, an ill-tested alpha-beta metamachine, inspired by SlugGo's
successes:


proc negascore {color} {
    return [expr {(2*[isBlack $color]-1)*[score $color]}]
}

proc eval_pos {color {depth 2} {width 5} {lower -999} {upper +999}} {
    if {$depth == 0 || [gg_genmove $color] == "PASS"} {
        return [negascore $color]
    }
    set best -999
    foreach {m v} [lrange [top_moves] 0 [expr {2*$width-1}]] {
        play $color $m
        set r [expr {-[eval_pos [other $color] [expr {$depth-1}] $width \
[expr {-$upper}] [expr {-$lower}]]}]
        undo
        if {$r>$best} {
            set best $r
        }
        if {$best > $lower} {
            set lower $best
        }
        if {$best > $upper} {
            break;
        }
    }
    return $best
}  

proc genmove {color} {
    global topwidth subwidth depth
    if {[gg_genmove $color] == "PASS"} {
        return PASS
    }
    set movelist {{PASS -999 -999}}
    foreach {m v} [lrange [top_moves] 0 [expr {2*$topwidth-1}]] {
        play $color $m
        set r [expr {-[eval_pos [other $color] $depth $subwidth]}]
        undo
        lappend movelist [list $m $v $r]
        log $movelist
    }
    set movelist [lsort -real -decreasing -index 2 $movelist]
    set m [lindex [lindex $movelist 0] 0]
    play $color $m
    return $m
}

You need to wrap it like the random move metamachine, and set topwidth, 
subwidth, and depth. Try 5, 1 and 7, maybe at level 1.

It has lots of issues (error handling, eg), and makes me want more from
GTP (introspection, eg.) but still, I'd be interested in hearing if this
is useful to anyone. All comments appreciated.

doug.


----

Here's gtp.tcl:

----
#!/usr/bin/tclsh

# open gtp engine process and init important variables
# multiple engines can be used
# by default, last engine initted is talked to for commands
proc gtp_init {{engine "gnugo --mode gtp"}} {
    global _gg
    set _gg [open "| $engine" RDWR]
    
    # default hook every command known
    
    foreach x [gnugo list_commands] {
        if {[info commands $x] ==  ""} {
            proc $x args "
            return \[eval gnugo $x \$args\]
            "
        }
    }  
    boardsize 19
    return $_gg
}

set _id {}
set tcl_prompt1 {}

# basic procs

proc gtp_format {result} {
  global _id
  return "=$_id $result\n"
}

proc gtp_error {errmsg} {
  global _id
  return "?$_id $errmsg\n"
}

#
# send command to gtp engine, return result
#
proc gtp_put {engine cmd args} {
  puts $engine [concat $cmd $args]
  flush $engine
  set rr {}
  set t [time {
      for {set r 1} {$r != {}} {} {
          set r [gets $engine]
          set rr "$rr\n$r"
      }  
  }]
  set rr [string trim $rr]
  if {![regexp {^=([0-9]*)(.*)} $rr {} {} r]} {
# should trim the ? off?
     error "gtp_put \"$engine $cmd $args\" gave error \"$rr\""
  }
  set r [string trim $r]

#  puts [list $cmd $args $rr]
#  return "$t: $rr"
  return $r
}

# use default engine
proc gnugo {cmd args} {
  global _gg
  return [eval gtp_put $_gg $cmd $args]
}


# and setup id handling

catch { rename unknown _unknown }
proc unknown args {
    global _id 
    if {[string is integer [lindex $args 0]]} {
        set _id [lindex $args 0]
        uplevel [lrange $args 1 end]
    } else {
        uplevel _unknown $args
    }
}


####
#
# default rehooks 

proc quit {} {
    gnugo quit
    exit
}

proc name {} {
  return "GTP.tcl script running [gnugo name] v. [gnugo version]"
}

proc version {} {
  return "0.1"
}

proc set_random_seed {seed} {
  expr {srand($seed)}
  gnugo set_random_seed $seed
}

# GTP doesn't allow boardsize inquiries, so hook everything which can 
# change
# it, and save a local copy

proc loadsgf {file args} {
    set pre [get_boardsize]
    set f [open $file]
    set s [read $f]
    close $f
    if {![regexp {SZ\[([0-9]+)\]} $s {} sz]} {
        set sz 19
    }
    boardsize $sz
    gnugo loadsgf $file $args
}

proc get_boardsize {} {
  global _boardsize
  return $_boardsize
}

proc boardsize {sz} {
  global _boardsize
  set _boardsize $sz
  gnugo boardsize $sz
}

# some useful functions

proc score {color {type estimate}} {
    switch $type {
        estimate {
            set s [estimate_score]
        }
        final {
            set s [final_score]
        }
    }
    if {![regexp {(B|W)\+([0-9]+\.[0-9]*)} $s {} winner score]} {
        error "can't parse score"
    }
    if {![isBlack $winner]} {
        return -$score
    } else {
        return $score
    }
}

proc isBlack {color} {
    if {[string match -nocase b* $color]} {
        return 1
    } elseif {[string match -nocase w* $color]} {
        return 0
    } else {
        error "$color is neither b or w"
    }
}

proc other {color} {
    if {[isBlack $color]} {
        return W
    }    else {
        return B
    }
}


proc sgf2coord {c} {
    global _boardsize
    set c [string tolower $c]
    binary scan $c cc j i
    binary scan "a" c a
   foreach v {i j} {
       set $v [expr $$v+1-$a]
   }
   if {$j>8} {
       set X [binary format c [expr $a+$j]]
   } else {
     set X [binary format c [expr $a+$j-1]]
   }
   return "$X[expr {$_boardsize+1-$i}]"
}


# a procedure to start interactive command loop, and never return
# 
proc start_loop {} {
    global tcl_interactive
    # it's unnecessary in an interactive shell, because there's already a 
# loop
    if {!$tcl_interactive} {
        while 1 {
            if {[catch {puts [gtp_format [eval [gets stdin]]]} err]} {
                puts [gtp_error $err]
            }
            set _id {}
        }
    }
}






reply via email to

[Prev in Thread] Current Thread [Next in Thread]