summaryrefslogtreecommitdiff
path: root/appl/spree/clients/bounce.b
diff options
context:
space:
mode:
Diffstat (limited to 'appl/spree/clients/bounce.b')
-rw-r--r--appl/spree/clients/bounce.b958
1 files changed, 958 insertions, 0 deletions
diff --git a/appl/spree/clients/bounce.b b/appl/spree/clients/bounce.b
new file mode 100644
index 00000000..f1960582
--- /dev/null
+++ b/appl/spree/clients/bounce.b
@@ -0,0 +1,958 @@
+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);
+}
+