code: purgatorio

ref: 51bcc63cc6bce3f9dab27cf6bd7155512b82c8fa
dir: /appl/spree/clients/bounce.b/

View raw version
implement Clientmod;

# bouncing balls demo.  it uses tk and multiple processes to animate a
# number of balls bouncing around the screen.  each ball has its own
# process; CPU time is doled out fairly to each process by using
# a central monitor loop.

include "sys.m";
	sys: Sys;
include "draw.m";
	draw: Draw;
	Display, Point, Rect, Image: import draw;
include "tk.m";
	tk: Tk;
include "tkclient.m";
	tkclient: Tkclient;
include "math.m";
	math: Math;
include "rand.m";
include "../client.m";

BALLSIZE: con 5;
ZERO: con 1e-6;
π: con Math->Pi;
Maxδ: con π / 4.0;			# max bat angle deflection

Line: adt {
	p, v:		Realpoint;
	s:		real;
	new:			fn(p1, p2: Point): ref Line;
	hittest:		fn(l: self ref Line, p: Point): (Realpoint, real, real);
	intersection:	fn(b: self ref Line, p, v: Realpoint): (int, Realpoint, real, real);
	point:		fn(b: self ref Line, s: real): Point;
};

Realpoint: adt {
	x, y: real;
};

cliquecmds := array[] of {
"canvas .c -bg black",
"bind .c <ButtonRelease-1> {send mouse 0 1 %x %y}",
"bind .c <ButtonRelease-2> {send mouse 0 2 %x %y}",
"bind .c <Button-1> {send mouse 1 1 %x %y}",
"bind .c <Button-2> {send mouse 1 2 %x %y}",
"bind . <Key-b> {send ucmd newball}",
"bind . <ButtonRelease-1> {focus .}",
"bind .Wm_t <ButtonRelease-1> +{focus .}",
"focus .",
"bind .c <Key-b> {send ucmd newball}",
"bind .c <Key-u> {grab release .c}",
"frame .f",
"button .f.b -text {Start} -command {send ucmd start}",
"button .f.n -text {New ball} -command {send ucmd newball}",
"pack .f.b .f.n -side left",
"pack .f -fill x",
"pack .c -fill both -expand 1",
"update",
};

Ballstate: adt {
	owner: int;		# index into member array
	hitobs: ref Obstacle;
	t0: int;
	p, v: Realpoint;
	speed: real;
};

Queue: adt {
	h, t: list of T; 
	put: fn(q: self ref Queue, s: T);
	get: fn(q: self ref Queue): T;
};


Obstacle: adt {
	line: 		ref Line;
	id: 		int;
	isbat: 	int;
	s1, s2: 	real;
	srvid:	int;
	owner:	int;
	new: 	fn(id: int): ref Obstacle;
	config: 	fn(b: self ref Obstacle);
};

Object: adt {
	obstacle: ref Obstacle;
	ballctl: chan of ref Ballstate;
};


Member: adt {
	id: int;
	colour: string;
};

win: ref Tk->Toplevel;

lines: list of ref Obstacle;
lineversion := 0;
memberid: int;
myturn: int;
stderr: ref Sys->FD;
timeoffset := 0;

objects: array of ref Object;
srvobjects: array of ref Obstacle;	# all for lasthit...
members: array of ref Member;

CORNER: con 60;
INSET: con 20;
WIDTH: con 500;
HEIGHT: con 500;

bats: list of ref Obstacle;
mkball: chan of (int, chan of chan of ref Ballstate);
cliquefd: ref Sys->FD;
currentlydragging := -1;
Ballexit: ref Ballstate;
Noobs: ref Obstacle;

nomod(s: string)
{
	sys->fprint(stderr, "bounce: cannot load %s: %r\n", s);
	sys->raise("fail:bad module");
}

client(ctxt: ref Draw->Context, argv: list of string, nil: int)
{
	sys = load Sys Sys->PATH;
	stderr = sys->fildes(2);
	draw = load Draw Draw->PATH;
	math = load Math Math->PATH;
	tk = load Tk Tk->PATH;
	tkclient = load Tkclient Tkclient->PATH;
	if (tkclient == nil)
		nomod(Tkclient->PATH);
	tkclient->init();
	cliquefd = sys->fildes(0);
	Ballexit = ref Ballstate;
	Noobs = Obstacle.new(-1);
	lines = tl lines;		# XXX ahem.

	if (len argv >= 3)		# argv: modname mnt dir ...
		membername = readfile(hd tl argv + "/name");

	sys->pctl(Sys->NEWPGRP, nil);
	wmctl: chan of string;
	(win, wmctl) = tkclient->toplevel(ctxt.screen, nil, "Bounce", 0);
	ucmd := chan of string;
	tk->namechan(win, ucmd, "ucmd");
	mouse := chan of string;
	tk->namechan(win, mouse, "mouse");
	for (i := 0; i < len cliquecmds; i++)
		cmd(win, cliquecmds[i]);
	cmd(win, ".c configure -width 500 -height 500");
	cmd(win, ".c configure -width [.c cget -actwidth] -height [.c cget -actheight]");
	imageinit();

	mch := chan of (int, Point);

	spawn mouseproc(mch);
	mkball = chan of (int, chan of chan of ref Ballstate);
	spawn monitor(mkball);
	balls: list of chan of ref Ballstate;

	spawn updateproc();
	sys->sleep(500);		# wait for things to calm down a little
	cliquecmd("time " + string sys->millisec());

	buts := 0;
	for (;;) alt {
	c := <-wmctl =>
		if (c == "exit")
			sys->write(cliquefd, array[0] of byte, 0);
		tkclient->wmctl(win, c);
	c := <-mouse =>
		(nil, toks) := sys->tokenize(c, " ");
		if ((hd toks)[0] == '1')
			buts |= int hd tl toks;
		else
			buts &= ~int hd tl toks;
		mch <-= (buts, Point(int hd tl tl toks, int hd tl tl tl toks));
	c := <-ucmd =>
		cliquecmd(c);
	}
}

cliquecmd(s: string): int
{
	if (sys->fprint(cliquefd, "%s\n", s) == -1) {
		err := sys->sprint("%r");
		notify(err);
		sys->print("bounce: cmd error on '%s': %s\n", s, err);
		return 0;
	}
	return 1;
}

updateproc()
{
	wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
	spawn updateproc1();
	buf := array[Sys->ATOMICIO] of byte;
	n := sys->read(wfd, buf, len buf);
	sys->print("updateproc process exited: %s\n", string buf[0:n]);
}

updateproc1()
{
	buf := array[Sys->ATOMICIO] of byte;
	while ((n := sys->read(cliquefd, buf, len buf)) > 0) {
		(nil, lines) := sys->tokenize(string buf[0:n], "\n");
		for (; lines != nil; lines = tl lines)
			applyupdate(hd lines);
		cmd(win, "update");
	}
	if (n < 0)
		sys->fprint(stderr, "bounce: error reading updates: %r\n");
	sys->fprint(stderr, "bounce: updateproc exiting\n");
}

UNKNOWN, BALL, OBSTACLE: con iota;

applyupdate(s: string)
{
#	sys->print("bounce: got update %s\n", s);
	(nt, toks) := sys->tokenize(s, " ");
	case hd toks {
	"create" =>
		# create id parentid vis type
		id := int hd tl toks;
		if (id >= len objects) {
			newobjects := array[id + 10] of ref Object;
			newobjects[0:] = objects;
			objects = newobjects;
		}
		objects[id] = ref Object;
	"del" =>
		# del parent start end objid...
		for (toks = tl tl tl tl toks; toks != nil; toks = tl toks) {
			id := int hd toks;
			if (objects[id].obstacle != nil)
				sys->fprint(stderr, "bounce: cannot delete obstructions yet\n");
			else
				objects[id].ballctl <-= Ballexit;
			objects[id] = nil;
		}
	"set" =>
		# set obj attr val
		id := int hd tl toks;
		attr := hd tl tl toks;
		val := tl tl tl toks;
		case attr {
		"state" =>
			# state lasthit owner p.x p.y v.x v.y s time
			state := ref Ballstate;
			(state.hitobs, val) = (srvobj(int hd val), tl val);
			(state.owner, val) = (int hd val, tl val);
			(state.p.x, val) = (real hd val, tl val);
			(state.p.y, val) = (real hd val, tl val);
			(state.v.x, val) = (real hd val, tl val);
			(state.v.y, val) = (real hd val, tl val);
			(state.speed, val) = (real hd val, tl val);
			(state.t0, val) = (int hd val, tl val);
			if (objects[id].ballctl == nil)
				objects[id].ballctl = makeball(id, state);
			else
				objects[id].ballctl <-= state;
		"pos" or "coords" or "owner" or "id" =>
			if (objects[id].obstacle == nil)
				objects[id].obstacle = Obstacle.new(id);
			o := objects[id].obstacle;
			case attr {
			"pos" =>
				(o.s1, val) = (real hd val, tl val);
				(o.s2, val) = (real hd val, tl val);
				o.isbat = 1;
			"coords" =>
				p1, p2: Point;
				(p1.x, val) = (int hd val, tl val);
				(p1.y, val) = (int hd val, tl val);
				(p2.x, val) = (int hd val, tl val);
				(p2.y, val) = (int hd val, tl val);
				o.line = Line.new(p1, p2);
			"owner" =>
				o.owner = hd val;
				if (o.owner == membername)
					bats = o :: bats;
			"id" =>
				o.srvid = int hd val;
				if (o.srvid >= len srvobjects) {
					newobjects := array[id + 10] of ref Obstacle;
					newobjects[0:] = srvobjects;
					srvobjects = newobjects;
				}
				srvobjects[o.srvid] = o;
			}
			if (currentlydragging != id)
				o.config();
		"arenasize" =>
			# arenasize w h
			cmd(win, ".c configure -width " + hd val + " -height " + hd tl val);
		* =>
			if (len attr > 5 && attr[0:5] == "score") {
				# scoreN val
				n := int attr[5:];
				w := ".f." + string n;
				if (!tkexists(w)) {
					cmd(win, "label " + w + "l -text '" + attr);
					cmd(win, "label " + w + " -relief sunken -bd 5 -width 5w");
					cmd(win, "pack " +w + "l " + w + " -side left");
				}
				cmd(win, w + " configure -text {" + hd val + "}");
			} else if (len attr > 6 && attr[0:6] == "member") {
				# memberN id colour
				n := int attr[6:];
				if (n >= len members) {
					newmembers := array[n + 1] of ref Member;
					newmembers[0:] = members;
					members = newmembers;
				}
				p := members[n] = ref Member(int hd val, hd tl val);
				cmd(win, ".c itemconfigure o" + string p.id + " -fill " + p.colour);
				if (p.id == memberid)
					myturn = n;
			}
			else
				sys->fprint(stderr, "bounce: unknown attr '%s'\n", attr);
		}
	"time" =>
		# time offset orig
		now := sys->millisec();
		time := int hd tl tl toks;
		transit := now - time;
		timeoffset = int hd tl toks - transit / 2;
		sys->print("transit time %d, timeoffset: %d\n", transit, timeoffset);
	* =>
		sys->fprint(stderr, "chat: unknown update message '%s'\n", s);
	}
}

tkexists(w: string): int
{
	return tk->cmd(win, w + " cget -bd")[0] != '!';
}

srvobj(id: int): ref Obstacle
{
	if (id < 0 || id >= len srvobjects || srvobjects[id] == nil)
		return Noobs;
	return srvobjects[id];
}

mouseproc(mch: chan of (int, Point))
{
	procname("mouse");
	for (;;) {
		hitbat: ref Obstacle = nil;
		minperp, hitdist: real;
		(buts, p) := <-mch;
		for (bl := bats; bl != nil; bl = tl bl) {
			b := hd bl;
			(normal, perp, dist) := b.line.hittest(p);
			perp = abs(perp);
			
			if ((hitbat == nil || perp < minperp) && (dist >= b.s1 && dist <= b.s2))
				(hitbat, minperp, hitdist) = (b, perp, dist);
		}
		if (hitbat == nil || minperp > 30.0) {
			while ((<-mch).t0)
				;
			continue;
		}
		offset := hitdist - hitbat.s1;
		if (buts & 2)
			(buts, p) = aim(mch, hitbat, p);
		if (buts & 1)
			drag(mch, hitbat, offset);
	}
}


drag(mch: chan of (int, Point), hitbat: ref Obstacle, offset: real)
{
	realtosrv := chan of string;
	dummytosrv := chan of string;
	tosrv := dummytosrv;
	currevent := "";

	currentlydragging = hitbat.id;

	line := hitbat.line;
	batlen := hitbat.s2 - hitbat.s1;

	cvsorigin := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
	spawn sendproc(realtosrv);

	cmd(win, "grab set .c");
	cmd(win, "focus .");
loop:	for (;;) alt {
	tosrv <-= currevent =>
		tosrv = dummytosrv;

	(buts, p) := <-mch =>
		if (buts & 2)
			(buts, p) = aim(mch, hitbat, p);
		(v, perp, dist) := line.hittest(p);
		dist -= offset;
		# constrain bat and mouse positions
		if (dist < 0.0 || dist + batlen > line.s) {
			if (dist < 0.0) {
				p = line.point(offset);
				dist = 1.0;
			} else {
				p = line.point(line.s - batlen + offset);
				dist = line.s - batlen;
			}
			p.x -= int (v.x * perp);
			p.y -= int (v.y * perp);
			win.image.display.cursorset(p.add(cvsorigin));
		}
		(hitbat.s1, hitbat.s2) = (dist, dist + batlen);
		hitbat.config();
		cmd(win, "update");
		currevent = "bat " + string hitbat.s1;
		tosrv = realtosrv;
		if (!buts)
			break loop;
	}
	cmd(win, "grab release .c");
	realtosrv <-= nil;
	currentlydragging = -1;
}

CHARGETIME: con 1000.0;
MAXCHARGE: con 50.0;

α: con 0.999;		# decay in one millisecond
D: con 5;
aim(mch: chan of (int, Point), hitbat: ref Obstacle, p: Point): (int, Point)
{
	cvsorigin := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
	startms := ms := sys->millisec();
	δ := Realpoint(0.0, 0.0);
	line := hitbat.line;
	charge := 0.0;
	pivot := line.point((hitbat.s1 + hitbat.s2) / 2.0);
	s1 := p2s(line.point(hitbat.s1));
	s2 := p2s(line.point(hitbat.s2));
	cmd(win, ".c create line 0 0 0 0 -tags wire -fill yellow");
	ballid := makeballitem(-1, myturn);
	bp, p2: Point;
	buts := 2;
	for (;;) {
		v := makeunit(δ);
		bp = pivot.add((int (v.x * charge), int (v.y * charge)));
		cmd(win, ".c coords wire "+s1+" "+p2s(bp)+" "+s2);
		ballmove(ballid, bp);
		cmd(win, "update");
		if ((buts & 2) == 0)
			break;
		(buts, p2) = <-mch;
		now := sys->millisec();
		fade := math->pow(α, real (now - ms));
		charge = real (now - startms) * (MAXCHARGE / CHARGETIME);
		if (charge > MAXCHARGE)
			charge = MAXCHARGE;
		ms = now;
		dp := p2.sub(p);
		δ.x = δ.x * fade + real dp.x;
		δ.y = δ.y * fade + real dp.y;
		mag := δ.x * δ.x + δ.y * δ.y;
		if (dp.x != 0 || dp.y != 0)
			win.image.display.cursorset(p.add(cvsorigin));
	}
	cmd(win, ".c delete wire " + ballid);
	cmd(win, "update");
	(δ.x, δ.y) = (-δ.x, -δ.y);
	cliquecmd("newball " + string hitbat.id + " " +
		p2s(bp) + " " + rp2s(makeunit(δ)) + " " + string (charge / 100.0));
	return (buts, p2);
}

makeunit(v: Realpoint): Realpoint
{
	mag := math->sqrt(v.x * v.x + v.y * v.y);
	if (mag < ZERO)
		return (1.0, 0.0);
	return (v.x / mag, v.y / mag);
}

sendproc(tosrv: chan of string)
{
	procname("send");
	while ((ev := <-tosrv) != nil)
		cliquecmd(ev);
}

makeball(id: int, state: ref Ballstate): chan of ref Ballstate
{
	mkballreply := chan of chan of ref Ballstate;
	mkball <-= (id, mkballreply);
	ballctl := <-mkballreply;
	ballctl <-= state;
	return ballctl;
}

blankobstacle: Obstacle;
Obstacle.new(id: int): ref Obstacle
{
	cmd(win, ".c create line 0 0 0 0 -width 3 -fill #aaaaaa" + " -tags l" + string id);
	o := ref blankobstacle;
	o.line = Line.new((0, 0), (0, 0));
	o.id = id;
	o.owner = -1;
	o.srvid = -1;
	lineversion++;
	lines = o :: lines;
	return o;
}

Obstacle.config(o: self ref Obstacle)
{
	if (o.isbat) {
		cmd(win, ".c coords l" + string o.id + " " +
			p2s(o.line.point(o.s1)) + " " + p2s(o.line.point(o.s2)));
		if (o.owner == memberid)
			cmd(win, ".c itemconfigure l" + string o.id + " -fill red");
		else
			cmd(win, ".c itemconfigure l" + string o.id + " -fill white");
	} else {
		cmd(win, ".c coords l" + string o.id + " " +
			p2s(o.line.point(0.0)) + " " + p2s(o.line.point(o.line.s)));
	}
}
	
# make sure cpu time is handed to all ball processes fairly
# by passing a "token" around to each process in turn.
# each process does its work when it *hasn't* got its
# token but it can't go through two iterations without
# waiting its turn.
#
# new processes are created by sending on mkball.
# the channel sent back can be used to control the position
# and velocity of the ball and to destroy it.
monitor(mkball: chan of (int, chan of chan of ref Ballstate))
{
	procname("mon");
	procl, proc: list of (chan of ref Ballstate, chan of int);
	rc := dummyrc := chan of int;
	for (;;) {
		alt {
		(id, ch) := <-mkball =>
			(newc, newrc) := (chan of ref Ballstate, chan of int);
			procl = (newc, newrc) :: procl;
			spawn animproc(id, newc, newrc);
			ch <-= newc;
			if (tl procl == nil) {		# first ball
				newc <-= nil;
				rc = newrc;
				proc = procl;
			}
		alive := <-rc =>					# got token.
			if (!alive) {
				# ball has exited: remove from list
				newprocl: list of (chan of ref Ballstate, chan of int);
				for (; procl != nil; procl = tl procl)
					if ((hd procl).t1 != rc)
						newprocl = hd procl :: newprocl;
				procl = newprocl;
			}
			if ((proc = tl proc) == nil)
				proc = procl;
			if (proc == nil) {
				rc = dummyrc;
			} else {
				c: chan of ref Ballstate;
				(c, rc) = hd proc;
				c <-= nil;				# hand token to next process.
			}
		}
	}
}

# buffer ball state commands, so at least balls we handle
# locally appear glitch free.
bufferproc(cmdch: chan of string)
{
	procname("buffer");
	buffer := ref Queue;
	bufhd: string;
	dummytosrv := chan of string;
	realtosrv := chan of string;
	spawn sendproc(realtosrv);
	tosrv := dummytosrv;
	for (;;) alt {
	tosrv <-= bufhd =>
		if ((bufhd = buffer.get()) == nil)
			tosrv = dummytosrv;
	s := <-cmdch =>
		if (s == nil) {
			# ignore other queued requests, as they're
			# only state changes for a ball that's now been deleted.
			realtosrv <-= nil;
			exit;
		}
		buffer.put(s);
		if (tosrv == dummytosrv) {
			tosrv = realtosrv;
			bufhd = buffer.get();
		}
	}
}
start: int;
# animate one ball. initial position and unit-velocity are
# given by p and v.
animproc(id: int, c: chan of ref Ballstate, rc: chan of int)
{
	procname("anim");
	while ((newstate := <-c) == nil)
		rc <-= 1;
	state := *newstate;
	totaldist := 0.0;		# distance ball has travelled from reference point to last intersection
	ballid := makeballitem(id, state.owner);
	smallcount := 0;
	version := lineversion;
	tosrv := chan of string;
	start := sys->millisec();
	spawn bufferproc(tosrv);
loop:	for (;;) {
		hitp: Realpoint;

		dist := 1000000.0;
		oldobs := state.hitobs;
		hitt: real;
		for (l := lines; l != nil; l = tl l) {
			obs := hd l;
			(ok, hp, hdist, t) := obs.line.intersection(state.p, state.v);
			if (ok && hdist < dist && obs != oldobs && (smallcount < 10 || hdist > 1.5)) {
				(hitp, state.hitobs, dist, hitt) = (hp, obs, hdist, t);
			}
		}
		if (dist > 10000.0) {
			sys->print("no intersection!\n");
			state = ballexit(1, ballid, tosrv, c, rc);
			totaldist = 0.0;
			continue loop;
		}
		if (dist < 0.0001)
			smallcount++;
		else
			smallcount = 0;
		t0 := int (totaldist / state.speed) + state.t0 - timeoffset;
		et := t0 + int (dist / state.speed);
		t := sys->millisec() - t0;
		dt := et - t0;
		do {
			s := real t * state.speed;
			currp := Realpoint(state.p.x + s * state.v.x,  state.p.y + s * state.v.y);
			ballmove(ballid, (int currp.x, int currp.y));
			cmd(win, "update");
			if (lineversion > version) {
				(state.p, state.hitobs, version) = (currp, oldobs, lineversion);
				totaldist += s;
				continue loop;
			}
			if ((newstate := <-c) != nil) {
				if (newstate == Ballexit)
					ballexit(0, ballid, tosrv, c, rc);
				state = *newstate;
				totaldist = 0.0;
				continue loop;
			}
			rc <-= 1;
			t = sys->millisec() - t0;
		} while (t < dt);
		totaldist += dist;
		state.p = hitp;
		hitobs := state.hitobs;
		if (hitobs.isbat) {
			if (hitobs.owner == memberid) {
				if (hitt >= hitobs.s1 && hitt <= hitobs.s2)
					state.v = batboing(hitobs, hitt, state.v);
				tosrv <-= "state " + 
					string id + 
					" " + string hitobs.srvid +
					" " + string state.owner +
					" " + rp2s(state.p) + " " + rp2s(state.v) +
					" " + string state.speed +
					" " + string (sys->millisec() + timeoffset);
			} else {
				# wait for enlightenment
				while ((newstate := <-c) == nil)
					rc <-= 1;
				if (newstate == Ballexit)
					ballexit(0, ballid, tosrv, c, rc);
				state = *newstate;
				totaldist = 0.0;
			}
		} else if (hitobs.owner == memberid) {
			# if line has an owner but isn't a bat, then it's
			# a terminating line, so we inform server.
			cliquecmd("lost " + string id);
			state = ballexit(1, ballid, tosrv, c, rc);
			totaldist = 0.0;
		} else
			state.v = boing(state.v, hitobs.line);
	}
}

#ballmask: ref Image;
imageinit()
{
#	displ := win.image.display;
#	ballmask = displ.newimage(((0, 0), (BALLSIZE+1, BALLSIZE+1)), 0, 0, Draw->White);
#	ballmask.draw(ballmask.r, displ.zeros, displ.ones, (0, 0));
#	ballmask.fillellipse((BALLSIZE/2, BALLSIZE/2), BALLSIZE/2, BALLSIZE/2, displ.ones,  (0, 0));
#	End: con Draw->Endsquare;
#	n := 5;
#	θ := 0.0;
#	δ := (2.0 * π) / real n;
#	c := Point(BALLSIZE / 2, BALLSIZE / 2).sub((1, 1));
#	r := real (BALLSIZE / 2);
#	for (i := 0; i < n; i++) {
#		p2 := Point(int (r * math->cos(θ)), int (r * math->sin(θ)));
#		sys->print("drawing from %s to %s\n", p2s(c), p2s(p2.add(c)));
#		ballmask.line(c, c.add(p2), End, End, 1, displ.ones, (0, 0));
#		θ += δ;
#	}
}

makeballitem(id, owner: int): string
{
	displ := win.image.display;
	return cmd(win, ".c create oval 0 0 1 1 -fill " + members[owner].colour +
			" -tags o" + string owner);
}

ballmove(ballid: string, p: Point)
{
	cmd(win, ".c coords " + ballid +
		" " + string (p.x - BALLSIZE) +
		" " + string (p.y - BALLSIZE) +
		" " + string (p.x + BALLSIZE) +
		" " + string (p.y + BALLSIZE));
}

ballexit(wait: int, ballid: string, tosrv: chan of string, c: chan of ref Ballstate, rc: chan of int): Ballstate
{
	if (wait) {
		while ((s := <-c) != Ballexit)
			if (s == nil)
				rc <-= 1;
			else
				return *s;			# maybe we're not exiting, after all...
	}
	cmd(win, ".c delete " + ballid + ";update");
#	cmd(win, "image delete " + ballid);
	tosrv <-= nil;
	<-c;
	rc <-= 0;		# inform monitor that we've gone
	exit;
}

# thread-safe access to the Rand module
randgenproc(ch: chan of int)
{
	procname("rand");
	rand := load Rand Rand->PATH;
	for (;;)
		ch <-= rand->rand(16r7fffffff);
}

abs(x: real): real
{
	if (x < 0.0)
		return -x;
	return x;
}

# bounce ball travelling in direction av off line b.
# return the new unit vector.
boing(av: Realpoint, b: ref Line): Realpoint
{
	d := math->atan2(b.v.y, b.v.x) * 2.0 - math->atan2(av.y, av.x);
	return (math->cos(d), math->sin(d));
}

# calculate how a bounce vector should be modified when
# hitting a bat. t gives the intersection point on the bat;
# ballv is the ball's vector.
batboing(bat: ref Obstacle, t: real, ballv: Realpoint): Realpoint
{
	ballθ := math->atan2(ballv.y, ballv.x);
	batθ := math->atan2(bat.line.v.y, bat.line.v.x);
	φ := ballθ - batθ;
	δ: real;
	t -= bat.s1;
	batlen := bat.s2 - bat.s1;
	if (math->sin(φ) > 0.0)
		δ = (t / batlen) * Maxδ * 2.0 - Maxδ;
	else
		δ = (t / batlen) * -Maxδ * 2.0 + Maxδ;
	θ := math->atan2(bat.line.v.y, bat.line.v.x) * 2.0 - ballθ;	# boing
	θ += δ;
	return (math->cos(θ), math->sin(θ));
}

Line.new(p1, p2: Point): ref Line
{
	ln := ref Line;
	ln.p = (real p1.x, real p1.y);
	v := Realpoint(real (p2.x - p1.x), real (p2.y - p1.y));
	ln.s =  math->sqrt(v.x * v.x + v.y * v.y);
	if (ln.s > ZERO)
		ln.v = (v.x / ln.s, v.y / ln.s);
	else
		ln.v = (1.0, 0.0);
	return ln;
}

# return normal from line, perpendicular distance from line and distance down line
Line.hittest(l: self ref Line, ip: Point): (Realpoint, real, real)
{
	p := Realpoint(real ip.x, real ip.y);
	v := Realpoint(-l.v.y, l.v.x);
	(nil, nil, perp, ldist) := l.intersection(p, v);
	return (v, perp, ldist);
}

Line.point(l: self ref Line, s: real): Point
{
	return (int (l.p.x + s * l.v.x), int (l.p.y + s * l.v.y));
}

# compute the intersection of lines a and b.
# b is assumed to be fixed, and a is indefinitely long
# but doesn't extend backwards from its starting point.
# a is defined by the starting point p and the unit vector v.
# return whether it hit, the point at which it hit if so,
# the distance of the intersection point from p,
# and the distance of the intersection point from b.p.
Line.intersection(b: self ref Line, p, v: Realpoint): (int, Realpoint, real, real)
{
	det := b.v.x * v.y - v.x * b.v.y;
	if (det > -ZERO && det < ZERO)
		return (0, (0.0, 0.0), 0.0, 0.0);

	y21 := b.p.y - p.y;
	x21 := b.p.x - p.x;
	s := (b.v.x * y21 - b.v.y * x21) / det;
	t := (v.x * y21 - v.y * x21) / det;
	if (s < 0.0)
		return (0, (0.0, 0.0), s, t);
	hit := t >= 0.0 && t <= b.s;
	hp: Realpoint;
	if (hit)
		hp = (p.x+v.x*s, p.y+v.y*s);
	return (hit, hp, s, t);
}

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

state2s(s: ref Ballstate): string
{
	return sys->sprint("[hitobs:%d(id %d), t0: %d, p: %g %g; v: %g %g; s: %g",
		s.hitobs.srvid, s.hitobs.id, s.t0, s.p.x, s.p.y, s.v.x, s.v.y, s.speed);
}

l2s(l: ref Line): string
{
	return p2s(l.point(0.0)) + " " + p2s(l.point(l.s));
}

rp2s(rp: Realpoint): string
{
	return string rp.x + " " + string rp.y;
}


p2s(p: Point): string
{
	return string p.x + " " + string p.y;
}

notifypid := -1;
notify(s: string)
{
	kill(notifypid);
	sync := chan of int;
	spawn notifyproc(s, sync);
	notifypid = <-sync;
}

notifyproc(s: string, sync: chan of int)
{
	procname("notify");
	sync <-= sys->pctl(0, nil);
	cmd(win, ".c delete notify");
	id := cmd(win, ".c create text 0 0 -anchor nw -fill red -tags notify -text '" + s);
	bbox := cmd(win, ".c bbox " + id);
	cmd(win, ".c create rectangle " + bbox + " -fill #ffffaa -tags notify");
	cmd(win, ".c raise " + id);
	cmd(win, "update");
	sys->sleep(750);
	cmd(win, ".c delete notify");
	cmd(win, "update");
	notifypid = -1;
}

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

T: type string;

Queue.put(q: self ref Queue, s: T)
{
	q.t = s :: q.t;
}

Queue.get(q: self ref Queue): T
{
	s: T;
	if(q.h == nil){
		q.h = revlist(q.t);
		q.t = nil;
	}
	if(q.h != nil){
		s = hd q.h;
		q.h = tl q.h;
	}
	return s;
}

revlist(ls: list of T) : list of T
{
	rs: list of T;
	for (; ls != nil; ls = tl ls)
		rs = hd ls :: rs;
	return rs;
}

procname(s: string)
{
#	sys->procname(sys->procname(nil) + " " + s);
}