shithub: purgatorio

ref: 60ecd07e6d3f5786c8723dc9172c35d580fdadc8
dir: /appl/wm/reversi.b/

View raw version
implement Reversi;

#
# Copyright © 2000 Vita Nuova Limited. All rights reserved.
#

include "sys.m";
	sys: Sys;
include "draw.m";
	draw: Draw;
	Point, Rect, Image, Font, Context, Screen, Display: import draw;
include "tk.m";
	tk: Tk;
	Toplevel: import tk;
include "tkclient.m";
	tkclient: Tkclient;
include "daytime.m";
	daytime: Daytime;
include "rand.m";
	rand: Rand;

# adtize and modularize

stderr: ref Sys->FD;

Reversi: module 
{
	init: fn(ctxt: ref Draw->Context, argv: list of string);
};

nosleep, printout, auto: int;
display: ref Draw->Display;

init(ctxt: ref Draw->Context, argv: list of string)
{
	sys = load Sys Sys->PATH;
	draw = load Draw Draw->PATH;
	tk = load Tk Tk->PATH;
	tkclient = load Tkclient Tkclient->PATH;
	tkclient->init();
	daytime = load Daytime Daytime->PATH;
	rand = load Rand Rand->PATH;

	argv = tl argv;
	while(argv != nil){
		s := hd argv;
		if(s != nil && s[0] == '-'){
			for(i := 1; i < len s; i++){
				case s[i]{
					'a' => auto = 1;
					'p' => printout = 1;
					's' => nosleep = 1;
				}
			}
		}
		argv = tl argv;
	}
	stderr = sys->fildes(2);
	rand->init(daytime->now());
	daytime = nil;

	if(ctxt == nil)
		ctxt = tkclient->makedrawcontext();
	display = ctxt.display;
	(win, wmctl) := tkclient->toplevel(ctxt, "", "Reversi", Tkclient->Resize | Tkclient->Hide);
	mainwin = win;
	sys->pctl(Sys->NEWPGRP, nil);
	cmdch := chan of string;
	tk->namechan(win, cmdch, "cmd");
	for(i := 0; i < len win_config; i++)
		cmd(win, win_config[i]);
	fittoscreen(win);
	pid := -1;
	sync := chan of int;
	mvch := chan of (int, int, int);
	initboard();
	setimage();
	spawn game(sync, mvch, 0);
	pid = <- sync;
	tkclient->onscreen(win, nil);
	tkclient->startinput(win, "kbd"::"ptr"::nil);
	lasts := 1;
	for(;;){
		alt{
			s := <-win.ctxt.kbd =>
				tk->keyboard(win, s);
			s := <-win.ctxt.ptr =>
				tk->pointer(win, *s);
			s := <-win.ctxt.ctl or
			s = <-win.wreq =>
				tkclient->wmctl(win, s);
			c := <- wmctl =>
				case c{
					"exit" =>
						if(pid != -1)
							kill(pid);
						exit;
					* =>
						e := tkclient->wmctl(win, c);
						if(e == nil && c[0] == '!'){
							setimage();
							drawboard();
						}
				}
			c := <- cmdch =>
				(nil, toks) := sys->tokenize(c, " ");
				case hd toks{
					"b1" or "b2" or "b3" =>
						alt{
							mvch <-= (SQUARE, int hd tl toks, int hd tl tl toks) => lasts = 1;
							* => ;
						}
					"bh" or "bm" or "wh" or "wm" =>
						col := BLACK;
						knd := HUMAN;
						if((hd toks)[0] == 'w')
							col = WHITE;
						if((hd toks)[1] == 'm')
							knd = MACHINE;
						kind[col] = knd;
					"blev" or "wlev" =>
						col := BLACK;
						e := "be";
						if((hd toks)[0] == 'w'){
							col = WHITE;
							e = "we";
						}
						sk := int cmd(win, ".f0." + e + " get");
						if(sk > MAXPLIES)
							sk = MAXPLIES;
						if(sk >= 0)
							skill[col] = sk;
					"last" =>
						alt{
							mvch <-= (REPLAY, lasts, 0) => lasts++;
							* => ;
						}
					* =>
						;
				}
			<- sync =>
				pid = -1;
				# exit;
				spawn game(sync, mvch, 0);
				pid = <- sync;
		}
	}
}

SQUARE, REPLAY: con iota;

WIDTH: con 400;
HEIGHT: con 400;

SZB: con 8;		# must be even
SZF: con SZB+2;
MC1: con SZB/2;
MC2: con MC1+1;
PIECES: con SZB*SZB;
SQUARES: con PIECES-4;
MAXMOVES: con 3*PIECES/2;
NOMOVE: con SZF*SZF - 1;

BLACK, WHITE, EMPTY, BORDER: con iota;
MACHINE, HUMAN: con iota;
SKILLB : con 6;
SKILLW : con 0;
MAXPLIES: con 6;

moves: array of int;
board: array of array of int;	# for display
brd: array of array of int;		# for calculations
val: array of array of int;
order: array of (int, int);
pieces: array of int;
value: array of int;
kind: array of int;
skill: array of int;
name: array of string;

mainwin: ref Toplevel;
brdimg: ref Image;
brdr: Rect;
brdx, brdy: int;

black, white, green: ref Image;

movech: chan  of (int, int, int);

setimage()
{
	brdw := int tk->cmd(mainwin, ".p cget -actwidth");
	brdh := int tk->cmd(mainwin, ".p cget -actheight");
#	if (brdw > display.image.r.dx())
#		brdw = display.image.r.dx() - 4;
#	if (brdh > display.image.r.dy())
#		brdh = display.image.r.dy() - 40;

	brdr = Rect((0,0), (brdw, brdh));
	brdimg = display.newimage(brdr, display.image.chans, 0, Draw->White);
	if(brdimg == nil)
		fatal("not enough image memory");
	tk->putimage(mainwin, ".p", brdimg, nil);
}

game(sync: chan of int, mvch: chan of (int, int, int), again: int)
{
	sync <-= sys->pctl(0, nil);
	movech = mvch;
	initbrd();
	drawboard();
	if(again)
		replay(moves);
	else
		play();
	sync <-= 0;
}

ordrect()
{
	i, j : int;

	n := 0;
	for(i = 1; i <= SZB; i++){
		for(j = 1; j <= SZB; j++){
			if(i < SZB/2 || j < SZB/2 || i > SZB/2+1 || j > SZB/2+1)
				order[n++] = (i, j);
		}
	}
	for(k := 0; k < SQUARES-1; k++){
		for(l := k+1; l < SQUARES; l++){
			(i, j) = order[k];
			(a, b) := order[l];
			if(val[i][j] > val[a][b])
				(order[k], order[l]) = (order[l], order[k]);
		}
	}
}

initboard()
{
	i, j, k: int;

	moves = array[MAXMOVES+1] of int;
	board = array[SZF] of array of int;
	brd = array[SZF] of array of int;
	for(i = 0; i < SZF; i++){
		board[i] = array[SZF] of int;
		brd[i] = array[SZF] of int;
	}
	val = array[SZF] of array of int;
	s := -pow(-1, SZB/2);
	for(i = 0; i < SZF; i++){
		val[i] = array[SZF] of int;
		val[i][0] = val[i][SZF-1] = 0;
		for(j = 1; j <= SZB; j++){
			for(k = SZB/2; k > 0; k--){
				if(i == k || i == SZB+1-k || j == k || j == SZB+1-k){
					val[i][j] = s*pow(-7, SZB/2-k);
					break;
				}
			}
		}
	}
	order = array[SQUARES] of (int, int);
	ordrect();
	pieces = array[2] of int;
	value = array[2] of int;
	kind = array[2] of int;
	kind[BLACK] = MACHINE;
	if(auto)
		kind[WHITE] = MACHINE;
	else
		kind[WHITE] = HUMAN;
	skill = array[2] of int;
	skill[BLACK] = SKILLB;
	skill[WHITE] = SKILLW;
	name = array[2] of string;
	name[BLACK] = "black";
	name[WHITE] = "white";
	black = display.color(Draw->Black);
	white = display.color(Draw->White);
	green = display.color(Draw->Green);
}

initbrd()
{
	i, j: int;

	for(i = 0; i < SZF; i++)
		for(j = 0; j < SZF; j++)
			brd[i][j] = EMPTY;
	for(i = 0; i < SZF; i++)
		brd[i][0] = brd[i][SZF-1] = BORDER;
	for(j = 0; j< SZF; j++)
		brd[0][j] = brd[SZF-1][j] = BORDER;
	brd[MC1][MC1] = brd[MC2][MC2] = BLACK;
	brd[MC1][MC2] = brd[MC2][MC1] = WHITE;
	for(i = 0; i < SZF; i++)
		for(j = 0; j < SZF; j++)
			board[i][j] = brd[i][j];
	pieces[BLACK] = pieces[WHITE] = 2;
	value[BLACK] = value[WHITE] = -2;
}

plays := 0;
bscore := 0;
wscore := 0;
bwins := 0;
wwins := 0;

play()
{
	n := 0;
	for(i := 0; i <= MAXMOVES; i++)
		moves[i] = NOMOVE;
	if(plays&1)
		(first, second) := (WHITE, BLACK);
	else
		(first, second) = (BLACK, WHITE);
	if(printout)
		sys->print("%d\n", first);
	moves[n++] = first;
	m1 := m2 := 1;
	for(;;){
		if(pieces[BLACK]+pieces[WHITE] == PIECES)
			break;
		m2 = m1;
		m1 = move(first, second);
		if(printout)
			sys->print("%d\n", m1);
		moves[n++] = m1;
		if(!m1 && !m2)
			break;
		(first, second) = (second, first);
	}
	if(auto)
		sys->print("score: %d-%d\n", pieces[BLACK], pieces[WHITE]);
	bscore += pieces[BLACK];
	wscore += pieces[WHITE];
	if(pieces[BLACK] > pieces[WHITE])
		bwins++;
	else if(pieces[BLACK] < pieces[WHITE])
		wwins++;
	plays++;
	if(auto)
		sys->print("	black: %d white: %d draw: %d total: (%d-%d)\n", bwins, wwins, plays-bwins-wwins, bscore, wscore);
	puts(sys->sprint("black %d:%d white", pieces[BLACK], pieces[WHITE]));
	sleep(2000);
	puts(sys->sprint("black %d:%d white", bwins, wwins));
	sleep(2000);
}

replay(moves: array of int)
{
	n := 0;
	first := moves[n++];
	second := BLACK+WHITE-first;
	m1 := m2 := 1;
	while (pieces[BLACK]+pieces[WHITE] < PIECES){
		m2 = m1;
		m1 = moves[n++];
		if(m1 == NOMOVE)
			break;
		if(m1 != 0)
			makemove(m1/SZF, m1%SZF, first, second, 1, 0);
		if(!m1 && !m2)
			break;
		(first, second) = (second, first);
	}
	# sys->print("score: %d-%d\n", pieces[BLACK], pieces[WHITE]);
}

lastmoves(p: int, moves: array of int)
{
	initbrd();
	k := MAXMOVES+1;
	for(i := 0; i <= MAXMOVES; i++){
		if(moves[i] == NOMOVE){
			k = i;
			break;
		}
	}
	if(k-p < 1)
		p = k-1;
	for(i = k-p; i < k; i++)
		if(moves[i] == 0)
			p++;
	if(k-p < 1)
		p = k-1;
	n := 0;
	me := moves[n++];
	you := BLACK+WHITE-me;
	while(n < k-p){
		m := moves[n++];
		if(m != 0)
			makemove(m/SZF, m%SZF, me, you, 1, 1);
		(me, you) = (you, me);
	}
	for(i = 0; i < SZF; i++)
		for(j := 0; j < SZF; j++)
			board[i][j] = brd[i][j];
	drawboard();
	sleep(1000);
	while(n < k){
		m := moves[n++];
		if(m != 0)
			makemove(m/SZF, m%SZF, me, you, 1, 0);
		if(n < k)
			sleep(500);
		(me, you) = (you, me);
	}
}

move(me: int, you: int): int
{
	if(kind[me] == MACHINE){
		puts("machine " + name[me] + " move");
		m := genmove(me, you);
		if(!m){
			puts("machine " + name[me] + " cannot go");
			sleep(2000);
		}
		return m;
	}
	else{
		m, n: int;

		mvs := findmoves(me, you);
		if(mvs == nil){
			puts("human " + name[me] + " cannot go");
			sleep(2000);
			return 0;
		}
		for(;;){
			puts("human " + name[me] + " move");
			(m, n) = getmove();
			if(m < 1 || n < 1 || m > SZB || n > SZB)
				continue;
			if(brd[m][n] == EMPTY)
				(valid, nil) := makemove(m, n, me, you, 0, 0);
			else
				valid = 0;
			if(valid)
				break;
			puts("illegal move");
			sleep(2000);
		}
		makemove(m, n, me, you, 1, 0);
		return m*SZF+n;
	}
}

fullsrch: int;

genmove(me: int, you: int): int
{
	m, n, v: int;

	mvs := findmoves(me, you);
	if(mvs == nil)
		return 0;
	if(skill[me] == 0){
		l := len mvs;
		r := rand->rand(l);
		# r = 0;
		while(--r >= 0)
			mvs = tl mvs;
		(m, n) = hd mvs;
	}
	else{
		plies := skill[me];
		left := PIECES-(pieces[BLACK]+pieces[WHITE]);
		if(left < plies)		# limit search
			plies = left;
		else if(left < 2*plies)	# expand search to end
			plies = left;
		else{				# expand search nearer end of game
			k := left/plies;
			if(k < 3)
				plies = ((k+2)*plies)/(k+1);
		}
		fullsrch = plies == left;
		visits = leaves = 0;
		(v, (m, n)) = minimax(me, you, plies, ∞, 1);
		if(0){
		# if((m==2&&n==2&&brd[1][1]!=BLACK) ||
		#    (m==2&&n==7&&brd[1][8]!=BLACK) ||
		#    (m==7&&n==2&&brd[8][1]!=BLACK) ||
		#    (m==7&&n==7&&brd[8][8]!=BLACK)){
			while(mvs != nil){
				(a, b) := hd mvs;
				(nil, sqs) := makemove(a, b, me, you, 1, 1);
				(v0, nil) := minimax(you, me, plies-1, ∞, 1);
				sys->print("	(%d, %d): %d\n", a, b, v0);
				undomove(a, b, me, you, sqs);
				mvs = tl mvs;
			}
			if(!fullsrch){
				sys->print("best move is %d, %d\n", m, n);
				kind[WHITE] = HUMAN;
			}
		}
		if(auto)		
			sys->print("eval = %d plies=%d goes=%d visits=%d\n", v, plies, len mvs, leaves);
	}
	makemove(m, n, me, you, 1, 0);
	return m*SZF+n;
}

findmoves(me: int, you: int): list of (int, int)
{
	mvs: list of (int, int);

	for(k := 0; k < SQUARES; k++){
		(i, j) := order[k];
		if(brd[i][j] == EMPTY){
			(valid, nil) := makemove(i, j, me, you, 0, 0);
			if(valid)
				mvs = (i, j) :: mvs;
		}
	}
	return mvs;
}

makemove(m: int, n: int, me: int, you: int, move: int, gen: int): (int, list of (int, int))
{
	sqs: list of (int, int);

	if(move){
		pieces[me]++;
		value[me] += val[m][n];
		brd[m][n] = me;
		if(!gen){
			board[m][n] = me;
			drawpiece(m, n, me, 1);
			panelupdate();
			sleep(1000);
		}
	}
	valid := 0;
	for(i := -1; i < 2; i++){
		for(j := -1; j < 2; j++){
			if(i != 0 || j != 0){
				v: int;

				(v, sqs) = dirmove(m, n, i, j, me, you, move, gen, sqs);
				valid |= v;
				if (valid && !move)
					return (1, sqs);
			}
		}
	}
	if(!valid && move)
		fatal(sys->sprint("bad makemove call (%d, %d)", m, n));
	return (valid, sqs);
}

dirmove(m: int, n: int, dx: int, dy: int, me: int, you: int, move: int, gen: int, sqs: list of (int, int)): (int, list of (int, int))
{
	p := 0;
	m += dx;
	n += dy;
	while(brd[m][n] == you){
		m += dx;
		n += dy;
		p++;
	}
	if(p > 0 && brd[m][n] == me){
		if(move){
			pieces[me] += p;
			pieces[you] -= p;
			m -= p*dx;
			n -= p*dy;
			while(--p >= 0){
				brd[m][n] = me;
				value[me] += val[m][n];
				value[you] -= val[m][n];
				if(gen)
					sqs = (m, n) :: sqs;
				else{
					board[m][n] = me;
					drawpiece(m, n, me, 0);
					# sleep(500);
					panelupdate();
				}
				m += dx;
				n += dy;
			}
		}
		return (1, sqs);
	}
	return (0, sqs);
}			

undomove(m: int, n: int, me: int, you: int, sqs: list of (int, int))
{
	brd[m][n] = EMPTY;
	pieces[me]--;
	value[me] -= val[m][n];
	for(; sqs != nil; sqs = tl sqs){
		(x, y) := hd sqs;
		brd[x][y] = you;
		pieces[me]--;
		pieces[you]++;
		value[me] -= val[x][y];
		value[you] += val[x][y];
	}
}

getmove(): (int, int)
{
	k, x, y: int;

	(k, x, y) = <- movech;
	if(k == REPLAY){
		lastmoves(x, moves);
		return getmove();
	}
	return (x/brdx+1, y/brdy+1);
}

drawboard()
{
	brdx = brdr.dx()/SZB;
	brdy = brdr.dy()/SZB;
	brdimg.draw(brdr, green, nil, (0, 0));
	for(i := 1; i < SZB; i++)
		drawline(lmap(i, 0), lmap(i, SZB));
	for(j := 1; j < SZB; j++)
		drawline(lmap(0, j), lmap(SZB, j));
	for(i = 1; i <= SZB; i++){
		for(j = 1; j <= SZB; j++){
			if (board[i][j] == BLACK || board[i][j] == WHITE)
				drawpiece(i, j, board[i][j], 0);
		}
	}
	panelupdate();
}

drawpiece(m, n, p, flash: int)
{
	if(p == BLACK)
		src := black;
	else
		src = white;
	if(0 && flash && kind[p] == MACHINE){
		for(i := 0; i < 4; i++){
			brdimg.fillellipse(cmap(m, n), 3*brdx/8, 3*brdy/8, src, (0, 0));
			panelupdate();
			sys->sleep(250);
			brdimg.fillellipse(cmap(m, n), 3*brdx/8, 3*brdy/8, green, (0, 0));
			panelupdate();
			sys->sleep(250);
		}
	}
	brdimg.fillellipse(cmap(m, n), 3*brdx/8, 3*brdy/8, src, (0, 0));
}

panelupdate()
{
	tk->cmd(mainwin, sys->sprint(".p dirty %d %d %d %d", brdr.min.x, brdr.min.y, brdr.max.x, brdr.max.y));
	tk->cmd(mainwin, "update");
}

drawline(p0, p1: Point)
{
	brdimg.line(p0, p1, Draw->Endsquare, Draw->Endsquare, 0, brdimg.display.black, (0, 0));
}

cmap(m, n: int): Point
{
	return brdr.min.add((m*brdx-brdx/2, n*brdy-brdy/2));
}

lmap(m, n: int): Point
{
	return brdr.min.add((m*brdx, n*brdy));
}

∞: con (1<<30);
MAXVISITS: con 1024;

visits, leaves : int;

minimax(me: int, you: int, plies: int, αβ: int, mv: int): (int, (int, int))
{
	if(plies == 0){
		visits++;
		leaves++;
		if(visits == MAXVISITS){
			visits = 0;
			sys->sleep(0);
		}
		return (eval(me, you), (0, 0));
	}
	mvs := findmoves(me, you);
	if(mvs == nil){
		if(mv)
			(v, nil) := minimax(you, me, plies, ∞, 0);
		else
			(v, nil) = minimax(you, me, plies-1, ∞, 0);
		return (-v, (0, 0));
	}
	bestv := -∞;
	bestm := (0, 0);
	e := 0;
	for(; mvs != nil; mvs = tl mvs){
		(m, n) := hd mvs;
		(nil, sqs) := makemove(m, n, me, you, 1, 1);
		(v, nil) := minimax(you, me, plies-1, -bestv, 1);
		v = -v;
		undomove(m, n, me, you, sqs);
		if(v > bestv || (v == bestv && rand->rand(++e) == 0)){
			if(v > bestv)
				e = 1;
			bestv = v;
			bestm = (m, n);
			if(bestv >= αβ)
				return (∞, (0, 0));
		}
	}
	return (bestv, bestm);
}
	
eval(me: int, you: int): int
{
	d := pieces[me]-pieces[you];
	if(fullsrch)
		return d;
	n := pieces[me]+pieces[you];
	v := 0;
	for(i := 1; i <= SZB; i += SZB-1)
		for(j := 1; j <= SZB; j += SZB-1)
			v += line(i, j, me, you);
	return (PIECES-n)*(value[me]-value[you]+v) + n*d;
}

line(m: int, n: int, me: int, you: int): int
{
	if(brd[m][n] == EMPTY)
		return 0;
	dx := dy := -1;
	if(m == 1)
		dx = 1;
	if(n == 1)
		dy = 1;
	return line0(m, n, 0, dy, me, you) +
		   line0(m, n, dx, 0, me, you) +
		   line0(m, n, dx, dy, me, you);
}

line0(m: int, n: int, dx: int, dy: int, me: int, you: int): int
{
	v := 0;
	p := brd[m][n];
	i := val[1][1];
	while(brd[m][n] == p){
		v += i;
		m += dx;
		n += dy;
	}
	if(p == you)
		return -v;
	if(p == me)
		return v;
	return v;
}

pow(n: int, m: int): int
{
	p := 1;
	while(--m >= 0)
		p *= n;
	return p;
}

fatal(s: string)
{
	sys->fprint(stderr, "%s\n", s);
	exit;
}

sleep(t: int)
{
	if(nosleep)
		sys->sleep(0);
	else
		sys->sleep(t);
}

kill(pid: int): int
{
	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
	if(fd == nil)
		return -1;
	if(sys->write(fd, array of byte "kill", 4) != 4)
		return -1;
	return 0;
}

cmd(top: ref Toplevel, s: string): string
{
	e := tk->cmd(top, s);
	if (e != nil && e[0] == '!')
		sys->fprint(stderr, "reversi: tk error on '%s': %s\n", s, e);
	return e;
}

# swidth: int;
# sfont: ref Font;

# gettxtattrs()
# {
#	swidth = int cmd(mainwin, ".f1.txt cget -width");	# always initial value ?
#	f := cmd(mainwin, ".f1.txt cget -font");
#	sfont = Font.open(brdimg.display, f);
# }
	
puts(s: string)
{
	# while(sfont.width(s) > swidth)
	#	s = s[0: len s -1];
	cmd(mainwin, ".f1.txt configure -text {" + s + "}");
	cmd(mainwin, "update");
}

fittoscreen(win: ref Tk->Toplevel)
{
	Point: import draw;
	if (display.image == nil)
		return;
	r :=  display.image.r;
	scrsize := Point(r.dx(), r.dy());
	bd := int cmd(win, ". cget -bd");
	winsize := Point(int cmd(win, ". cget -actwidth") + bd * 2, int cmd(win, ". cget -actheight") + bd * 2);
	if (winsize.x > scrsize.x)
		cmd(win, ". configure -width " + string (scrsize.x - bd * 2));
	if (winsize.y > scrsize.y)
		cmd(win, ". configure -height " + string (scrsize.y - bd * 2));
	actr: Rect;
	actr.min = Point(int cmd(win, ". cget -actx"), int cmd(win, ". cget -acty"));
	actr.max = actr.min.add((int cmd(win, ". cget -actwidth") + bd*2,
				int cmd(win, ". cget -actheight") + bd*2));
	(dx, dy) := (actr.dx(), actr.dy());
	if (actr.max.x > r.max.x)
		(actr.min.x, actr.max.x) = (r.max.x - dx, r.max.x);
	if (actr.max.y > r.max.y)
		(actr.min.y, actr.max.y) = (r.max.y - dy, r.max.y);
	if (actr.min.x < r.min.x)
		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
	if (actr.min.y < r.min.y)
		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
	cmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
	cmd(win, "update");
}
					
win_config := array[] of {
	"frame .f",
	"button .f.last -text {last move} -command {send cmd last}",
	"menubutton .f.bk -text Black -menu .f.bk.bm",
	"menubutton .f.wk -text White -menu .f.wk.wm",
	"menu .f.bk.bm",
	".f.bk.bm add command -label Human -command { send cmd bh }",
	".f.bk.bm add command -label Machine -command { send cmd bm }",
	"menu .f.wk.wm",
	".f.wk.wm add command -label Human -command { send cmd wh }",
	".f.wk.wm add command -label Machine -command { send cmd wm }",
	"pack .f.bk -side left",
	"pack .f.wk -side right",
	"pack .f.last -side top",

	"frame .f0",
	"label .f0.bl -text {Black level}",
	"label .f0.wl -text {White level}",
	"entry .f0.be -width 32",
	"entry .f0.we -width 32",
	".f0.be insert 0 " + string SKILLB,
	".f0.we insert 0 " + string SKILLW,
	"pack .f0.bl -side left",
	"pack .f0.be -side left",
	"pack .f0.wl -side right",
	"pack .f0.we -side right",

	"frame .f1",
	"label .f1.txt -text { } -width " + string WIDTH,
	"pack .f1.txt -side top -fill x",

	"panel .p -width " + string WIDTH + " -height " + string HEIGHT,

	"pack .f -side top -fill x",
	"pack .f0 -side top -fill x",
	"pack .f1 -side top -fill x",
	"pack .p -side bottom -fill both -expand 1",
	"pack propagate . 0",

	"bind .p <Button-1> {send cmd b1 %x %y}",
	"bind .p <Button-2> {send cmd b2 %x %y}",
	"bind .p <Button-3> {send cmd b3 %x %y}",
	"bind .f0.be <Key-\n> {send cmd blev}",
	"bind .f0.we <Key-\n> {send cmd wlev}",
	"update",
};