summaryrefslogtreecommitdiff
path: root/appl/spree
diff options
context:
space:
mode:
Diffstat (limited to 'appl/spree')
-rw-r--r--appl/spree/archives.b515
-rw-r--r--appl/spree/clients/bounce.b958
-rw-r--r--appl/spree/clients/cards.b2220
-rw-r--r--appl/spree/clients/chat.b194
-rw-r--r--appl/spree/clients/gather.b178
-rw-r--r--appl/spree/clients/lobby.b562
-rw-r--r--appl/spree/clients/othello.b270
-rw-r--r--appl/spree/engines/afghan.b302
-rw-r--r--appl/spree/engines/bounce.b258
-rw-r--r--appl/spree/engines/canfield.b340
-rw-r--r--appl/spree/engines/chat.b60
-rw-r--r--appl/spree/engines/debug.b163
-rw-r--r--appl/spree/engines/freecell.b428
-rw-r--r--appl/spree/engines/gather.b267
-rw-r--r--appl/spree/engines/hearts.b300
-rw-r--r--appl/spree/engines/liars.b490
-rw-r--r--appl/spree/engines/liars.y132
-rw-r--r--appl/spree/engines/lobby.b389
-rw-r--r--appl/spree/engines/othello.b242
-rw-r--r--appl/spree/engines/racingdemon.b464
-rw-r--r--appl/spree/engines/snap.b241
-rw-r--r--appl/spree/engines/spider.b259
-rw-r--r--appl/spree/engines/spit.b483
-rw-r--r--appl/spree/engines/whist.b305
-rw-r--r--appl/spree/gather.m10
-rw-r--r--appl/spree/join.b115
-rw-r--r--appl/spree/join.m5
-rw-r--r--appl/spree/joinsession.b115
-rw-r--r--appl/spree/joinsession.m7
-rw-r--r--appl/spree/lib/allow.b194
-rw-r--r--appl/spree/lib/allow.m9
-rw-r--r--appl/spree/lib/base64.b72
-rw-r--r--appl/spree/lib/base64.m5
-rw-r--r--appl/spree/lib/cardlib.b917
-rw-r--r--appl/spree/lib/cardlib.m114
-rw-r--r--appl/spree/lib/commandline.b191
-rw-r--r--appl/spree/lib/commandline.m16
-rw-r--r--appl/spree/lib/objstore.b65
-rw-r--r--appl/spree/lib/objstore.m8
-rw-r--r--appl/spree/lib/testsets.b152
-rw-r--r--appl/spree/lib/tricks.b140
-rw-r--r--appl/spree/lib/tricks.m21
-rw-r--r--appl/spree/man/gamesrv.man2471
-rw-r--r--appl/spree/man/gamesrv.man4296
-rw-r--r--appl/spree/man/styxservers-nametree.man2180
-rw-r--r--appl/spree/man/styxservers.man2902
-rw-r--r--appl/spree/mkfile66
-rw-r--r--appl/spree/other/tst.b151
-rw-r--r--appl/spree/other/tstboing.b158
-rwxr-xr-xappl/spree/other/tstlines.sh53
-rw-r--r--appl/spree/other/tstwin.b351
-rw-r--r--appl/spree/spree.b1554
-rw-r--r--appl/spree/spree.m140
53 files changed, 16498 insertions, 0 deletions
diff --git a/appl/spree/archives.b b/appl/spree/archives.b
new file mode 100644
index 00000000..65249628
--- /dev/null
+++ b/appl/spree/archives.b
@@ -0,0 +1,515 @@
+implement Archives;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "bufio.m";
+ bufio: Bufio;
+ Iobuf: import bufio;
+include "sets.m";
+ sets: Sets;
+ Set, set, A, B, All, None: import sets;
+include "string.m";
+ str: String;
+include "spree.m";
+ spree: Spree;
+ Clique, Member, Attributes, Attribute, Object: import spree;
+ MAXPLAYERS: import Spree;
+
+stderr: ref Sys->FD;
+
+Qc: con " \t{}=\n";
+Saveinfo: adt {
+ clique: ref Clique;
+ idmap: array of int; # map clique id to archive id
+ memberids: Set; # set of member ids to archive
+};
+
+Error: exception(string);
+
+Cliqueparse: adt {
+ iob: ref Iobuf;
+ line: int;
+ filename: string;
+ lasttok: int;
+ errstr: string;
+
+ gettok: fn(gp: self ref Cliqueparse): (int, string) raises (Error);
+ lgettok: fn(gp: self ref Cliqueparse, t: int): string raises (Error);
+ getline: fn(gp: self ref Cliqueparse): list of string raises (Error);
+ error: fn(gp: self ref Cliqueparse, e: string) raises (Error);
+};
+
+WORD: con 16rff;
+
+init(cliquemod: Spree)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ bufio = load Bufio Bufio->PATH;
+ if (bufio == nil) {
+ sys->fprint(stderr, "cliquearchive: cannot load %s: %r\n", Bufio->PATH);
+ raise "fail:bad module";
+ }
+ sets = load Sets Sets->PATH;
+ if (sets == nil) {
+ sys->fprint(stderr, "cliquearchive: cannot load %s: %r\n", Sets->PATH);
+ raise "fail:bad module";
+ }
+ str = load String String->PATH;
+ if (str == nil) {
+ sys->fprint(stderr, "cliquearchive: cannot load %s: %r\n", String->PATH);
+ raise "fail:bad module";
+ }
+ sets->init();
+ spree = cliquemod;
+}
+
+write(clique: ref Clique, info: list of (string, string), name: string, memberids: Sets->Set): string
+{
+ sys->print("saveclique, saving %d objects\n", objcount(clique.objects[0]));
+ iob := bufio->create(name, Sys->OWRITE, 8r666);
+ if (iob == nil)
+ return sys->sprint("cannot open %s: %r", name);
+
+ # integrate suspended members with current members
+ # for the archive.
+
+ si := ref Saveinfo(clique, array[memberids.limit()] of int, memberids);
+ members := clique.members();
+ pa := array[len members] of (string, int);
+ for (i := 0; members != nil; members = tl members) {
+ p := hd members;
+ if (memberids.holds(p.id))
+ pa[i++] = (p.name, p.id);
+ }
+ pa = pa[0:i];
+ sortmembers(pa); # ensure members stay in the same order when rearchived.
+ pl: list of string;
+ for (i = len pa - 1; i >= 0; i--) {
+ si.idmap[pa[i].t1] = i;
+ pl = pa[i].t0 :: pl;
+ }
+ iob.puts(quotedc("session" :: clique.archive.argv, Qc));
+ iob.putc('\n');
+ iob.puts(quotedc("members" :: pl, Qc));
+ iob.putc('\n');
+ il: list of string;
+ for (; info != nil; info = tl info)
+ il = (hd info).t0 :: (hd info).t1 :: il;
+ iob.puts(quotedc("info" :: il, Qc));
+ iob.putc('\n');
+ writeobject(iob, 0, si, clique.objects[0]);
+ iob.close();
+ return nil;
+}
+
+writeobject(iob: ref Iobuf, depth: int, si: ref Saveinfo, obj: ref Object)
+{
+ indent(iob, depth);
+ iob.puts(quotedc(obj.objtype :: nil, Qc));
+ iob.putc(' ');
+ iob.puts(mapset(si, obj.visibility).str());
+ writeattrs(iob, si, obj);
+ if (len obj.children > 0) {
+ iob.puts(" {\n");
+ for (i := 0; i < len obj.children; i++)
+ writeobject(iob, depth + 1, si, obj.children[i]);
+ indent(iob, depth);
+ iob.puts("}\n");
+ } else
+ iob.putc('\n');
+}
+
+writeattrs(iob: ref Iobuf, si: ref Saveinfo, obj: ref Object)
+{
+ a := obj.attrs.a;
+ n := 0;
+ for (i := 0; i < len a; i++)
+ n += len a[i];
+ attrs := array[n] of ref Attribute;
+ j := 0;
+ for (i = 0; i < len a; i++)
+ for (l := a[i]; l != nil; l = tl l)
+ attrs[j++] = hd l;
+ sortattrs(attrs);
+ for (i = 0; i < len attrs; i++) {
+ attr := attrs[i];
+ if (attr.val == nil)
+ continue;
+ iob.putc(' ');
+ iob.puts(quotedc(attr.name :: nil, Qc));
+ vis := mapset(si, attr.visibility);
+ if (!vis.eq(All))
+ iob.puts("{" + vis.str() + "}");
+ iob.putc('=');
+ iob.puts(quotedc(attr.val :: nil, Qc));
+ }
+}
+
+mapset(si: ref Saveinfo, s: Set): Set
+{
+ idmap := si.idmap;
+ m := s.msb() != 0;
+ limit := si.memberids.limit();
+ r := None;
+ for (i := 0; i < limit; i++)
+ if (m == !s.holds(i))
+ r = r.add(idmap[i]);
+ if (m)
+ r = All.X(A&~B, r);
+ return r;
+}
+
+readheader(filename: string): (ref Archive, string)
+{
+ iob := bufio->open(filename, Sys->OREAD);
+ if (iob == nil)
+ return (nil, sys->sprint("cannot open '%s': %r", filename));
+ gp := ref Cliqueparse(iob, 1, filename, Bufio->EOF, nil);
+
+ {
+ line := gp.getline();
+ if (len line < 2 || hd line != "session")
+ gp.error("expected 'session' line, got " + str->quoted(line));
+ argv := tl line;
+ line = gp.getline();
+ if (line == nil || tl line == nil || hd line != "members")
+ gp.error("expected 'members' line");
+ members := l2a(tl line);
+ line = gp.getline();
+ if (line == nil || hd line != "info")
+ gp.error("expected 'info' line");
+ if (len tl line % 2 != 0)
+ gp.error("'info' line must have an even number of fields");
+ info: list of (string, string);
+ for (line = tl line; line != nil; line = tl tl line)
+ info = (hd line, hd tl line) :: info;
+ arch := ref Archive(argv, members, info, nil);
+ iob.close();
+ return (arch, nil);
+ } exception e {
+ Error =>
+ return (nil, x := e);
+ }
+}
+
+read(filename: string): (ref Archive, string)
+{
+ iob := bufio->open(filename, Sys->OREAD);
+ if (iob == nil)
+ return (nil, sys->sprint("cannot open '%s': %r", filename));
+ gp := ref Cliqueparse(iob, 1, filename, Bufio->EOF, nil);
+
+ {
+ line := gp.getline();
+ if (len line < 2 || hd line != "session")
+ gp.error("expected 'session' line, got " + str->quoted(line));
+ argv := tl line;
+ line = gp.getline();
+ if (line == nil || tl line == nil || hd line != "members")
+ gp.error("expected 'members' line");
+ members := l2a(tl line);
+ line = gp.getline();
+ if (line == nil || hd line != "info")
+ gp.error("expected 'info' line");
+ if (len tl line % 2 != 0)
+ gp.error("'info' line must have an even number of fields");
+ info: list of (string, string);
+ for (line = tl line; line != nil; line = tl tl line)
+ info = (hd line, hd tl line) :: info;
+ root := readobject(gp);
+ if (root == nil)
+ return (nil, filename + ": no root object found");
+ n := objcount(root);
+ arch := ref Archive(argv, members, info, array[n] of ref Object);
+ arch.objects[0] = root;
+ root.parentid = -1;
+ root.id = 0;
+ allocobjects(root, arch.objects, 1);
+ iob.close();
+ return (arch, nil);
+ } exception e {
+ Error =>
+ return (nil, x := e);
+ }
+}
+
+allocobjects(parent: ref Object, objects: array of ref Object, n: int): int
+{
+ base := n;
+ children := parent.children;
+ objects[n:] = children;
+ n += len children;
+ for (i := 0; i < len children; i++) {
+ child := children[i];
+ (child.id, child.parentid) = (base + i, parent.id);
+ n = allocobjects(child, objects, n);
+ }
+ return n;
+}
+
+objcount(o: ref Object): int
+{
+ n := 1;
+ a := o.children;
+ for (i := 0; i < len a; i++)
+ n += objcount(a[i]);
+ return n;
+}
+
+readobject(gp: ref Cliqueparse): ref Object raises (Error)
+{
+ {
+ # object format:
+ # objtype visibility [attr[{vis}]=val]... [{\nchildren\n}]\n
+ (t, s) := gp.gettok(); #{
+ if (t == Bufio->EOF || t == '}')
+ return nil;
+ if (t != WORD)
+ gp.error("expected WORD");
+ objtype := s;
+ vis := sets->str2set(gp.lgettok(WORD));
+ attrs := Attributes.new();
+ objs: array of ref Object;
+ loop: for (;;) {
+ (t, s) = gp.gettok();
+ case t {
+ WORD =>
+ attr := s;
+ attrvis := All;
+ (t, s) = gp.gettok();
+ if (t == '{') { #}
+ attrvis = sets->str2set(gp.lgettok(WORD)); #{
+ gp.lgettok('}');
+ gp.lgettok('=');
+ } else if (t != '=')
+ gp.error("expected '='");
+ val := gp.lgettok(WORD);
+ attrs.set(attr, val, attrvis);
+ '{' => #}
+ gp.lgettok('\n');
+ objl: list of ref Object;
+ while ((obj := readobject(gp)) != nil)
+ objl = obj :: objl;
+ n := len objl;
+ objs = array[n] of ref Object;
+ for (n--; n >= 0; n--)
+ (objs[n], objl) = (hd objl, tl objl);
+ gp.lgettok('\n');
+ break loop;
+ '\n' =>
+ break loop;
+ * =>
+ gp.error("expected WORD or '{'"); #}
+ }
+ }
+ return ref Object(-1, attrs, vis, -1, objs, -1, objtype);
+ } exception e {Error => raise e;}
+}
+
+Cliqueparse.error(gp: self ref Cliqueparse, e: string) raises (Error)
+{
+ raise Error(sys->sprint("%s:%d: parse error after %s: %s", gp.filename, gp.line,
+ tok2str(gp.lasttok), e));
+}
+
+Cliqueparse.getline(gp: self ref Cliqueparse): list of string raises (Error)
+{
+ {
+ line, nline: list of string;
+ for (;;) {
+ (t, s) := gp.gettok();
+ if (t == '\n')
+ break;
+ if (t != WORD)
+ gp.error("expected a WORD");
+ line = s :: line;
+ }
+ for (; line != nil; line = tl line)
+ nline = hd line :: nline;
+ return nline;
+ } exception e {Error => raise e;}
+}
+
+# get a token, which must be of type t.
+Cliqueparse.lgettok(gp: self ref Cliqueparse, mustbe: int): string raises (Error)
+{
+ {
+ (t, s) := gp.gettok();
+ if (t != mustbe)
+ gp.error("lgettok expected " + tok2str(mustbe));
+ return s;
+ } exception e {Error => raise e;}
+
+}
+
+Cliqueparse.gettok(gp: self ref Cliqueparse): (int, string) raises (Error)
+{
+ {
+ iob := gp.iob;
+ while ((c := iob.getc()) == ' ' || c == '\t')
+ ;
+ t: int;
+ s: string;
+ case c {
+ Bufio->EOF or
+ Bufio->ERROR =>
+ t = Bufio->EOF;
+ '\n' =>
+ gp.line++;
+ t = '\n';
+ '{' =>
+ t = '{';
+ '}' =>
+ t = '}';
+ '=' =>
+ t = '=';
+ '\'' =>
+ for(;;) {
+ while ((nc := iob.getc()) != '\'' && nc >= 0) {
+ s[len s] = nc;
+ if (nc == '\n')
+ gp.line++;
+ }
+ if (nc == Bufio->EOF || nc == Bufio->ERROR)
+ gp.error("unterminated quote");
+ if (iob.getc() != '\'') {
+ iob.ungetc();
+ break;
+ }
+ s[len s] = '\''; # 'xxx''yyy' becomes WORD(xxx'yyy)
+ }
+ t = WORD;
+ * =>
+ do {
+ s[len s] = c;
+ c = iob.getc();
+ if (in(c, Qc)) {
+ iob.ungetc();
+ break;
+ }
+ } while (c >= 0);
+ t = WORD;
+ }
+ gp.lasttok = t;
+ return (t, s);
+ } exception e {Error => raise e;}
+}
+
+tok2str(t: int): string
+{
+ case t {
+ Bufio->EOF =>
+ return "EOF";
+ WORD =>
+ return "WORD";
+ '\n' =>
+ return "'\\n'";
+ * =>
+ return sys->sprint("'%c'", t);
+ }
+}
+
+# stolen from lib/string.b - should be part of interface in string.m
+quotedc(argv: list of string, cl: string): string
+{
+ s := "";
+ while (argv != nil) {
+ arg := hd argv;
+ for (i := 0; i < len arg; i++) {
+ c := arg[i];
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\'' || in(c, cl))
+ break;
+ }
+ if (i < len arg || arg == nil) {
+ s += "'" + arg[0:i];
+ for (; i < len arg; i++) {
+ if (arg[i] == '\'')
+ s[len s] = '\'';
+ s[len s] = arg[i];
+ }
+ s[len s] = '\'';
+ } else
+ s += arg;
+ if (tl argv != nil)
+ s[len s] = ' ';
+ argv = tl argv;
+ }
+ return s;
+}
+
+in(c: int, cl: string): int
+{
+ n := len cl;
+ for (i := 0; i < n; i++)
+ if (cl[i] == c)
+ return 1;
+ return 0;
+}
+
+indent(iob: ref Iobuf, depth: int)
+{
+ for (i := 0; i < depth; i++)
+ iob.putc('\t');
+}
+
+sortmembers(p: array of (string, int))
+{
+ membermergesort(p, array[len p] of (string, int));
+}
+
+membermergesort(a, b: array of (string, int))
+{
+ r := len a;
+ if (r > 1) {
+ m := (r-1)/2 + 1;
+ membermergesort(a[0:m], b[0:m]);
+ membermergesort(a[m:], b[m:]);
+ b[0:] = a;
+ for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+ if (b[i].t1 > b[j].t1)
+ a[k] = b[j++];
+ else
+ a[k] = b[i++];
+ }
+ if (i < m)
+ a[k:] = b[i:m];
+ else if (j < r)
+ a[k:] = b[j:r];
+ }
+}
+
+sortattrs(a: array of ref Attribute)
+{
+ attrmergesort(a, array[len a] of ref Attribute);
+}
+
+attrmergesort(a, b: array of ref Attribute)
+{
+ r := len a;
+ if (r > 1) {
+ m := (r-1)/2 + 1;
+ attrmergesort(a[0:m], b[0:m]);
+ attrmergesort(a[m:], b[m:]);
+ b[0:] = a;
+ for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+ if (b[i].name > b[j].name)
+ a[k] = b[j++];
+ else
+ a[k] = b[i++];
+ }
+ if (i < m)
+ a[k:] = b[i:m];
+ else if (j < r)
+ a[k:] = b[j:r];
+ }
+}
+
+l2a(l: list of string): array of string
+{
+ n := len l;
+ a := array[n] of string;
+ for (i := 0; i < n; i++)
+ (a[i], l) = (hd l, tl l);
+ return a;
+} \ No newline at end of file
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);
+}
+
diff --git a/appl/spree/clients/cards.b b/appl/spree/clients/cards.b
new file mode 100644
index 00000000..17601bd7
--- /dev/null
+++ b/appl/spree/clients/cards.b
@@ -0,0 +1,2220 @@
+implement Cards;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect, Display, Image, Font: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "math.m";
+ math: Math;
+
+Cards: module {
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+# fairly general card clique client.
+# inherent restrictions:
+# no dragging of cards visible over the net; it's unclear how
+# to handle the coordinate spaces involved
+
+Object: adt {
+ id: int;
+ pick {
+ Card =>
+ parentid: int;
+ face: int; # 1 is face up
+ number: int;
+ rear: int;
+ Member =>
+ cid: int;
+ name: string;
+ Stack =>
+ o: ref Layobject.Stack;
+ Widget =>
+ o: ref Layobject.Widget;
+ Menuentry =>
+ parentid: int;
+ text: string;
+ Layoutframe =>
+ lay: ref Layout.Frame;
+ Layoutobj =>
+ lay: ref Layout.Obj;
+ Scoretable =>
+ scores: array of ref Object.Score;
+ Score =>
+ row: array of (int, string);
+ height: int;
+ Button =>
+ Other =>
+ }
+};
+
+# specify how an object is laid out.
+Layout: adt {
+ id: int;
+ parentid: int;
+ opts: string; # pack options
+ orientation: int;
+ pick {
+ Frame =>
+ lays: cyclic array of ref Layout;
+ Obj =>
+ layid: int; # reference to layid of laid-out object
+ }
+};
+
+# an object which can be laid out on the canvas
+Layobject: adt {
+ id: int;
+ parentid: int;
+ w: string;
+ size: Point;
+ needrepack: int;
+ orientation: int;
+ layid: int;
+ pick {
+ Stack =>
+ style: int;
+ cards: array of ref Object.Card; # fake objects when invisible
+ pos: Point; # top-left origin of first card in stack
+ delta: Point; # card offset delta.
+ animq: ref Queue; # queue of pending animations.
+ actions: int;
+ maxcards: int;
+ title: string;
+ visible: int;
+ n: int; # for concealed stacks, n cards in stack.
+ ownerid: int; # owner of selection
+ sel: ref Selection;
+ showsize,
+ hassize: int;
+ Widget =>
+ wtype: string;
+ entries: array of ref Object.Menuentry;
+ cmd: string; # only used for entry widgets
+ width: int;
+ }
+};
+
+Animation: adt {
+ tag: string; # canvas tag common to cards being moved.
+ srcpt: Point; # where cards are coming from.
+ cards: array of ref Object.Card; # objects being transferred.
+ dstid: int;
+ index: int;
+ waitch: chan of ref Animation; # notification comes on this chan when finished.
+};
+
+Selection: adt {
+ pick {
+ XRange =>
+ r: Range;
+ Indexes =>
+ idxl: list of int;
+ Empty =>
+ }
+};
+
+MAXPLAYERS: con 4;
+
+# layout actions
+lFRAME, lOBJECT: con iota;
+
+# possible actions on a card on a stack.
+aCLICK: con 1<<iota;
+
+# styles of stack display
+styDISPLAY, styPILE: con iota;
+
+# orientations
+oLEFT, oRIGHT, oUP, oDOWN: con iota;
+
+Range: adt {
+ start, end: int;
+};
+
+T: type ref Animation;
+Queue: adt {
+ h, t: list of T;
+ put: fn(q: self ref Queue, s: T);
+ get: fn(q: self ref Queue): T;
+ isempty: fn(q: self ref Queue): int;
+ peek: fn(q: self ref Queue): T;
+};
+
+configcmds := array[] of {
+"frame .buts",
+"frame .cf",
+"canvas .c -width 400 -height 450 -bg green",
+"label .status -text 0",
+"checkbutton .buts.scores -text {Show scores} -command {send cmd scores}",
+"button .buts.sizetofit -text {Fit} -command {send cmd sizetofit}",
+"checkbutton .buts.debug -text {Debug} -variable debug -command {send cmd debug}",
+"pack .buts.sizetofit .buts.debug .status -in .buts -side left",
+"pack .buts -side top -fill x",
+"pack .c -in .cf -side top -fill both -expand 1",
+"pack .cf -side top -fill both -expand 1",
+"bind .c <Button-1> {send cmd b1 %X %Y}",
+"bind .c <ButtonRelease-1} {send cmd b1r %X %Y}",
+"bind .c <Button-2> {send cmd b2 %X %Y}",
+"bind .c <ButtonRelease-2> {send cmd b2r %X %Y}",
+"bind .c <ButtonPress-3> {send cmd b3 %X %Y}",
+"bind .c <ButtonRelease-3> {send cmd b3r %X %Y}",
+"bind . <Configure> {send cmd config}",
+"pack propagate .buts 0",
+".status configure -text {}",
+"pack propagate . 0",
+};
+
+objects: array of ref Object;
+layobjects := array[20] of list of ref Layobject;
+members := array[8] of list of ref Object.Member;
+win: ref Tk->Toplevel;
+drawctxt: ref Draw->Context;
+me: ref Object.Member;
+layout: ref Layout;
+scoretable: ref Object.Scoretable;
+showingscores := 0;
+debugging := 0;
+
+stderr: ref Sys->FD;
+animfinishedch: chan of (ref Animation, chan of chan of ref Animation);
+yieldch: chan of int;
+cardlockch: chan of int;
+notifych: chan of string;
+tickregisterch, tickunregisterch: chan of chan of int;
+starttime := 0;
+cvsfont: ref Font;
+
+packwin: ref Tk->Toplevel; # invisible; used to steal tk's packing algorithms...
+packobjs: list of ref Layobject;
+repackobjs: list of ref Layobject;
+needresize := 0;
+needrepack := 0;
+
+animid := 0;
+fakeid := -2; # ids allocated to "fake" cards in private hands; descending
+nimages := 0;
+Hiddenpos := Point(5000, 5000);
+
+cliquefd: ref Sys->FD;
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ draw = load Draw Draw->PATH;
+ tk = load Tk Tk->PATH;
+ math = load Math Math->PATH;
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil) {
+ sys->fprint(stderr, "cards: cannot load %s: %r\n", Tkclient->PATH);
+ raise "fail:bad module";
+ }
+ tkclient->init();
+ drawctxt = ctxt;
+ client1();
+}
+
+# maximum number of rears (overridden by actual rear images)
+rearcolours := array[] of {
+ int 16r0000ccff,
+ int 16rff0000ff,
+ int 16rffff00ff,
+ int 16r008000ff,
+ int 16rffffffff,
+ int 16rffaa00ff,
+ int 16r00ffffff,
+ int 16r808080ff,
+ int 16r00ff00ff,
+ int 16r800000ff,
+ int 16r800080ff,
+};
+Rearborder := 3;
+Border := 6;
+Selectborder := 3;
+cardsize: Point;
+carddelta := Point(12, 15); # offset in order to see card number/suit
+Selectcolour := "red";
+Textfont := "/fonts/pelm/unicode.8.font";
+
+client1()
+{
+ cliquefd = sys->fildes(0);
+ if (readconfig() == -1)
+ raise "fail:error";
+
+ winctl: chan of string;
+ (win, winctl) = tkclient->toplevel(drawctxt, "-font " + Textfont,
+ "Cards", Tkclient->Appl);
+ cmd(win, ". unmap");
+ bcmd := chan of string;
+ tk->namechan(win, bcmd, "cmd");
+ srvcmd := chan of string;
+ tk->namechan(win, srvcmd, "srv");
+
+ if (readcardimages() == -1)
+ raise "fail:error";
+ for (i := 0; i < len configcmds; i++)
+ cmd(win, configcmds[i]);
+ tkclient->onscreen(win, nil);
+ tkclient->startinput(win, "kbd"::"ptr"::nil);
+
+ fontname := cmd(win, ".c cget -font");
+ cvsfont = Font.open(drawctxt.display, fontname);
+ if (cvsfont == nil) {
+ sys->fprint(stderr, "cards: cannot open font %s: %r\n", fontname);
+ raise "fail:error";
+ }
+ fontname = nil;
+
+ cardlockch = chan of int;
+ spawn lockproc();
+
+ yieldch = chan of int;
+ spawn yieldproc();
+
+ notifych = chan of string;
+ spawn notifierproc();
+
+ updatech := chan of array of byte;
+ spawn readproc(cliquefd, updatech);
+
+ spawn updateproc(updatech);
+ b1down := 0;
+
+ tickregisterch = chan of chan of int;
+ tickunregisterch = chan of chan of int;
+ spawn timeproc();
+ spawn eventproc(win);
+
+ for (;;) alt {
+ c := <-bcmd =>
+ (n, toks) := sys->tokenize(c, " ");
+ case hd toks {
+ "b3" =>
+ curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
+ int cmd(win, ".c canvasy " + hd tl tl toks));
+ b3raise(bcmd, curp);
+ "b2" =>
+ curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
+ int cmd(win, ".c canvasy " + hd tl tl toks));
+ dopan(bcmd, "b2", curp);
+ "b1" =>
+ if (!b1down) {
+ # b1 x y
+ # x and y in screen coords
+ curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
+ int cmd(win, ".c canvasy " + hd tl tl toks));
+ b1down = b1action(bcmd, curp);
+ }
+ "b1r" =>
+ b1down = 0;
+ "entry" =>
+ id := int hd tl toks;
+ lock();
+ cc := "";
+ pick o := objects[id] {
+ Widget =>
+ cc = o.o.cmd;
+ * =>
+ sys->print("entry message from unknown obj: id %d\n", id);
+ }
+ unlock();
+ if (cc != nil) {
+ w := ".buts." + string id + ".b";
+ s := cmd(win, w + " get");
+ cardscmd(cc + " " + s);
+ cmd(win, w + " selection range 0 end");
+ cmd(win, "update");
+ }
+ "config" =>
+ lock();
+ needresize = 1;
+ updatearena();
+ unlock();
+ cmd(win, "update");
+ "scores" =>
+ if (scoretable == nil)
+ break;
+ if (!showingscores) {
+ cmd(win, ".c move score " + string -Hiddenpos.x + " " + string -Hiddenpos.y);
+ cmd(win, ".c raise score");
+ } else
+ cmd(win, ".c move score " + p2s(Hiddenpos));
+ cmd(win, "update");
+ showingscores = !showingscores;
+ "sizetofit" =>
+ lock();
+ sizetofit();
+ unlock();
+ cmd(win, "update");
+ "debug" =>
+ debugging = int cmd(win, "variable debug");
+ }
+ c := <-srvcmd => # from button or menu entry
+ cardscmd(c);
+ s := <-win.ctxt.ctl or
+ s = <-win.wreq or
+ s = <-winctl =>
+ if (s == "exit")
+ sys->write(cliquefd, array[0] of byte, 0);
+ tkclient->wmctl(win, s);
+ }
+}
+
+eventproc(win: ref Tk->Toplevel)
+{
+ for(;;)alt{
+ s := <-win.ctxt.kbd =>
+ tk->keyboard(win, s);
+ s := <-win.ctxt.ptr =>
+ tk->pointer(win, *s);
+ }
+}
+
+readproc(fd: ref Sys->FD, updatech: chan of array of byte)
+{
+ buf := rest := array[Sys->ATOMICIO * 2] of byte;
+ while ((n := sys->read(fd, rest, Sys->ATOMICIO)) > 0) {
+ updatech <-= rest[0:n];
+ rest = rest[n:];
+ if (len rest < Sys->ATOMICIO)
+ buf = rest = array[Sys->ATOMICIO * 2] of byte;
+ }
+ updatech <-= nil;
+}
+
+
+b1action(bcmd: chan of string, p: Point): int
+{
+ (hitsomething, id) := hitcard(p);
+ if (!hitsomething) {
+ dopan(bcmd, "b1", p);
+ return 0;
+ }
+ if (id < 0) { # either error, or someone else's private card
+ sys->print("no card hit (%d)\n", id);
+ return 1;
+ }
+ lock();
+ if (objects[id] == nil) {
+ notify("it's gone");
+ unlock();
+ return 1;
+ }
+ stack: ref Layobject.Stack;
+ index := -1;
+ pick o := objects[id] {
+ Card =>
+ card := o;
+ parentid := card.parentid;
+ stack = stackobj(parentid);
+ for (index = 0; index < len stack.cards; index++)
+ if (stack.cards[index] == card)
+ break;
+ if (index == len stack.cards)
+ index = -1;
+ Stack =>
+ stack = o.o;
+ * =>
+ unlock();
+ return 1;
+ }
+ actions := stack.actions;
+ stackid := stack.id;
+ unlock();
+ # XXX potential problems when object ids get reused.
+ # the object id that we saw before the unlock()
+ # might now refer to a different object, so the user
+ # might be performing a different action to the one intended.
+ # this should be changed throughout... hmm.
+ if (actions == 0) {
+ notify("no way josé");
+ sys->print("no way: stack %d, actions %d\n", stackid, actions);
+ return 1;
+ }
+ cardscmd("click " + string stackid + " " + string index);
+ return 1;
+}
+
+dopan(bcmd: chan of string, b: string, p: Point)
+{
+ r := b + "r";
+ for (;;) {
+ (n, toks) := sys->tokenize(<-bcmd, " ");
+ if (hd toks == b) {
+ pan(p, (int hd tl toks, int hd tl tl toks));
+ p = Point(int cmd(win, ".c canvasx " + hd tl toks),
+ int cmd(win, ".c canvasy " + hd tl tl toks));
+ cmd(win, "update");
+ } else if (hd toks == r)
+ return;
+ }
+}
+
+b3raise(bcmd: chan of string, p: Point)
+{
+ currcard := -1;
+ above := "";
+loop: for (;;) {
+ (nil, id) := hitcard(p);
+ if (id != currcard) {
+ if (currcard != -1 && above != nil)
+ cmd(win, ".c lower i" + string currcard + " " + above);
+ if (id == -1 || tagof(objects[id]) != tagof(Object.Card)) {
+ above = nil;
+ currcard = -1;
+ } else {
+ above = cmd(win, ".c find above i" + string id);
+ cmd(win, ".c raise i" + string id);
+ cmd(win, "update");
+ currcard = id;
+ }
+ }
+ (nil, toks) := sys->tokenize(<-bcmd, " ");
+ case hd toks {
+ "b3" =>
+ p = Point(int cmd(win, ".c canvasx " + hd tl toks),
+ int cmd(win, ".c canvasy " + hd tl tl toks));
+ "b3r" =>
+ break loop;
+ }
+ }
+ if (currcard != -1 && above != nil) {
+ cmd(win, ".c lower i" + string currcard + " " + above);
+ cmd(win, "update");
+ }
+}
+
+hitcard(p: Point): (int, int)
+{
+ (nil, hitids) := sys->tokenize(cmd(win, ".c find overlapping " + r2s((p, p))), " ");
+ if (hitids == nil)
+ return (0, -1);
+ ids: list of string;
+ for (; hitids != nil; hitids = tl hitids)
+ ids = hd hitids :: ids;
+ for (; ids != nil; ids = tl ids) {
+ (nil, tags) := sys->tokenize(cmd(win, ".c gettags " + hd ids), " ");
+ for (; tags != nil; tags = tl tags) {
+ tag := hd tags;
+ if (tag[0] == 'i' || tag[0] == 'r' || tag[0] == 'n' || tag[0] == 'N')
+ return (1, int (hd tags)[1:]);
+ if (tag[0] == 's') # ignore selection
+ break;
+ }
+ if (tags == nil)
+ break;
+ }
+ return (1, -1);
+}
+
+cardscmd(s: string): int
+{
+ if (debugging)
+ sys->print("cmd: %s\n", s);
+ if (sys->fprint(cliquefd, "%s", s) == -1) {
+ err := sys->sprint("%r");
+ notify(err);
+ sys->print("cmd error on '%s': %s\n", s, err);
+ return 0;
+ }
+ return 1;
+}
+
+updateproc(updatech: chan of array of byte)
+{
+ wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+ spawn updateproc1(updatech);
+ 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(updatech: chan of array of byte)
+{
+ animfinishedch = chan of (ref Animation, chan of chan of ref Animation);
+ first := 1;
+ for (;;) {
+ alt {
+ v := <-animfinishedch =>
+ lock();
+ animterminated(v);
+ updatearena();
+ cmd(win, "update");
+ unlock();
+ u := <-updatech =>
+ if (u == nil) {
+ # XXX notify user that clique has been hung up somehow
+ exit;
+ }
+ moretocome := 0;
+ if (len u > 2 && u[len u-1] == byte '*' && u[len u-2] == byte '\n') {
+ u = u[0:len u - 2];
+ moretocome = 1;
+ }
+ (nil, lines) := sys->tokenize(string u, "\n");
+ lock();
+ starttime = sys->millisec();
+ for (; lines != nil; lines = tl lines)
+ applyupdate(hd lines);
+ updatearena();
+ if (!moretocome) {
+ if (first) {
+ sizetofit();
+ first = 0;
+ }
+ cmd(win, "update");
+ }
+ unlock();
+ }
+ }
+}
+
+updatearena()
+{
+ if (needrepack)
+ repackall();
+ if (needresize)
+ resizeall();
+ for (pstk := repackobjs; pstk != nil; pstk = tl pstk)
+ repackobj(hd pstk);
+ repackobjs = nil;
+}
+
+applyupdate(s: string)
+{
+ if (debugging) {
+ sys->print("update: %s\n", s);
+# showtk = 1;
+ }
+ (nt, toks) := sys->tokenize(s, " ");
+ case hd toks {
+ "create" =>
+ # create id parentid vis type
+ id := int hd tl toks;
+ if (id >= len objects)
+ objects = (array[len objects + 10] of ref Object)[0:] = objects;
+ if (objects[id] != nil)
+ panic(sys->sprint("object %d already exists!", id));
+ parentid := int hd tl tl toks;
+ vis := int hd tl tl tl toks;
+ objtype := tl tl tl tl toks;
+ case hd objtype {
+ "stack" =>
+ objects[id] = makestack(id, parentid, vis);
+ needrepack = 1;
+ "card" =>
+ stk := stackobj(parentid);
+ completeanim(stk);
+ if (!stk.visible) {
+ # if creating in a private stack, we assume
+ # that the cards were there already, and
+ # just make them real again.
+
+ # first find a fake card.
+ for (i := 0; i < len stk.cards; i++)
+ if (stk.cards[i].id < 0)
+ break;
+ c: ref Object.Card;
+ if (i == len stk.cards) {
+ # no fake cards - we'll create one instead.
+ # this can happen if we've entered halfway through
+ # a clique, so don't know how many cards people
+ # are holding.
+ c = makecard(id, stk);
+ insertcards(stk, array[] of {c}, len stk.cards);
+ } else {
+ c = stk.cards[i];
+ changecardid(c, id);
+ }
+ objects[id] = c;
+ } else {
+ objects[id] = c := makecard(id, stk);
+ insertcards(stk, array[] of {c}, len stk.cards);
+ }
+ "widget" =>
+ objects[id] = makewidget(id, parentid, hd tl objtype);
+ "menuentry" =>
+ objects[id] = makemenuentry(id, parentid, tl objtype);
+ "member" =>
+ objects[id] = ref Object.Member(id, -1, "");
+ "layframe" =>
+ lay := ref Layout.Frame(id, parentid, "", -1, nil);
+ objects[id] = ref Object.Layoutframe(id, lay);
+ addlayout(lay);
+ "layobj" =>
+ lay := ref Layout.Obj(id, parentid, "", -1, -1);
+ objects[id] = ref Object.Layoutobj(id, lay);
+ addlayout(lay);
+ "scoretable" =>
+ if (scoretable != nil)
+ panic("cannot make two scoretables");
+ scoretable = objects[id] = ref Object.Scoretable(id, nil);
+ "score" =>
+ pick l := objects[parentid] {
+ Scoretable =>
+ nl := array[len l.scores + 1] of ref Object.Score;
+ nl[0:] = l.scores;
+ nl[len nl - 1] = objects[id] = ref Object.Score(id, nil, 0);
+ l.scores = nl;
+ cmd(win, "pack .buts.scores -side left");
+ * =>
+ panic("score created outside scoretable object");
+ }
+ "button" =>
+ objects[id] = ref Object.Button(id);
+ cmd(win, "button .buts." + string id);
+ cmd(win, "pack .buts." + string id + " -side left");
+ * =>
+ if (parentid != -1)
+ sys->print("cards: unknown objtype: '%s'\n", hd objtype);
+ objects[id] = ref Object.Other(id);
+ }
+
+ "tx" =>
+ # tx src dst start end dstindex
+ src, dst: ref Layobject.Stack;
+ index: int;
+ r: Range;
+ (src, toks) = (stackobj(int hd tl toks), tl tl toks);
+ (dst, toks) = (stackobj(int hd toks), tl toks);
+ (r.start, toks) = (int hd toks, tl toks);
+ (r.end, toks) = (int hd toks, tl toks);
+ (index, toks) = (int hd toks, tl toks);
+ transfer(src, r, dst, index);
+
+ "del" =>
+ # del parent start end objs...
+ oo := objects[int hd tl toks]; # parent
+ r := Range(int hd tl tl toks, int hd tl tl tl toks);
+ pick o := oo {
+ Stack => # deleting cards from a stack.
+ stk := o.o;
+ completeanim(stk);
+ if (!stk.visible) {
+ # if deleting from a private area, we assume the cards aren't
+ # actually being deleted at all, but merely becoming
+ # invisible, so turn them into fakes.
+ for (i := r.start; i < r.end; i++) {
+ card := stk.cards[i];
+ objects[card.id] = nil;
+ changecardid(card, --fakeid);
+ cardsetattr(card, "face", "0" :: nil);
+ }
+ } else {
+ cards := extractcards(stk, r);
+ for (i := 0; i < len cards; i++)
+ destroy(cards[i]);
+ }
+ Layoutframe => # deleting the layout specification.
+ lay := o.lay;
+ if (r.start != 0 || r.end != len lay.lays)
+ panic("cannot partially delete layouts");
+ for (i := r.start; i < r.end; i++)
+ destroy(objects[lay.lays[i].id]);
+ lay.lays = nil;
+ needrepack = 1;
+ Widget =>
+ # must be a menu widget
+ cmd(win, ".buts." + string o.id + ".m delete " +
+ string r.start + " " + string r.end);
+ * =>
+ for (objs := tl tl tl tl toks; objs != nil; objs = tl objs)
+ destroy(objects[int hd objs]);
+ }
+
+ "set" =>
+ # set obj attr val
+ id := int hd tl toks;
+ (attr, val) := (hd tl tl toks, tl tl tl toks);
+ pick o := objects[id] {
+ Card =>
+ cardsetattr(o, attr, val);
+ Widget =>
+ widgetsetattr(o.o, attr, val);
+ Stack =>
+ stacksetattr(o.o, attr, val);
+ Member =>
+ membersetattr(o, attr, val);
+ Layoutframe =>
+ laysetattr(o.lay, attr, val);
+ Layoutobj =>
+ laysetattr(o.lay, attr, val);
+ Score =>
+ scoresetattr(o, attr, val);
+ Button =>
+ buttonsetattr(o, attr, val);
+ Menuentry =>
+ menuentrysetattr(o, attr, val);
+ * =>
+ sys->fprint(stderr, "unknown attr set on object(tag %d), %s\n", tagof(objects[id]), s);
+ }
+
+ "say" or
+ "remark" =>
+ notify(join(tl toks));
+ * =>
+ sys->fprint(stderr, "cards: unknown update message '%s'\n", s);
+ }
+}
+
+addlayout(lay: ref Layout)
+{
+ pick lo := objects[lay.parentid] {
+ Layoutframe =>
+ l := lo.lay;
+ nl := array[len l.lays + 1] of ref Layout;
+ nl[0:] = l.lays;
+ nl[len nl - 1] = lay;
+ l.lays = nl;
+ * =>
+ if (layout == nil)
+ layout = lay;
+ else
+ panic("cannot make two layout objects");
+ }
+}
+
+makestack(id, parentid: int, vis: int): ref Object.Stack
+{
+ o := ref Object.Stack(
+ id,
+ ref Layobject.Stack(
+ id,
+ parentid,
+ "", # pack widget name
+ (0, 0), # size
+ 0, # needrepack
+ -1, # orientation
+ -1, # layid
+ -1, # style
+ nil, # cards
+ Hiddenpos, # pos
+ (0, 0), # delta
+ ref Queue,
+ 0, # actions
+ 0, # maxcards
+ "", # title
+ vis, # visible
+ 0, # n
+ -1, # ownerid
+ ref Selection.Empty, # sel
+ 1, # showsize
+ 0 # hassize
+ )
+ );
+ cmd(win, ".c create rectangle -10 -10 -10 -10 -width 3 -tags r" + string id);
+ return o;
+}
+
+makewidget(id, parentid: int, wtype: string): ref Object.Widget
+{
+ wctype := wtype;
+ if (wtype == "menu")
+ wctype = "menubutton";
+ # XXX the widget is put in a frame 'cos of bugs in the canvas
+ # to do with size propagation.
+ w := cmd(win, "frame .buts." + string id + " -bg transparent");
+ cmd(win, wctype + " " + w + ".b");
+ cmd(win, "pack " + w + ".b -fill both -expand 1");
+ case wtype {
+ "menu" =>
+ cmd(win, "menu " + w + ".m");
+ cmd(win, w + ".b configure -menu " + w + ".m" +
+ " -relief raised");
+ "entry" =>
+ cmd(win, "bind " + w + ".b <Key-\n> {send cmd entry " + string id + "}");
+ }
+ cmd(win, ".c create window -1000 -1000 -tags r" + string id +
+ " -window " + w + " -anchor nw");
+ o := ref Object.Widget(
+ id,
+ ref Layobject.Widget(
+ id,
+ parentid,
+ nil, # w
+ (0, 0), # size
+ 0, # needrepack
+ -1, # orientation
+ -1, # style
+
+ wtype,
+ nil, # entries
+ "", # cmd
+ 0 # width
+ )
+ );
+ return o;
+}
+
+menutitleid := 0; # hack to identify menu entries
+makemenuentry(id, parentid: int, nil: list of string): ref Object.Menuentry
+{
+ m := ".buts." + string parentid + ".m";
+ t := "@" + string menutitleid++;
+ cmd(win, m + " add command -text " + t);
+ return ref Object.Menuentry(id, parentid, t);
+}
+
+makecard(id: int, stack: ref Layobject.Stack): ref Object.Card
+{
+ cmd(win, ".c create image 5000 5000 -anchor nw -tags i" + string id);
+ return ref Object.Card(id, stack.id, -1, -1, 0);
+}
+
+buttonsetattr(b: ref Object.Button, attr: string, val: list of string)
+{
+ w := ".buts." + string b.id;
+ case attr {
+ "text" =>
+ cmd(win, w + " configure -text '" + join(val));
+ "command" =>
+ cmd(win, w + " configure -command 'send srv " + join(val));
+ * =>
+ sys->print("unknown attribute on button: %s\n", attr);
+ }
+}
+
+widgetsetattr(b: ref Layobject.Widget, attr: string, val: list of string)
+{
+ w := ".buts." + string b.id + ".b";
+ case attr {
+ "text" =>
+ t := join(val);
+ if (b.wtype == "entry") {
+ cmd(win, w + " delete 0 end");
+ cmd(win, w + " insert 0 '" + t);
+ cmd(win, w + " select 0 end"); # XXX ??
+ } else {
+ cmd(win, w + " configure -text '" + t);
+ needresize = 1;
+ }
+ "command" =>
+ case b.wtype {
+ "button" =>
+ cmd(win, w + " configure -command 'send srv " + join(val));
+ "entry" =>
+ b.cmd = join(val);
+ }
+ "width" => # width in characters
+ b.width = int hd val;
+ sys->print("configuring %s for width %s\n", w, hd val);
+ cmd(win, w + " configure -width " + hd val + "w");
+ needresize = 1;
+ "layid" =>
+ setlayid(b, int hd val);
+ * =>
+ sys->print("unknown attribute on button: %s\n", attr);
+ }
+}
+
+findmenuentry(m: string, title: string): int
+{
+ end := int cmd(win, m + " index end");
+ for (i := 0; i <= end; i++) {
+ t := cmd(win, m + " entrycget " + string i + " -text");
+ if (t == title)
+ return i;
+ }
+ return -1;
+}
+
+menuentrysetattr(e: ref Object.Menuentry, attr: string, val: list of string)
+{
+ m := ".buts." + string e.parentid + ".m";
+ idx := findmenuentry(m, e.text);
+ if (idx == -1) {
+ sys->print("couldn't find menu entry '%s'\n", e.text);
+ return;
+ }
+ case attr {
+ "text" =>
+ t := join(val);
+ cmd(win, m + " entryconfigure " + string idx +" -text '" + t);
+ e.text = t;
+ "command" =>
+ cmd(win, m + " entryconfigure " + string idx +
+ " -command 'send srv " + join(val));
+ * =>
+ sys->print("unknown attribute on menu entry: %s\n", attr);
+ }
+}
+
+stacksetattr(stack: ref Layobject.Stack, attr: string, val: list of string)
+{
+ id := string stack.id;
+ case attr {
+ "maxcards" =>
+ stack.maxcards = int hd val;
+ needresize = 1;
+ "layid" =>
+ setlayid(stack, int hd val);
+ "showsize" =>
+ stack.showsize = int hd val;
+ showsize(stack);
+ "title" =>
+ title := join(val);
+ if (title != stack.title) {
+ if (stack.title == nil) {
+ cmd(win, ".c create text 5000 6000 -anchor n -tags t" + string id +
+ " -fill #ffffaa");
+ needresize = 1;
+ } else if (title == nil) {
+ cmd(win, ".c delete t" + string id);
+ needresize = 1;
+ }
+ if (title != nil)
+ cmd(win, ".c itemconfigure t" + string id + " -text '" + title);
+ stack.title = title;
+ }
+ "n" =>
+ # there are "n" cards in this stack, honest guv.
+ n := int hd val;
+ if (!stack.visible) {
+ if (n > len stack.cards) {
+ a := array[n - len stack.cards] of ref Object.Card;
+ for (i := 0; i < len a; i++) {
+ a[i] = makecard(--fakeid, stack);
+ cardsetattr(a[i], "face", "0" :: nil);
+ }
+ insertcards(stack, a, len stack.cards);
+ } else if (n < len stack.cards) {
+ for (i := len stack.cards - 1; i >= n; i--)
+ if (stack.cards[i].id >= 0)
+ break;
+ cards := extractcards(stack, (i + 1, len stack.cards));
+ for (i = 0; i < len cards; i++)
+ destroy(cards[i]);
+ }
+ }
+ stack.n = n;
+ "style" =>
+ case hd val {
+ "pile" =>
+ stack.style = styPILE;
+ "display" =>
+ stack.style = styDISPLAY;
+ * =>
+ sys->print("unknown stack style '%s'\n", hd val);
+ }
+ needresize = 1;
+ "owner" =>
+ if (val != nil)
+ stack.ownerid = int hd val;
+ else
+ stack.ownerid = -1;
+ changesel(stack, stack.sel);
+ "sel" =>
+ sel: ref Selection;
+ if (val == nil)
+ sel = ref Selection.Empty;
+ else if (tl val != nil && hd tl val == "-")
+ sel = ref Selection.XRange((int hd val, int hd tl tl val));
+ else {
+ idxl: list of int;
+ for (; val != nil; val = tl val)
+ idxl = int hd val :: idxl;
+ sel = ref Selection.Indexes(idxl);
+ }
+ changesel(stack, sel);
+ * =>
+ if (len attr >= len "actions" && attr[0:len "actions"] == "actions") {
+ oldactions := stack.actions;
+ act := 0;
+ for (; val != nil; val = tl val) {
+ case hd val {
+ "click" =>
+ act |= aCLICK;
+ * =>
+ sys->print("unknown action '%s'\n", hd val);
+ }
+ }
+ stack.actions = act;
+ } else
+ sys->fprint(stderr, "bad stack attr '%s'\n", attr);
+ }
+}
+
+showsize(stack: ref Layobject.Stack)
+{
+ id := string stack.id;
+ needsize := stack.showsize && len stack.cards > 0 && stack.style == styPILE;
+ if (needsize != stack.hassize) {
+ if (stack.hassize)
+ cmd(win, ".c delete n" + id + " N" + id);
+ else {
+ cmd(win, ".c create rectangle -5000 0 0 0 -fill #ffffaa -tags n" + id);
+ cmd(win, ".c create text -5000 0 -anchor sw -fill red -tags N" + id);
+ }
+ stack.hassize = needsize;
+ }
+ if (needsize) {
+ cmd(win, ".c itemconfigure N" + id + " -text " + string len stack.cards);
+ sr := cardrect(stack, (len stack.cards - 1, len stack.cards));
+ cmd(win, ".c coords N" + id + " " + p2s((sr.min.x, sr.max.y)));
+ bbox := cmd(win, ".c bbox N" + id);
+ cmd(win, ".c coords n" + id + " " + bbox);
+ cmd(win, ".c raise n" + id + "; .c raise N" + id);
+ }
+}
+
+changesel(stack: ref Layobject.Stack, newsel: ref Selection)
+{
+ sid := "s" + string stack.id;
+ cmd(win, ".c delete " + sid);
+
+ if (me != nil && stack.ownerid == me.cid) {
+ pick sel := newsel {
+ Indexes =>
+ for (l := sel.idxl; l != nil; l = tl l) {
+ s := cmd(win, ".c create rectangle " +
+ r2s(cardrect(stack, (hd l, hd l + 1)).inset(-1)) +
+ " -width " + string Selectborder +
+ " -outline " + Selectcolour +
+ " -tags {" + sid + " " + sid + "." + string hd l + "}");
+ cmd(win, ".c lower " + s + " i" + string stack.cards[hd l].id);
+ }
+ XRange =>
+ cmd(win, ".c create rectangle " +
+ r2s(cardrect(stack, sel.r).inset(-1)) +
+ " -outline " + Selectcolour +
+ " -width " + string Selectborder +
+ " -tags " + sid);
+ }
+ }
+ stack.sel = newsel;
+}
+
+cardsetattr(card: ref Object.Card, attr: string, val: list of string)
+{
+ id := string card.id;
+ case attr {
+ "face" =>
+ card.face = int hd val;
+ if (card.face) {
+ if (card.number != -1)
+ cmd(win, ".c itemconfigure i" + id + " -image c" + string card.number );
+ } else
+ cmd(win, ".c itemconfigure i" + id + " -image rear" + string card.rear);
+ "number" =>
+ card.number = int hd val;
+ if (card.face)
+ cmd(win, ".c itemconfigure i" + id + " -image c" + string card.number );
+ "rear" =>
+ card.rear = int hd val;
+ if (card.face == 0)
+ cmd(win, ".c itemconfigure i" + id + " -image rear" + string card.rear);
+ * =>
+ sys->print("unknown attribute on card: %s\n", attr);
+ }
+}
+
+setlayid(layobj: ref Layobject, layid: int)
+{
+ if (layobj.layid != -1)
+ panic("obj already has a layout id (" + string layobj.layid + ")");
+ layobj.layid = layid;
+ x := layobj.layid % len layobjects;
+ layobjects[x] = layobj :: layobjects[x];
+ needrepack = 1;
+}
+
+membersetattr(p: ref Object.Member, attr: string, val: list of string)
+{
+ case attr {
+ "you" =>
+ me = p;
+ p.cid = int hd val;
+ for (i := 0; i < len objects; i++) {
+ if (objects[i] != nil) {
+ pick o := objects[i] {
+ Stack =>
+ if (o.o.ownerid == p.cid)
+ objneedsrepack(o.o);
+ }
+ }
+ }
+ "name" =>
+ p.name = hd val;
+ "id" =>
+ p.cid = int hd val;
+ "status" =>
+ if (p == me)
+ cmd(win, ".status configure -text '" + join(val));
+ "cliquetitle" =>
+ if (p == me)
+ tkclient->settitle(win, join(val));
+ * =>
+ sys->print("unknown attribute on member: %s\n", attr);
+ }
+}
+
+laysetattr(lay: ref Layout, attr: string, val: list of string)
+{
+ case attr {
+ "opts" =>
+ # orientation opts
+ case hd val {
+ "up" =>
+ lay.orientation = oUP;
+ "down" =>
+ lay.orientation = oDOWN;
+ "left" =>
+ lay.orientation = oLEFT;
+ "right" =>
+ lay.orientation = oRIGHT;
+ * =>
+ sys->print("unknown orientation '%s'\n", hd val);
+ }
+ lay.opts = join(tl val);
+ "layid" =>
+# sys->print("layout obj %d => layid %s\n", lay.id, hd val);
+ pick l := lay {
+ Obj =>
+ l.layid = int hd val;
+ needrepack = 1;
+ * =>
+ sys->print("cannot set layid on Layout.Frame!\n");
+ }
+ * =>
+ sys->print("unknown attribute on lay: %s\n", attr);
+ }
+ needrepack = 1;
+}
+
+scoresetattr(score: ref Object.Score, attr: string, val: list of string)
+{
+ if (attr != "score")
+ return;
+ cmd(win, ".c delete score");
+
+ Padx: con 10; # padding to the right of each item
+ Pady: con 6; # padding below each item.
+
+ n := len val;
+ row := score.row = array[n] of (int, string);
+ height := 0;
+
+ # calculate values for this row
+ for ((col, vl) := (0, val); vl != nil; (col, vl) = (col + 1, tl vl)) {
+ v := hd vl;
+ size := textsize(v);
+ size.y += Pady;
+ if (size.y > height)
+ height = size.y;
+ row[col] = (size.x + Padx, v);
+ }
+ score.height = height;
+ totheight := 0;
+ scores := scoretable.scores;
+
+ # calculate number of columns
+ ncols := 0;
+ for (i := 0; i < len scores; i++)
+ if (len scores[i].row > ncols)
+ ncols = len scores[i].row;
+
+ # calculate column widths
+ colwidths := array[ncols] of {* => 0};
+ for (i = 0; i < len scores; i++) {
+ r := scores[i].row;
+ for (j := 0; j < len r; j++) {
+ (w, nil) := r[j];
+ if (w > colwidths[j])
+ colwidths[j] = w;
+ }
+ totheight += scores[i].height;
+ }
+ # create all table items
+ p := Hiddenpos;
+ for (i = 0; i < len scores; i++) {
+ p.x = Hiddenpos.x;
+ r := scores[i].row;
+ for (j := 0; j < len r; j++) {
+ (w, text) := r[j];
+ cmd(win, ".c create text " + p2s(p) + " -anchor nw -tags {score scoreent}-text '" + text);
+ p.x += colwidths[j];
+ }
+ p.y += scores[i].height;
+ }
+ r := Rect(Hiddenpos, p);
+ r.min.x -= Padx;
+ r.max.y -= Pady / 2;
+
+ cmd(win, ".c create rectangle " + r2s(r) + " -fill #ffffaa -tags score");
+
+ # horizontal lines
+ y := 0;
+ for (i = 0; i < len scores - 1; i++) {
+ ly := y + scores[i].height - Pady / 2;
+ cmd(win, ".c create line " + r2s(((r.min.x, ly), (r.max.x, ly))) + " -fill gray -tags score");
+ y += scores[i].height;
+ }
+
+ cmd(win, ".c raise scoreent");
+ cmd(win, ".c move score " + p2s(Hiddenpos.sub(r.min)));
+}
+
+textsize(s: string): Point
+{
+ return (cvsfont.width(s), cvsfont.height);
+}
+
+changecardid(c: ref Object.Card, newid: int)
+{
+ (nil, tags) := sys->tokenize(cmd(win, ".c gettags i" + string c.id), " ");
+ for (; tags != nil; tags = tl tags) {
+ tag := hd tags;
+ if (tag[0] >= '0' && tag[0] <= '9')
+ break;
+ }
+ cvsid := hd tags;
+ cmd(win, ".c dtag " + cvsid + " i" + string c.id);
+ c.id = newid;
+ cmd(win, ".c addtag i" + string c.id + " withtag " + cvsid);
+}
+
+stackobj(id: int): ref Layobject.Stack
+{
+ obj := objects[id];
+ if (obj == nil)
+ panic("nil stack object");
+ pick o := obj {
+ Stack =>
+ return o.o;
+ * =>
+ panic("expected obj " + string id + " to be a stack");
+ }
+ return nil;
+}
+
+# if there are updates pending on the stack,
+# then wait for them all to finish before we can do
+# any operations on the stack (e.g. insert, delete, create, etc)
+completeanim(stk: ref Layobject.Stack)
+{
+ while (!stk.animq.isempty())
+ animterminated(<-animfinishedch);
+}
+
+transfer(src: ref Layobject.Stack, r: Range, dst: ref Layobject.Stack, index: int)
+{
+ # we don't bother animating movement within a stack; maybe later?
+ if (src == dst) {
+ transfercards(src, r, dst, index);
+ return;
+ }
+ completeanim(src);
+
+ if (!src.visible) {
+ # cards being transferred out of private area should
+ # have already been created, but check anyway.
+ if (r.start != 0)
+ panic("bad transfer out of private");
+ for (i := 0; i < r.end; i++)
+ if (src.cards[i].id < 0)
+ panic("cannot transfer fake card");
+ }
+
+ startanimating(newanimation(src, r), dst, index);
+}
+
+objneedsrepack(obj: ref Layobject)
+{
+ if (!obj.needrepack) {
+ obj.needrepack = 1;
+ repackobjs = obj :: repackobjs;
+ }
+}
+
+repackobj(obj: ref Layobject)
+{
+ pick o := obj {
+ Stack =>
+ cards := o.cards;
+ pos := o.pos;
+ delta := o.delta;
+ for (i := 0; i < len cards; i++) {
+ p := pos.add(delta.mul(i));
+ id := string cards[i].id;
+ cmd(win, ".c coords i" + id + " " + p2s(p));
+ cmd(win, ".c raise i" + id); # XXX could be more efficient.
+ cmd(win, ".c lower s" + string o.id + "." + string i + " i" + id);
+ }
+ changesel(o, o.sel);
+ showsize(o);
+ }
+ obj.needrepack = 0;
+}
+
+cardrect(stack: ref Layobject.Stack, r: Range): Rect
+{
+ if (r.start == r.end)
+ return ((-10, -10), (-10, -10));
+ cr := Rect((0, 0), cardsize).addpt(stack.pos);
+ delta := stack.delta;
+ return union(cr.addpt(delta.mul(r.start)), cr.addpt(delta.mul(r.end - 1)));
+}
+
+repackall()
+{
+ sys->print("repackall()\n");
+ needrepack = 0;
+ if (layout == nil) {
+ sys->print("no layout\n");
+ return;
+ }
+ if (packwin == nil) {
+ # use an unmapped tk window to do our packing arrangements
+ packwin = tk->toplevel(drawctxt.display, "-bd 0");
+ packwin.wreq = nil; # stop window requests piling up.
+ }
+ cmd(packwin, "destroy " + cmd(packwin, "pack slaves ."));
+ packobjs = nil;
+ packit(layout, ".0");
+ sys->print("%d packobjs\n", len packobjs);
+ needresize = 1;
+}
+
+# make the frames for the objects to be laid out, in the
+# offscreen window.
+packit(lay: ref Layout, f: string)
+{
+ cmd(packwin, "frame " + f);
+ cmd(packwin, "pack " + f + " " + lay.opts);
+ pick l := lay {
+ Frame =>
+ for (i := 0; i < len l.lays; i++)
+ packit(l.lays[i], f + "." + string i);
+ Obj =>
+ if ((obj := findlayobject(l.layid)) != nil) {
+ obj.w = f;
+ obj.orientation = l.orientation;
+ packobjs = obj :: packobjs;
+ } else
+ sys->print("cannot find layobject %d\n", l.layid);
+ }
+}
+
+sizetofit()
+{
+ if (packobjs == nil)
+ return;
+ cmd(packwin, "pack propagate . 1");
+ cmd(packwin, ". configure -width 0 -height 0"); # make sure propagation works.
+ csz := actsize(packwin, ".");
+ cmd(win, "bind . <Configure> {}");
+ cmd(win, "pack propagate . 1");
+ cmd(win, ". configure -width 0 -height 0");
+
+ cmd(win, ".c configure -width " + string csz.x + " -height " + string csz.y
+ + " -scrollregion {0 0 " + p2s(csz) + "}");
+ winr := actrect(win, ".");
+ screenr := win.image.screen.image.r;
+ if (!winr.inrect(screenr)) {
+ if (winr.dx() > screenr.dx())
+ (winr.min.x, winr.max.x) = (screenr.min.x, screenr.max.x);
+ if (winr.dy() > screenr.dy())
+ (winr.min.y, winr.max.y) = (screenr.min.y, screenr.max.y);
+ if (winr.max.x > screenr.max.x)
+ (winr.min.x, winr.max.x) = (screenr.max.x - winr.dx(), screenr.max.x);
+ if (winr.max.y > screenr.max.y)
+ (winr.min.y, winr.max.y) = (screenr.max.y - winr.dy(), screenr.max.y);
+ }
+ cmd(win, "pack propagate . 0");
+ cmd(win, ". configure " +
+ " -x " + string winr.min.x +
+ " -y " + string winr.min.y +
+ " -width " + string winr.dx() +
+ " -height " + string winr.dy());
+ needresize = 1;
+ updatearena();
+ cmd(win, "bind . <Configure> {send cmd config}");
+}
+
+setorigin(r: Rect, p: Point): Rect
+{
+ sz := Point(r.max.x - r.min.x, r.max.y - r.min.y);
+ return (p, p.add(sz));
+}
+
+resizeall()
+{
+ needresize = 0;
+ if (packobjs == nil)
+ return;
+ cmd(packwin, "pack propagate . 1");
+ cmd(packwin, ". configure -width 0 -height 0"); # make sure propagation works.
+ for (sl := packobjs; sl != nil; sl = tl sl) {
+ obj := hd sl;
+ sizeobj(obj);
+ cmd(packwin, obj.w + " configure -width " + string obj.size.x +
+ " -height " + string obj.size.y);
+ }
+ csz := actsize(packwin, ".");
+ sz := actsize(win, ".cf");
+ if (sz.x > csz.x || sz.y > csz.y) {
+ cmd(packwin, "pack propagate . 0");
+ if (sz.x > csz.x) {
+ cmd(packwin, ". configure -width " + string sz.x);
+ cmd(win, ".c xview moveto 0");
+ csz.x = sz.x;
+ }
+ if (sz.y > csz.y) {
+ cmd(packwin, ". configure -height " + string sz.y);
+ cmd(win, ".c yview moveto 0");
+ csz.y = sz.y;
+ }
+ }
+ cmd(win, ".c configure -width " + string csz.x + " -height " + string csz.y
+ + " -scrollregion {0 0 " + p2s(csz) + "}");
+ onscreen();
+ for (sl = packobjs; sl != nil; sl = tl sl) {
+ obj := hd sl;
+ r := actrect(packwin, obj.w);
+ positionobj(obj, r);
+ }
+}
+
+# make sure that there aren't any unnecessary blank
+# bits in the scroll area.
+onscreen()
+{
+ (n, toks) := sys->tokenize(cmd(win, ".c xview"), " ");
+ cmd(win, ".c xview moveto " + hd toks);
+ (n, toks) = sys->tokenize(cmd(win, ".c yview"), " ");
+ cmd(win, ".c yview moveto " + hd toks);
+}
+
+# work out the size of an object to be laid out.
+sizeobj(obj: ref Layobject)
+{
+ pick o := obj {
+ Stack =>
+ delta := Point(0, 0);
+ case o.style {
+ styDISPLAY =>
+ case o.orientation {
+ oRIGHT => delta.x = carddelta.x;
+ oLEFT => delta.x = -carddelta.x;
+ oDOWN => delta.y = carddelta.y;
+ oUP => delta.y = -carddelta.y;
+ }
+ styPILE =>
+ ; # no offset
+ }
+ o.delta = delta;
+ r := Rect((0, 0), size(cardrect(o, (0, max(len o.cards, o.maxcards)))));
+ if (o.title != nil) {
+ p := Point(r.min.x + r.dx() / 2, r.min.y);
+ tr := s2r(cmd(win, ".c bbox t" + string o.id));
+ tbox := Rect((p.x - tr.dx() / 2, p.y - tr.dy()), (p.x + tr.dx() / 2, p.y));
+ r = union(r, tbox);
+ }
+ o.size = r.max.sub(r.min).add((Border * 2, Border * 2));
+# sys->print("sized stack %d => %s\n", o.id, p2s(o.size));
+ Widget =>
+ w := ".buts." + string o.id;
+ o.size.x = int cmd(win, w + " cget -width");
+ o.size.y = int cmd(win, w + " cget -height");
+# sys->print("sized widget %d (%s) => %s\n", o.id,
+# cmd(win, "winfo class " + w + ".b"), p2s(o.size));
+ }
+}
+
+# set a laid-out object's position on the canvas, given
+# its allocated rectangle, r.
+positionobj(obj: ref Layobject, r: Rect)
+{
+ pick o := obj {
+ Stack =>
+# sys->print("positioning stack %d, r %s\n", o.id, r2s(r));
+ delta := o.delta;
+ sz := o.size.sub((Border * 2, Border * 2));
+ r.min.x += (r.dx() - sz.x) / 2;
+ r.min.y += (r.dy() - sz.y) / 2;
+ r.max = r.min.add(sz);
+ if (o.title != nil) {
+ cmd(win, ".c coords t" +string o.id + " " +
+ string (r.min.x + r.dx() / 2) + " " + string r.min.y);
+ tr := s2r(cmd(win, ".c bbox t" + string o.id));
+ r.min.y = tr.max.y;
+ sz = size(cardrect(o, (0, max(len o.cards, o.maxcards))));
+ r.min.x += (r.dx() - sz.x) / 2;
+ r.min.y += (r.dy() - sz.y) / 2;
+ r.max = r.min.add(sz);
+ }
+ o.pos = r.min;
+ if (delta.x < 0)
+ o.pos.x = r.max.x - cardsize.x;
+ if (delta.y < 0)
+ o.pos.y = r.max.y - cardsize.y;
+ cmd(win, ".c coords r" + string o.id + " " + r2s(r.inset(-(Border / 2))));
+ objneedsrepack(o);
+ Widget =>
+# sys->print("positioning widget %d, r %s\n", o.id, r2s(r));
+ cmd(win, ".c coords r" + string o.id + " " + p2s(r.min));
+ bd := int cmd(win, ".buts." + string o.id + " cget -bd");
+ cmd(win, ".c itemconfigure r" + string o.id +
+ " -width " + string (r.dx() - bd * 2) +
+ " -height " + string (r.dy() - bd * 2));
+ }
+}
+
+size(r: Rect): Point
+{
+ return r.max.sub(r.min);
+}
+
+transfercards(src: ref Layobject.Stack, r: Range, dst: ref Layobject.Stack, index: int)
+{
+ cards := extractcards(src, r);
+ n := r.end - r.start;
+ # if we've just removed some cards from the destination,
+ # then adjust the destination index accordingly.
+ if (src == dst && index > r.start) {
+ if (index < r.end)
+ index = r.start;
+ else
+ index -= n;
+ }
+ insertcards(dst, cards, index);
+}
+
+extractcards(src: ref Layobject.Stack, r: Range): array of ref Object.Card
+{
+ if (len src.cards > src.maxcards)
+ needresize = 1;
+ deltag(src.cards[r.start:r.end], "c" + string src.id);
+ n := r.end - r.start;
+ cards := src.cards[r.start:r.end];
+ newcards := array[len src.cards - n] of ref Object.Card;
+ newcards[0:] = src.cards[0:r.start];
+ newcards[r.start:] = src.cards[r.end:];
+ src.cards = newcards;
+ objneedsrepack(src); # XXX not necessary if moving from top?
+ return cards;
+}
+
+insertcards(dst: ref Layobject.Stack, cards: array of ref Object.Card, index: int)
+{
+ n := len cards;
+ newcards := array[len dst.cards + n] of ref Object.Card;
+ newcards[0:] = dst.cards[0:index];
+ newcards[index + n:] = dst.cards[index:];
+ newcards[index:] = cards;
+ dst.cards = newcards;
+
+ for (i := 0; i < len cards; i++)
+ cards[i].parentid = dst.id;
+ addtag(dst.cards[index:index + n], "c" + string dst.id);
+ objneedsrepack(dst); # XXX not necessary if adding to top?
+ if (len dst.cards > dst.maxcards)
+ needresize = 1;
+}
+
+destroy(obj: ref Object)
+{
+ if (obj.id >= 0)
+ objects[obj.id] = nil;
+ id := string obj.id;
+ pick o := obj {
+ Card =>
+ cmd(win, ".c delete i" + id); # XXX crashed here once...
+ Widget =>
+ cmd(win, ".c delete r" + id);
+ w := ".buts." + id;
+ cmd(win, "destroy " + w);
+ dellayobject(o.o);
+ Stack =>
+ completeanim(o.o);
+ cmd(win, ".c delete r" + id + " s" + id + " n" + id + " N" + id);
+ if (o.o.title != nil)
+ cmd(win, ".c delete t" + id);
+ cmd(win, ".c delete c" + id); # any remaining "fake" cards
+ needrepack = 1;
+ dellayobject(o.o);
+ Button =>
+ cmd(win, "destroy .buts." + string o.id);
+ Member =>
+ if (o.cid != -1) {
+ # XXX remove member from members hash.
+ }
+ Layoutobj =>
+ if ((l := findlayobject(o.lay.layid)) != nil) {
+ # XXX are we sure they're not off-screen anyway?
+ cmd(win, ".c move r" + string l.id + " 5000 5000");
+ cmd(win, ".c move c" + string l.id + " 5000 5000");
+ cmd(win, ".c move N" + string l.id + " 5000 5000");
+ cmd(win, ".c move n" + string l.id + " 5000 5000");
+ cmd(win, ".c move s" + string l.id + " 5000 5000");
+ }
+ if (layout == o.lay)
+ layout = nil;
+ Layoutframe =>
+ if (layout == o.lay)
+ layout = nil;
+ }
+}
+
+dellayobject(lay: ref Layobject)
+{
+ if (lay.layid == -1)
+ return;
+ x := lay.layid % len layobjects;
+ nl: list of ref Layobject;
+ for (ll := layobjects[x]; ll != nil; ll = tl ll)
+ if ((hd ll).layid != lay.layid)
+ nl = hd ll :: nl;
+ layobjects[x] = nl;
+}
+
+findlayobject(layid: int): ref Layobject
+{
+ if (layid == -1)
+ return nil;
+ for (ll := layobjects[layid % len layobjects]; ll != nil; ll = tl ll)
+ if ((hd ll).layid == layid)
+ return hd ll;
+ return nil;
+}
+
+deltag(cards: array of ref Object.Card, tag: string)
+{
+ for (i := 0; i < len cards; i++)
+ cmd(win, ".c dtag i" + string cards[i].id + " " + tag);
+}
+
+addtag(cards: array of ref Object.Card, tag: string)
+{
+ for (i := 0; i < len cards; i++)
+ cmd(win, ".c addtag " + tag + " withtag i" + string cards[i].id);
+}
+
+join(v: list of string): string
+{
+ if (v == nil)
+ return nil;
+ s := hd v;
+ for (v = tl v; v != nil; v = tl v)
+ s += " " + hd v;
+ return s;
+}
+
+notify(s: string)
+{
+ notifych <-= s;
+}
+
+notifierproc()
+{
+ notifypid := -1;
+ sync := chan of int;
+ for (;;) {
+ s := <-notifych;
+ kill(notifypid);
+ spawn notifyproc(s, sync);
+ notifypid = <-sync;
+ }
+}
+
+notifyproc(s: string, sync: chan of int)
+{
+ sync <-= sys->pctl(0, nil);
+ cmd(win, ".c delete notify");
+ id := cmd(win, ".c create text " + p2s(visibleorigin()) + " -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(1500);
+ cmd(win, ".c delete notify");
+ cmd(win, "update");
+}
+
+# move canvas so that canvas point canvp lies under
+# screen point scrp.
+pan(canvp, scrp: Point)
+{
+ o := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+ co := canvp.sub(scrp.sub(o));
+ sz := Point(int cmd(win, ".c cget -width"), int cmd(win, ".c cget -height"));
+
+ cmd(win, ".c xview moveto " + string (real co.x / real sz.x));
+ cmd(win, ".c yview moveto " + string (real co.y / real sz.y));
+}
+
+# return the top left point that's currently visible
+# in the canvas, taking into account scrolling.
+visibleorigin(): Point
+{
+ (scrx, scry) := (cmd(win, ".c cget -actx"), cmd(win, ".c cget -acty"));
+ return Point (int cmd(win, ".c canvasx " + scrx),
+ int cmd(win, ".c canvasy " + scry));
+}
+
+s2r(s: string): Rect
+{
+ r: Rect;
+ (n, toks) := sys->tokenize(s, " ");
+ if (n < 4)
+ panic("malformed rectangle " + s);
+ (r.min.x, toks) = (int hd toks, tl toks);
+ (r.min.y, toks) = (int hd toks, tl toks);
+ (r.max.x, toks) = (int hd toks, tl toks);
+ (r.max.y, toks) = (int hd toks, tl toks);
+ return r;
+}
+
+r2s(r: Rect): string
+{
+ return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+p2s(p: Point): string
+{
+ return string p.x + " " + string p.y;
+}
+
+union(r1, r2: Rect): Rect
+{
+ if (r1.min.x > r2.min.x)
+ r1.min.x = r2.min.x;
+ if (r1.min.y > r2.min.y)
+ r1.min.y = r2.min.y;
+
+ if (r1.max.x < r2.max.x)
+ r1.max.x = r2.max.x;
+ if (r1.max.y < r2.max.y)
+ r1.max.y = r2.max.y;
+ return r1;
+}
+
+kill(pid: int)
+{
+ if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+ sys->write(fd, array of byte "kill", 4);
+}
+
+lockproc()
+{
+ for (;;) {
+ <-cardlockch;
+ cardlockch <-=1;
+ }
+}
+
+lock()
+{
+ cardlockch <-= 1;
+}
+
+unlock()
+{
+ <-cardlockch;
+}
+
+openimage(file: string, id: string): Point
+{
+ if (tk->cmd(win, "image create bitmap " + id + " -file " + file)[0] == '!')
+ return (0, 0);
+ return (int tk->cmd(win, "image width " + id),
+ int tk->cmd(win, "image height " + id));
+}
+
+# read images into tk.
+readimages(dir: string, prefix: string): (int, Point)
+{
+ displ := drawctxt.display;
+ if (cardsize.x > 0 && cardsize.y > 0 &&
+ (img := displ.open(dir + "/" + prefix + ".all.bit")) != nil) {
+ if (img.r.dx() % cardsize.x != 0 || img.r.dy() != cardsize.y)
+ sys->fprint(stderr, "cards: inconsistent complete image, ignoring\n");
+ else {
+ n := img.r.dx() / cardsize.x;
+ x := img.r.min.x;
+ sys->print("found %d cards in complete image\n", n);
+ for (i := 0; i < n; i++) {
+ c := displ.newimage(((0, 0), cardsize), img.chans, 0, 0);
+ c.draw(c.r, img, nil, (x, 0));
+ id := prefix + string i;
+ cmd(win, "image create bitmap " + id);
+ tk->putimage(win, id, c, nil);
+ x += cardsize.x;
+ }
+ return (n, cardsize);
+ }
+ }
+
+ size := openimage("@" + dir + "/" + prefix + "0.bit", prefix + "0");
+ if (size.x == 0) {
+ sys->print("no first image (filename: '%s')\n", dir + "/" + prefix + "0.bit");
+ return (0, (0, 0));
+ }
+ i := 1;
+ for (;;) {
+ nsize := openimage("@" + dir + "/" + prefix + string i + ".bit", prefix + string i);
+ if (nsize.x == 0)
+ break;
+ if (!nsize.eq(size))
+ sys->fprint(stderr, "warning: inconsistent image size in %s/%s%d.bit, " +
+ "[%d %d] vs [%d %d]\n", dir, prefix, i, size.x, size.y, nsize.x, nsize.y);
+ i++;
+ }
+ return (i, size);
+}
+
+newanimation(src: ref Layobject.Stack, r: Range): ref Animation
+{
+ a := ref Animation;
+ a.srcpt = src.pos.add(src.delta.mul(r.start));
+ cards := extractcards(src, r);
+ a.cards = cards;
+ a.waitch = chan of ref Animation;
+ return a;
+}
+
+startanimating(a: ref Animation, dst: ref Layobject.Stack, index: int)
+{
+ q := dst.animq;
+ if (q.isempty())
+ spawn animqueueproc(a.waitch);
+
+ a.tag = "a" + string animid++;
+ addtag(a.cards, a.tag);
+ q.put(a);
+ a.dstid = dst.id;
+ a.index = index;
+ spawn animproc(a);
+}
+
+SPEED: con 1.5; # animation speed in pixels/millisec
+
+animproc(a: ref Animation)
+{
+ tick := chan of int;
+ dst := stackobj(a.dstid);
+ if (dst == nil)
+ panic("animation destination has gone!");
+ dstpt := dst.pos.add(dst.delta.mul(a.index));
+ srcpt := a.srcpt;
+ d := dstpt.sub(srcpt);
+ # don't bother animating if moving to or from a hidden stack.
+ if (!srcpt.eq(Hiddenpos) && !dst.pos.eq(Hiddenpos) && !d.eq((0, 0))) {
+ mag := math->sqrt(real(d.x * d.x + d.y * d.y));
+ (vx, vy) := (real d.x / mag, real d.y / mag);
+ currpt := a.srcpt; # current position of cards
+ t0 := starttime;
+ dt := int (mag / SPEED);
+ t := 0;
+ tickregister(tick);
+ cmd(win, ".c raise " + a.tag);
+ while (t < dt) {
+ s := real t * SPEED;
+ p := Point(srcpt.x + int (s * vx), srcpt.y + int (s * vy));
+ dp := p.sub(currpt);
+ cmd(win, ".c move " + a.tag + " " + string dp.x + " " + string dp.y);
+ currpt = p;
+ t = <-tick - t0;
+ }
+ tickunregister(tick);
+ cmd(win, "update");
+ }
+ a.waitch <-= a;
+}
+
+tickregister(tick: chan of int)
+{
+ tickregisterch <-= tick;
+}
+
+tickunregister(tick: chan of int)
+{
+ tickunregisterch <-= tick;
+}
+
+tickproc(tick: chan of int)
+{
+ for (;;)
+ tick <-= 1;
+}
+
+timeproc()
+{
+ reg: list of chan of int;
+ dummytick := chan of int;
+ realtick := chan of int;
+ tick := dummytick;
+ spawn tickproc(realtick);
+ for (;;) {
+ alt {
+ c := <-tickregisterch =>
+ if (reg == nil)
+ tick = realtick;
+ reg = c :: reg;
+ c := <-tickunregisterch =>
+ r: list of chan of int;
+ for (; reg != nil; reg = tl reg)
+ if (hd reg != c)
+ r = hd reg :: r;
+ reg = r;
+ if (reg == nil)
+ tick = dummytick;
+ <-tick =>
+ t := sys->millisec();
+ for (r := reg; r != nil; r = tl r) {
+ alt {
+ hd r <-= t =>
+ ;
+ * =>
+ ;
+ }
+ }
+ cmd(win, "update");
+ }
+ }
+}
+
+yield()
+{
+ yieldch <-= 1;
+}
+
+yieldproc()
+{
+ for (;;)
+ <-yieldch;
+}
+
+
+# send completed animations down animfinishedch;
+# wait for a reply, which is either a new animation to wait
+# for (the next in the queue) or nil, telling us to exit
+animqueueproc(waitch: chan of ref Animation)
+{
+ rc := chan of chan of ref Animation;
+ while (waitch != nil) {
+ animfinishedch <-= (<-waitch, rc);
+ waitch = <-rc;
+ }
+}
+
+# an animation has finished.
+# move the cards into their final place in the stack,
+# remove the animation from the queue it's on,
+# and inform the mediating process of the next animation process in the queue.
+animterminated(v: (ref Animation, chan of chan of ref Animation))
+{
+ (a, rc) := v;
+ deltag(a.cards, a.tag);
+ dst := stackobj(a.dstid);
+ insertcards(dst, a.cards, a.index);
+ repackobj(dst);
+ cmd(win, "update");
+ q := dst.animq;
+ q.get();
+ if (q.isempty())
+ rc <-= nil;
+ else {
+ a = q.peek();
+ rc <-= a.waitch;
+ }
+}
+
+actrect(win: ref Tk->Toplevel, w: string): Rect
+{
+ r: Rect;
+ r.min.x = int cmd(win, w + " cget -actx") + int cmd(win, w + " cget -bd");
+ r.min.y = int cmd(win, w + " cget -acty") + int cmd(win, w + " cget -bd");
+ r.max.x = r.min.x + int cmd(win, w + " cget -actwidth");
+ r.max.y = r.min.y + int cmd(win, w + " cget -actheight");
+ return r;
+}
+
+actsize(win: ref Tk->Toplevel, w: string): Point
+{
+ return (int cmd(win, w + " cget -actwidth"), int cmd(win, w + " cget -actheight"));
+}
+
+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;
+}
+
+Queue.peek(q: self ref Queue): T
+{
+ s: T;
+ if (q.isempty())
+ return s;
+ s = q.get();
+ q.h = s :: q.h;
+ return s;
+}
+
+Queue.isempty(q: self ref Queue): int
+{
+ return q.h == nil && q.t == nil;
+}
+
+revlist(ls: list of T) : list of T
+{
+ rs: list of T;
+ for (; ls != nil; ls = tl ls)
+ rs = hd ls :: rs;
+ return rs;
+}
+
+readconfig(): int
+{
+ for (lines := readconfigfile("/icons/cards/config"); lines != nil; lines = tl lines) {
+ t := hd lines;
+ case hd t {
+ "rearborder" =>
+ Rearborder = int hd tl t;
+ "border" =>
+ Border = int hd tl t;
+ "selectborder" =>
+ Selectborder = int hd tl t;
+ "xdelta" =>
+ carddelta.x = int hd tl t;
+ "ydelta" =>
+ carddelta.y = int hd tl t;
+ "font" =>
+ Textfont = hd tl t;
+ "selectcolour" =>
+ Selectcolour = hd tl t;
+ "cardsize" =>
+ if (len t != 3)
+ sys->fprint(stderr, "cards: invalid value for cardsize attribute\n");
+ else
+ cardsize = (int hd tl t, int hd tl tl t);
+ * =>
+ sys->fprint(stderr, "cards: unknown config attribute: %s\n", hd t);
+ }
+ }
+ return 0;
+}
+
+readcardimages(): int
+{
+ (nimages, cardsize) = readimages("/icons/cards", "c");
+ if (nimages == 0) {
+ sys->fprint(stderr, "cards: no card images found\n");
+ return -1;
+ }
+ sys->print("%d card images found\n", nimages);
+
+ (nrears, rearsize) := readimages("/icons/cardrears", "rear");
+ if (nrears > 0 && !rearsize.eq(cardsize)) {
+ sys->fprint(stderr, "cards: card rear sizes don't match card sizes (%s vs %s)\n", p2s(rearsize), p2s(cardsize));
+ return -1;
+ }
+ sys->print("%d card rear images found\n", nrears);
+ cr := Rect((0, 0), cardsize);
+ for (i := nrears; i < len rearcolours; i++) {
+ cmd(win, "image create bitmap rear" + string i);
+ img := drawctxt.display.newimage(cr, Draw->XRGB32, 0, Draw->Black);
+ img.draw(cr.inset(Rearborder),
+ drawctxt.display.color(rearcolours[i] - nrears), nil, (0, 0));
+ tk->putimage(win, "rear" + string i, img, nil);
+ }
+ return 0;
+}
+
+readconfigfile(f: string): list of list of string
+{
+ sys->print("opening config file '%s'\n", f);
+ fd := sys->open(f, Sys->OREAD);
+ if (fd == nil)
+ return nil;
+ buf := array[Sys->ATOMICIO] of byte;
+ nb := sys->read(fd, buf, len buf);
+ if (nb <= 0)
+ return nil;
+ (nil, lines) := sys->tokenize(string buf[0:nb], "\r\n");
+ r: list of list of string;
+ for (; lines != nil; lines = tl lines) {
+ (n, toks) := sys->tokenize(hd lines, " \t");
+ if (n == 0)
+ continue;
+ if (n < 2)
+ sys->fprint(stderr, "cards: invalid config line: %s\n", hd lines);
+ else
+ r = toks :: r;
+ }
+ return r;
+}
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+ Point: import draw;
+ if (win.image == nil || win.image.screen == nil)
+ return;
+ r := win.image.screen.image.r;
+ scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y));
+ 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.min.x - dx, r.max.x - dx);
+ if (actr.max.y > r.max.y)
+ (actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy);
+ 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);
+}
+
+panic(s: string)
+{
+ sys->fprint(stderr, "cards: panic: %s\n", s);
+ raise "panic";
+}
+
+showtk := 0;
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ if (showtk)
+ sys->print("tk: %s\n", s);
+ e := tk->cmd(top, s);
+ if (e != nil && e[0] == '!') {
+ sys->fprint(stderr, "tk error %s on '%s'\n", e, s);
+ raise "panic";
+ }
+ return e;
+}
+
+max(a, b: int): int
+{
+ if (a > b)
+ return a;
+ return b;
+}
diff --git a/appl/spree/clients/chat.b b/appl/spree/clients/chat.b
new file mode 100644
index 00000000..d474ef8f
--- /dev/null
+++ b/appl/spree/clients/chat.b
@@ -0,0 +1,194 @@
+implement Clientmod;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect, Display, Image: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "../client.m";
+include "commandline.m";
+ commandline: Commandline;
+ Cmdline: import commandline;
+
+stderr: ref Sys->FD;
+
+memberid := -1;
+win: ref Tk->Toplevel;
+
+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;
+ tk = load Tk Tk->PATH;
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil) {
+ sys->fprint(stderr, "chat: cannot load %s: %r\n", Tkclient->PATH);
+ sys->raise("fail:bad module");
+ }
+ commandline = load Commandline Commandline->PATH;
+ if (commandline == nil) {
+ sys->fprint(stderr, "chat: cannot load %s: %r\n", Commandline->PATH);
+ sys->raise("fail:bad module");
+ }
+ commandline->init();
+
+ tkclient->init();
+ client1(ctxt);
+}
+cmdlinech: chan of string;
+cmdline: ref Cmdline;
+
+client1(ctxt: ref Draw->Context)
+{
+ cliquefd := sys->fildes(0);
+
+ sys->pctl(Sys->NEWPGRP, nil);
+
+ winctl: chan of string;
+ (win, winctl) = tkclient->toplevel(ctxt.screen, nil,
+ "Cards", Tkclient->Appl);
+ cmdlinech = chan of string;
+
+ srvcmd := chan of string;
+ spawn updateproc(cliquefd, srvcmd);
+
+ for (;;) alt {
+ c := <-cmdlinech =>
+ for (cmds := cmdline.event(c); cmds != nil; cmds = tl cmds)
+ cliquecmd(cliquefd, "say " + quote(hd cmds));
+ c := <-srvcmd =>
+ applyupdate(c);
+ cmd(win, "update");
+ c := <-winctl =>
+ if (c == "exit")
+ sys->write(cliquefd, array[0] of byte, 0);
+ tkclient->wmctl(win, c);
+ }
+}
+
+quote(s: string): string
+{
+ for (i := 0; i < len s; i++)
+ if (s[i] == ' ')
+ s[i] = '_';
+ return s;
+}
+
+unquote(s: string): string
+{
+ for (i := 0; i < len s; i++)
+ if (s[i] == '_')
+ s[i] = ' ';
+ return s;
+}
+
+cliquecmd(fd: ref Sys->FD, s: string): int
+{
+ if (sys->fprint(fd, "%s\n", s) == -1) {
+ sys->print("chat: cmd error on '%s': %r\n", s);
+ return 0;
+ }
+ return 1;
+}
+
+
+updateproc(fd: ref Sys->FD, srvcmd: chan of string)
+{
+ wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+ spawn updateproc1(fd, srvcmd);
+ 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(fd: ref Sys->FD, srvcmd: chan of string)
+{
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(fd, buf, len buf)) > 0) {
+ (nil, lines) := sys->tokenize(string buf[0:n], "\n");
+ for (; lines != nil; lines = tl lines)
+ srvcmd <-= hd lines;
+ }
+ if (n < 0)
+ sys->fprint(stderr, "chat: error reading updates: %r\n");
+ sys->fprint(stderr, "chat: updateproc exiting\n");
+}
+
+
+applyupdate(s: string)
+{
+ (nt, toks) := sys->tokenize(s, " ");
+ case hd toks {
+ "memberid" =>
+ # memberid clientid memberid name
+ memberid = int hd tl tl toks;
+ cmd(win, "frame .me");
+ cmd(win, "label .me.l -text {Type here}");
+ (cmdline, cmdlinech) = Cmdline.new(win, ".me.f", nil);
+ cmd(win, "pack .me -side top -fill x");
+ cmd(win, "pack .me.l -side top");
+ cmd(win, "pack .me.f -side top -fill both -expand 1 -anchor w");
+
+ "joinclique" =>
+ # joinclique cliqueid clientid memberid name
+ id := int hd tl tl tl toks;
+ name := hd tl tl tl tl toks;
+ if (id == memberid)
+ break;
+ f := "." + string id;
+ cmd(win, "frame " + f);
+ cmd(win, "label " + f + ".l -text '" + name);
+ tf := f + ".tf";
+ cmd(win, "frame " + tf);
+ cmd(win, "scrollbar " + tf + ".s -orient vertical -command {" + tf + ".t yview}");
+ cmd(win, "text " + tf + ".t -height 5h");
+ cmd(win, "pack " + f + ".l -side top");
+ cmd(win, "pack " + tf + ".s -side left -fill y");
+ cmd(win, "pack " + tf + ".t -side top -fill both -expand 1");
+ cmd(win, "pack " + tf + " -side top -fill both -expand 1");
+ cmd(win, "pack " + f + " -side top -fill both -expand 1");
+
+ "say" =>
+ # say memberid text
+ id := int hd tl toks;
+ if (id == memberid)
+ break;
+ t := "." + string id + ".tf.t";
+ cmd(win, t + " insert end '" + unquote(hd tl tl toks) + "\n");
+ cmd(win, t + " see end");
+ * =>
+ sys->fprint(stderr, "chat: unknown update message '%s'\n", s);
+ }
+}
+
+concat(v: list of string): string
+{
+ if (v == nil)
+ return nil;
+ s := hd v;
+ for (v = tl v; v != nil; v = tl v)
+ s += " " + hd v;
+ return s;
+}
+
+kill(pid: int)
+{
+ if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+ sys->write(fd, array of byte "kill", 4);
+}
+
+showtk := 0;
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ if (showtk)
+ sys->print("tk: %s\n", s);
+ e := tk->cmd(top, s);
+ if (e != nil && e[0] == '!')
+ sys->fprint(stderr, "tk error %s on '%s'\n", e, s);
+ return e;
+}
+
diff --git a/appl/spree/clients/gather.b b/appl/spree/clients/gather.b
new file mode 100644
index 00000000..113985b3
--- /dev/null
+++ b/appl/spree/clients/gather.b
@@ -0,0 +1,178 @@
+implement Gather;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect, Display, Image, Font: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "commandline.m";
+ commandline: Commandline;
+ Cmdline: import commandline;
+include "sh.m";
+
+Gather: module {
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+CLIENTDIR: con "/dis/spree/clients";
+
+drawctxt: ref Draw->Context;
+cliquefd: ref Sys->FD;
+stderr: ref Sys->FD;
+
+mnt, dir: string;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ draw = load Draw Draw->PATH;
+ tk = load Tk Tk->PATH;
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil) {
+ sys->fprint(stderr, "gather: cannot load %s: %r\n", Tkclient->PATH);
+ raise "fail:bad module";
+ }
+ tkclient->init();
+ commandline = load Commandline Commandline->PATH;
+ if(commandline == nil) {
+ sys->fprint(stderr, "gather: cannot load %s: %r\n", Commandline->PATH);
+ raise "fail:bad module";
+ }
+ commandline->init();
+ drawctxt = ctxt;
+ cliquefd = sys->fildes(0);
+
+ if (len argv >= 3) {
+ mnt = hd tl argv;
+ dir = hd tl tl argv;
+ } else
+ sys->fprint(stderr, "gather: expected mnt, dir args\n");
+ client1();
+}
+
+client1()
+{
+ (win, winctl) := tkclient->toplevel(drawctxt, nil, "Gathering", Tkclient->Appl);
+ ech := chan of string;
+ tk->namechan(win, ech, "e");
+ (chat, chatevent) := Cmdline.new(win, ".chat", nil);
+ updatech := chan of string;
+ spawn readproc(updatech);
+
+ cmd(win, "button .b -text Start -command {send e start}");
+ cmd(win, "pack .b -side top -anchor w");
+ cmd(win, "pack .chat -fill both -expand 1");
+ cmd(win, "pack propagate . 0");
+ cmd(win, "update");
+ tkclient->onscreen(win, nil);
+ tkclient->startinput(win, "kbd"::"ptr"::nil);
+ 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 or
+ s = <-winctl =>
+ tkclient->wmctl(win, s);
+ line := <-updatech =>
+ (n, toks) := sys->tokenize(line, " ");
+ if (toks == nil)
+ continue;
+ case hd toks {
+ "clienttype" =>
+ chat.addtext("starting " + hd tl toks + " session...\n");
+ cmd(win, "update");
+ path := CLIENTDIR + "/" + hd tl toks + ".dis";
+ mod := load Command path;
+ if (mod == nil) {
+ chat.addtext(sys->sprint("could not load %s: %r\n", path));
+ chat.addtext("bye bye\n");
+ cliquefd = nil;
+ } else {
+ win = nil;
+ chat = nil;
+ startclient(mod, hd tl toks :: mnt :: dir :: tl tl toks);
+ exit;
+ }
+ "chat" =>
+ chat.addtext(hd tl toks + ": " + concat(tl tl toks) + "\n");
+ "title" =>
+ tkclient->settitle(win, "Gather " + concat(tl toks));
+ "join" or
+ "leave" or
+ "watch" or
+ "unwatch" =>
+ chat.addtext(line + "\n");
+ * =>
+ chat.addtext("unknown update: " + line + "\n");
+ }
+ cmd(win, "update");
+ c := <-chatevent =>
+ lines := chat.event(c);
+ for (; lines != nil; lines = tl lines)
+ cliquecmd("chat " + hd lines, chat);
+ c := <-ech =>
+ cliquecmd(c, chat);
+ }
+}
+
+cliquecmd(s: string, chat: ref Cmdline)
+{
+ if (sys->fprint(cliquefd, "%s", s) == -1) {
+ chat.addtext(sys->sprint("command failed: %r\n"));
+ cmd(chat.top, "update");
+ }
+}
+
+prefixed(s: string, prefix: string): int
+{
+ return len s >= len prefix && s[0:len prefix] == prefix;
+}
+
+readproc(updatech: chan of string)
+{
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(cliquefd, buf, Sys->ATOMICIO)) > 0) {
+ (nil, lines) := sys->tokenize(string buf[0:n], "\n");
+ for (; lines != nil; lines = tl lines) {
+ updatech <-= hd lines;
+ if (prefixed(hd lines, "clienttype"))
+ exit;
+ }
+ }
+ updatech <-= nil;
+}
+
+startclient(mod: Command, argv: list of string)
+{
+ {
+ mod->init(drawctxt, argv);
+ } exception e {
+ "*" =>
+ sys->print("client %s broken: %s\n", hd argv, e);
+ }
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+ r := tk->cmd(win, s);
+ if(len r > 0 && r[0] == '!')
+ sys->print("error executing '%s': %s\n", s, r[1:]);
+ return r;
+}
+
+concat(l: list of string): string
+{
+ if (l == nil)
+ return nil;
+ s := hd l;
+ for (l = tl l; l != nil; l = tl l)
+ s += " " + hd l;
+ return s;
+}
diff --git a/appl/spree/clients/lobby.b b/appl/spree/clients/lobby.b
new file mode 100644
index 00000000..1af52827
--- /dev/null
+++ b/appl/spree/clients/lobby.b
@@ -0,0 +1,562 @@
+implement Lobby;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect, Display, Image, Font: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "../join.m";
+ join: Join;
+include "dividers.m";
+ dividers: Dividers;
+ Divider: import dividers;
+include "commandline.m";
+ commandline: Commandline;
+ Cmdline: import commandline;
+include "sh.m";
+
+Lobby: module {
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+CLIENTDIR: con "/dis/spree/clients";
+NAMEFONT: con "/fonts/charon/plain.small.font";
+TITLEFONT: con "/fonts/charon/bold.normal.font";
+HEADERFONT: con "/fonts/charon/italic.normal.font";
+
+Object: adt {
+ id: int;
+ pick {
+ Session =>
+ filename: string;
+ owner: string;
+ invitations: list of string;
+ members: list of string;
+ invited: int;
+ Sessiontype =>
+ start: string;
+ name: string;
+ title: string;
+ clienttype: string;
+ Invite =>
+ session: ref Object.Session;
+ name: string;
+ Member =>
+ parentid: int;
+ name: string;
+ Archive =>
+ Other =>
+ }
+};
+
+drawctxt: ref Draw->Context;
+cliquefd: ref Sys->FD;
+objects: array of ref Object;
+myname: string;
+maxid := 0;
+
+badmodule(m: string)
+{
+ sys->fprint(sys->fildes(2), "lobby: cannot load %s: %r\n", m);
+ raise "fail:bad module";
+}
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+ sys = load Sys Sys->PATH;
+ draw = load Draw Draw->PATH;
+ tk = load Tk Tk->PATH;
+
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil)
+ badmodule(Tkclient->PATH);
+ tkclient->init();
+
+ commandline = load Commandline Commandline->PATH;
+ if(commandline == nil)
+ badmodule(Commandline->PATH);
+ commandline->init();
+
+ dividers = load Dividers Dividers->PATH;
+ if (dividers == nil)
+ badmodule(Dividers->PATH);
+ dividers->init();
+
+ join = load Join Join->PATH;
+ if (join == nil)
+ badmodule(Join->PATH);
+
+ drawctxt = ctxt;
+ cliquefd = sys->fildes(0);
+ sys->pctl(Sys->NEWPGRP, nil);
+ client1();
+}
+
+columns := array[] of {("name", ""), ("members", ""), ("watch", "Watch"), ("join", "Join"), ("invite", "Invite")};
+
+reqwidth(win: ref Tk->Toplevel, w: string): int
+{
+ return 2 * int cmd(win, w + " cget -bd") + int cmd(win, w + " cget -width");
+}
+
+client1()
+{
+ (win, winctl) := tkclient->toplevel(drawctxt, nil, "Lobby", Tkclient->Appl);
+ ech := chan of string;
+ tk->namechan(win, ech, "e");
+ (chat, chatevent) := Cmdline.new(win, ".d2", nil);
+ updatech := chan of list of string;
+ spawn readproc(updatech);
+
+ cmd(win, "frame .buts");
+ cmd(win, "menubutton .buts.start -text New -menu .buts.start.m");
+ cmd(win, "menu .buts.start.m");
+ cmd(win, "pack .buts.start -side left");
+ cmd(win, "button .buts.kick -text Kick -command {send e kick}");
+ cmd(win, "pack .buts.kick -side left");
+ cmd(win, "pack .buts -side top -fill x");
+
+ cmd(win, "frame .d1");
+
+ cmd(win, "scrollbar .d1.s -orient vertical -command {.d1.c yview}");
+ cmd(win, "canvas .d1.c -yscrollcommand {.d1.s set}");
+ cmd(win, "pack .d1.s -side left -fill y");
+ cmd(win, "pack .d1.c -side top -fill both -expand 1");
+ cmd(win, "frame .t");
+ cmd(win, ".d1.c create window 0 0 -anchor nw -window .t");
+ cmd(win, "frame .t.f1 -bd 2 -relief sunken");
+ cmd(win, "pack .t.f1 -side top -fill both -expand 1");
+
+ cmd(win, "label .t.f1.sessionlabel -text Sessions -font " + TITLEFONT);
+ cmd(win, "pack .t.f1.sessionlabel");
+ cmd(win, "frame .t.s");
+ cmd(win, "pack .t.s -in .t.f1 -side top -fill both -expand 1");
+
+ cmd(win, "frame .t.f2 -bd 2 -relief sunken");
+ cmd(win, "label .t.archiveslabel -text Archives -font " + TITLEFONT);
+ cmd(win, "pack .t.archiveslabel");
+ cmd(win, "frame .t.a");
+ cmd(win, "pack .t.a -in .t.f2 -side top -fill both -expand 1 -anchor w");
+ cmd(win, "pack .t.f2 -side top -fill both -expand 1");
+
+ cmd(win, "label .t.a.title0 -text Title -font " + HEADERFONT);
+ cmd(win, "label .t.a.title1 -text Members -font " + HEADERFONT);
+ cmd(win, "grid .t.a.title0 .t.a.title1 -sticky w");
+ cmd(win, "grid columnconfigure .t.a 1 -weight 1");
+
+ cmd(win, "bind .t <Configure> {.d1.c configure -scrollregion {0 0 [.t cget -width] [.t cget -height]}}");
+
+ cmd(win, "button .tmp");
+ for (i := 0; i < len columns; i++) {
+ (name, mintext) := columns[i];
+ cmd(win, ".tmp configure -text '" + mintext);
+ cmd(win, "grid columnconfigure .t.s " + string i +
+ " -name " + name +
+ " -minsize " + string reqwidth(win, ".tmp"));
+ }
+ cmd(win, "grid columnconfigure .t.s members -weight 1");
+ cmd(win, "destroy .tmp");
+ cmd(win, "menu .invite");
+
+ (divider, dividerevent) := Divider.new(win, ".d", ".d1" :: ".d2" :: nil, Dividers->NS);
+ cmd(win, "pack .d -side top -fill both");
+ cmd(win, "pack propagate . 0");
+ tkclient->onscreen(win, nil);
+ tkclient->startinput(win, "kbd"::"ptr"::nil);
+ 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 or
+ s = <-winctl =>
+ tkclient->wmctl(win, s);
+ c := <-dividerevent =>
+ divider.event(c);
+ c := <-chatevent =>
+ lines := chat.event(c);
+ for (; lines != nil; lines = tl lines) {
+ line := hd lines;
+ if (len line > 0 && line[len line-1]=='\n')
+ line = line[0:len line-1];
+ cliquecmd("chat " + line);
+ }
+ lines := <-updatech =>
+#sys->print("++\n");
+ for (; lines != nil; lines = tl lines) {
+#sys->print("+%s\n", hd lines);
+ doupdate(win, chat, hd lines);
+ }
+ cmd(win, "update");
+ c := <-ech =>
+ (n, toks) := sys->tokenize(c, " ");
+ case hd toks {
+ "watch" =>
+ joinclique(win, chat, int hd tl toks, "watch");
+ "join" =>
+ joinclique(win, chat, int hd tl toks, "join");
+ "start" =>
+ start(win, chat, int hd tl toks);
+ "postinvite" =>
+ postinvite(win, int hd tl toks, hd tl tl toks);
+ "unarchive" =>
+ e := cliquecmd("unarchive " + hd tl toks);
+ if (e != nil)
+ chat.addtext("failed to unarchive: " + e + "\n");
+ "invite" =>
+ # invite sessionid name
+ (id, name) := (hd tl toks, hd tl tl toks);
+ vname := "inv." + name;
+ v := int cmd(win, "variable " + vname);
+ s := "invite";
+ if (!v)
+ s = "uninvite";
+ e := cliquecmd(s + " " + string id + " " + name);
+ if (e != nil) {
+ chat.addtext("invite failed: " + e + "\n");
+ cmd(win, "variable " + vname + " " + string !v);
+ }
+ "kick" =>
+ e := cliquecmd("kick");
+ if (e != nil)
+ chat.addtext("kick failed: " + e + "\n");
+ * =>
+ sys->print("unknown msg %s\n", c);
+ }
+ cmd(win, "update");
+ }
+ }
+}
+
+joinclique(nil: ref Tk->Toplevel, chat: ref Cmdline, id: int, how: string)
+{
+ pick o := objects[id] {
+ Session =>
+ e := join->join(drawctxt, "/n/remote", o.filename, how);
+ if (e != nil)
+ chat.addtext("couldn't join clique: " + e + "\n");
+ else
+ chat.addtext("joined clique ok\n");
+ * =>
+ sys->print("join bad id %d (type %d)\n", id, tagof objects[id]);
+ }
+}
+
+start(nil: ref Tk->Toplevel, chat: ref Cmdline, id: int)
+{
+ pick o := objects[id] {
+ Sessiontype =>
+ e := cliquecmd("start " + o.start);
+ if (e != nil)
+ chat.addtext("failed to start clique: " + e + "\n");
+ * =>
+ sys->print("start bad id %d (type %d)\n", id, tagof objects[id]);
+ }
+}
+
+postinvite(win: ref Tk->Toplevel, id: int, widget: string)
+{
+ pick o := objects[id] {
+ Session =>
+ cmd(win, ".invite delete 0 end");
+ cmd(win, ".invite add checkbutton -text All -variable inv.all -command {send e invite " + string id + " all}");
+ for (invites := o.invitations; invites != nil; invites = tl invites)
+ if (hd invites == "all")
+ break;
+ cmd(win, "variable inv.all " + string (invites != nil));
+
+ for (i := 0; i < len objects; i++) {
+ if (objects[i] == nil)
+ continue;
+ pick p := objects[i] {
+ Member =>
+ if (tagof(objects[p.parentid]) != tagof(Object.Session) && p.name != o.owner) {
+ for (invites = o.invitations; invites != nil; invites = tl invites)
+ if (hd invites == p.name)
+ break;
+ invited := invites != nil;
+ cmd(win, "variable inv." + p.name + " " + string invited);
+ cmd(win, ".invite add checkbutton -variable inv." + p.name +
+ " -command {send e invite " + string id + " " + p.name + "}" +
+ " -text '" + p.name);
+ }
+ }
+ }
+ x := int cmd(win, widget + " cget -actx");
+ y := int cmd(win, widget + " cget -acty");
+ h := 2 * int cmd(win, widget + " cget -bd") + int cmd(win, widget + " cget -actheight");
+ cmd(win, ".invite post " + string x + " " + string (y + h));
+ * =>
+ sys->print("bad invited id %d (type %d)\n", id, tagof objects[id]);
+ }
+}
+
+panic(s: string)
+{
+ sys->print("lobby panic: %s\n", s);
+ raise "panic";
+}
+
+doupdate(win: ref Tk->Toplevel, chat: ref Cmdline, line: string)
+{
+ (n, toks) := sys->tokenize(line, " ");
+ if (n == 0)
+ return;
+ case hd toks {
+ "chat" =>
+ chat.addtext(sys->sprint("%s: %s\n", hd tl toks, concat(tl tl toks)));
+ "create" =>
+ # create id parentid vis type
+ id := int hd tl toks;
+ if (id >= len objects)
+ objects = (array[len objects + 10] of ref Object)[0:] = objects;
+ if (objects[id] != nil)
+ panic(sys->sprint("object %d already exists!", id));
+ parentid := int hd tl tl toks;
+ objtype := tl tl tl tl toks;
+ o: ref Object;
+ case hd objtype {
+ "sessiontype" =>
+ o = ref Object.Sessiontype(id, nil, nil, nil, nil);
+ "session" =>
+ cmd(win, "grid rowinsert .t.s 0");
+ cmd(win, "grid rowconfigure .t.s 0 -name id" + string id);
+ f := ".t.s.f" + string id;
+ cmd(win, "frame " + f); # dummy, so we can destroy row easily
+ cmd(win, "label "+f+".name");
+ cmd(win, "grid "+f+".name -row id" + string id + " -column name -in .t.s");
+ cmd(win, "button "+f+".watch -text Watch -command {send e watch " + string id + "}");
+ cmd(win, "grid "+f+".watch -row id" + string id + " -column watch -in .t.s");
+ cmd(win, "label "+f+".members -font " + NAMEFONT);
+ cmd(win, "grid "+f+".members -row id" + string id + " -column members -in .t.s");
+ o = ref Object.Session(id, nil, nil, nil, nil, 0);
+ "member" =>
+ o = ref Object.Member(id, parentid, nil);
+ "invite" =>
+ pick parent := objects[parentid] {
+ Session =>
+ o = ref Object.Invite(id, parent, nil);
+ * =>
+ panic("invite not under session");
+ }
+ "archive" =>
+ cmd(win, "grid rowinsert .t.a 1");
+ cmd(win, "grid rowconfigure .t.a 1 -name id" + string id);
+ f := ".t.a.f" + string id;
+ cmd(win, "frame " + f);
+ cmd(win, "label "+f+".name");
+ cmd(win, "grid "+f+".name -row id" + string id + " -column 0 -in .t.a -sticky w");
+ cmd(win, "label "+f+".members -anchor w -font " + NAMEFONT);
+ cmd(win, "grid "+f+".members -row id" + string id + " -column 1 -in .t.a -sticky ew");
+ cmd(win, "button "+f+".unarchive -text Unarchive -command {send e unarchive " + string id + "}");
+ cmd(win, "grid "+f+".unarchive -row id" + string id + " -column 2 -in .t.a");
+ o = ref Object.Archive(id);
+ * =>
+ o = ref Object.Other(id);
+ }
+ objects[id] = o;
+
+ "del" =>
+ # del parent start end objs...
+ for (objs := tl tl tl tl toks; objs != nil; objs = tl objs) {
+ id := int hd objs;
+ pick o := objects[id] {
+ Session =>
+ cmd(win, "grid rowdelete .t.s id" + string id);
+ cmd(win, "destroy .t.s.f" + string id);
+ Archive =>
+ cmd(win, "grid rowdelete .t.a id" + string id);
+ cmd(win, "destroy .t.a.f" + string id);
+ Sessiontype =>
+ sys->print("cannot destroy sessiontypes yet\n");
+ Member =>
+ pick parent := objects[o.parentid] {
+ Session =>
+ parent.members = removeitem(parent.members, o.name);
+ cmd(win, sys->sprint(".t.s.f%d.members configure -text '%s", o.parentid, concat(parent.members)));
+ * =>
+ chat.addtext(o.name + " has left\n");
+ }
+ Invite =>
+ s := o.session;
+ invites := s.invitations;
+ invited := 0;
+ for (s.invitations = nil; invites != nil; invites = tl invites) {
+ inv := hd invites;
+ if (inv != o.name) {
+ s.invitations = inv :: s.invitations;
+ if (inv == "all" || inv == myname)
+ invited = 1;
+ }
+ }
+ if (!invited && s.invited) {
+ cmd(win, "destroy .t.s.f" + hd tl toks + ".join");
+ s.invited = 0;
+ }
+ }
+ objects[id] = nil;
+ }
+
+ "name" =>
+ myname = hd tl toks;
+ tkclient->settitle(win, "Lobby (" + myname + ")");
+
+ "set" =>
+ # set obj attr val
+ id := int hd tl toks;
+ (attr, val) := (hd tl tl toks, tl tl tl toks);
+ pick o := objects[id] {
+ Session =>
+ f := ".t.s.f" + string id;
+ case attr {
+ "filename" =>
+ o.filename = hd val;
+ "owner" =>
+ if (hd val == myname) {
+ cmd(win, "label "+f+".invite -text Invite -bd 2 -relief raised");
+ cmd(win, "bind "+f+".invite <Button-1> {send e postinvite " + string id + " %W}");
+ cmd(win, "grid "+f+".invite -row id" + string id + " -column invite -in .t.s");
+ }
+ o.owner = hd val;
+ "title" =>
+ cmd(win, f + ".name configure -text '" + concat(val));
+ }
+ Archive =>
+ f := ".t.a.f" + string id;
+ case attr {
+ "name" =>
+ cmd(win, f + ".name configure -text '" + concat(val));
+ "members" =>
+ cmd(win, f + ".members configure -text '" + concat(val));
+ }
+ Sessiontype =>
+ case attr {
+ "start" =>
+ o.start = concat(val);
+ "clienttype" =>
+ o.clienttype = hd val;
+ "title" =>
+ if (o.title != nil)
+ panic("can't change sessiontype name!");
+ else {
+ o.title = concat(val);
+ cmd(win, ".buts.start.m add command" +
+ " -command {send e start " + string id + "}" +
+ " -text '" + o.title);
+ }
+ "name" =>
+ o.name = hd val;
+ }
+ Member =>
+ case attr {
+ "name" =>
+ if (o.name != nil)
+ panic("cannot change member name!");
+ o.name = hd val;
+ pick parent := objects[o.parentid] {
+ Session =>
+ parent.members = o.name :: parent.members;
+ cmd(win, sys->sprint(".t.s.f%d.members configure -text '%s", o.parentid, concat(parent.members)));
+ * =>
+ chat.addtext(o.name + " has arrived\n");
+ }
+ }
+ Invite =>
+ case attr {
+ "name" =>
+ o.name = hd val;
+ s := o.session;
+ sid := string s.id;
+ f := ".t.s.f" + sid;
+ invited := o.name == myname || o.name == "all";
+ s.invitations = o.name :: s.invitations;
+ if (invited && !s.invited) {
+ cmd(win, "button "+f+".join -text Join -command {send e join " + sid + "}");
+ cmd(win, "grid "+f+".join -row id" + sid + " -column join -in .t.s");
+ s.invited = 1;
+ }
+ }
+ }
+ }
+}
+
+removeitem(l: list of string, i: string): list of string
+{
+ rl: list of string;
+ for (; l != nil; l = tl l)
+ if (hd l != i)
+ rl = hd l :: rl;
+ return rl;
+}
+
+numsplit(s: string): (string, int)
+{
+ for (i := len s - 1; i >= 0; i--)
+ if (s[i] < '0' || s[i] > '9')
+ break;
+ if (i == len s -1)
+ return (s, 0);
+ return (s[0:i+1], int s[i+1:]);
+}
+
+cliquecmd(s: string): string
+{
+ if (sys->fprint(cliquefd, "%s", s) == -1) {
+ e := sys->sprint("%r");
+ sys->print("error on '%s': %s\n", s, e);
+ return e;
+ }
+ return nil;
+}
+
+prefixed(s: string, prefix: string): int
+{
+ return len s >= len prefix && s[0:len prefix] == prefix;
+}
+
+readproc(updatech: chan of list of string)
+{
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(cliquefd, buf, Sys->ATOMICIO)) > 0) {
+ (nil, lines) := sys->tokenize(string buf[0:n], "\n");
+ if (lines != nil)
+ updatech <-= lines;
+ }
+ updatech <-= nil;
+}
+
+startclient(mod: Command, argv: list of string)
+{
+ {
+ mod->init(drawctxt, argv);
+ } exception e {
+ "*" =>
+ sys->print("client %s broken: %s\n", hd argv, e);
+ exit;
+ }
+ mod->init(drawctxt, argv);
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+ r := tk->cmd(win, s);
+ if(len r > 0 && r[0] == '!')
+ sys->print("error executing '%s': %s\n", s, r[1:]);
+ return r;
+}
+
+concat(l: list of string): string
+{
+ if (l == nil)
+ return nil;
+ s := hd l;
+ for (l = tl l; l != nil; l = tl l)
+ s += " " + hd l;
+ return s;
+}
diff --git a/appl/spree/clients/othello.b b/appl/spree/clients/othello.b
new file mode 100644
index 00000000..2a146b8e
--- /dev/null
+++ b/appl/spree/clients/othello.b
@@ -0,0 +1,270 @@
+implement Othello;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+
+SQ: con 30; # Square size in pixels
+N: con 8;
+
+stderr: ref Sys->FD;
+
+Othello: module {
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Black, White, Nocolour: con iota;
+colours := array[] of {White => "white", Black => "black"};
+
+win: ref Tk->Toplevel;
+board: array of array of int;
+notifypid := -1;
+membername: string;
+membernames := array[2] of string;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ draw = load Draw Draw->PATH;
+ tk = load Tk Tk->PATH;
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil) {
+ sys->fprint(stderr, "othello: cannot load %s: %r\n", Tkclient->PATH);
+ raise "fail:bad module";
+ }
+ tkclient->init();
+
+ if (len argv >= 3) { # argv: modname mnt dir ...
+ membername = readfile(hd tl argv + "/name");
+ sys->print("name is %s\n", membername);
+ }
+ client1(ctxt);
+}
+
+configcmds := array[] of {
+"canvas .c -height " + string (SQ * N) + " -width " + string (SQ * N) + " -bg green",
+"label .status -text {No clique in progress}",
+"frame .f",
+"label .f.l -text {watching} -bg white",
+"label .f.turn -text {}",
+"pack .f.l -side left -expand 1 -fill x",
+"pack .f.turn -side left -fill x -expand 1",
+"pack .c -side top",
+"pack .status .f -side top -fill x",
+"bind .c <ButtonRelease-1> {send cmd b1up %x %y}",
+};
+
+client1(ctxt: ref Draw->Context)
+{
+ cliquefd := sys->fildes(0);
+
+ sys->pctl(Sys->NEWPGRP, nil);
+
+ winctl: chan of string;
+ (win, winctl) = tkclient->toplevel(ctxt, nil,
+ "Othello", Tkclient->Appl);
+ bcmd := chan of string;
+ tk->namechan(win, bcmd, "cmd");
+ for (i := 0; i < len configcmds; i++)
+ cmd(win, configcmds[i]);
+
+ for (i = 0; i < N; i++)
+ for (j := 0; j < N; j++)
+ cmd(win, ".c create rectangle " + r2s(square(i, j)));
+ board = array[N] of {* => array[N] of {* => Nocolour}};
+ tkclient->onscreen(win, nil);
+ tkclient->startinput(win, "ptr"::"kbd"::nil);
+ spawn updateproc(cliquefd);
+
+ for (;;) alt {
+ c := <-bcmd =>
+ (n, toks) := sys->tokenize(c, " ");
+ case hd toks {
+ "b1up" =>
+ (inboard, x, y) := boardpos((int hd tl toks, int hd tl tl toks));
+ if (!inboard)
+ break;
+ othellocmd(cliquefd, "move " + string x + " " + string y);
+ cmd(win, "update");
+ }
+ s := <-win.ctxt.kbd =>
+ tk->keyboard(win, s);
+ s := <-win.ctxt.ptr =>
+ tk->pointer(win, *s);
+ s := <-win.ctxt.ctl or
+ s = <-win.wreq or
+ s = <-winctl =>
+ if (s == "exit")
+ sys->write(cliquefd, array[0] of byte, 0);
+ tkclient->wmctl(win, s);
+ }
+}
+
+othellocmd(fd: ref Sys->FD, s: string): int
+{
+ if (sys->fprint(fd, "%s\n", s) == -1) {
+ notify(sys->sprint("%r"));
+ return 0;
+ }
+ return 1;
+}
+
+updateproc(cliquefd: ref Sys->FD)
+{
+ 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, "othello: error reading updates: %r\n");
+ sys->fprint(stderr, "othello: updateproc exiting\n");
+}
+
+applyupdate(s: string)
+{
+ (nt, toks) := sys->tokenize(s, " ");
+ case hd toks {
+ "create" =>
+ ; # ignore - there's only one object (the board)
+ "set" =>
+ # set objid attr val
+ toks = tl tl toks;
+ (attr, val) := (hd toks, hd tl toks);
+ case attr {
+ "members" =>
+ membernames[Black] = hd tl toks;
+ membernames[White] = hd tl tl toks;
+ status(membernames[Black]+ "(Black) vs. " + string membernames[White] + "(White)");
+ if (membername == membernames[Black])
+ cmd(win, ".f.l configure -text Black");
+ else if (membername == membernames[White])
+ cmd(win, ".f.l configure -text White");
+ "turn" =>
+ turn := int val;
+ if (turn != Nocolour) {
+ if (membername == membernames[turn])
+ cmd(win, ".f.turn configure -text {(Your turn)}");
+ else if (membername == membernames[!turn])
+ cmd(win, ".f.turn configure -text {}");
+ }
+ "winner" =>
+ text := "it was a draw";
+ winner := int val;
+ if (winner != Nocolour)
+ text = colours[int val] + " won.";
+ status("clique over. " + text);
+ cmd(win, ".f.l configure -text {watching}");
+ * =>
+ (x, y) := (attr[0] - 'a', attr[1] - 'a');
+ set(x, y, int val);
+ }
+ * =>
+ sys->fprint(stderr, "othello: unknown update message '%s'\n", s);
+ }
+}
+
+status(s: string)
+{
+ cmd(win, ".status configure -text '" + s);
+}
+
+itemopts(colour: int): string
+{
+ return "-fill " + colours[colour] +
+ " -outline " + colours[!colour];
+}
+
+set(x, y, colour: int)
+{
+ id := piece(x, y);
+ if (colour == Nocolour)
+ cmd(win, ".c delete " + id);
+ else if (board[x][y] != Nocolour)
+ cmd(win, ".c itemconfigure " + id + " " + itemopts(colour));
+ else
+ cmd(win, ".c create oval " + r2s(square(x, y)) + " " +
+ itemopts(colour) +
+ " -tags {piece " + id + "}");
+ board[x][y] = colour;
+}
+
+notify(s: string)
+{
+ kill(notifypid);
+ sync := chan of int;
+ spawn notifyproc(s, sync);
+ notifypid = <-sync;
+}
+
+notifyproc(s: string, sync: chan of int)
+{
+ 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;
+}
+
+boardpos(p: Point): (int, int, int)
+{
+ (x, y) := (p.x / SQ, p.y / SQ);
+ if (x < 0 || x > N - 1 || y < 0 || y > N - 1)
+ return (0, 0, 0);
+ return (1, x, y);
+}
+
+square(x, y: int): Rect
+{
+ return ((SQ*x, SQ*y), (SQ*(x + 1), SQ*(y + 1)));
+}
+
+piece(x, y: int): string
+{
+ return "p" + string x + "." + string y;
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ e := tk->cmd(top, s);
+ if (e != nil && e[0] == '!')
+ sys->fprint(stderr, "tk error %s on '%s'\n", e, s);
+ return e;
+}
+
+r2s(r: Rect): string
+{
+ return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+kill(pid: int)
+{
+ if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+ sys->write(fd, array of byte "kill", 4);
+}
+
+readfile(f: string): string
+{
+ if ((fd := sys->open(f, Sys->OREAD)) == nil)
+ return nil;
+ a := array[8192] of byte;
+ n := sys->read(fd, a, len a);
+ if (n <= 0)
+ return nil;
+ return string a[0:n];
+}
+
diff --git a/appl/spree/engines/afghan.b b/appl/spree/engines/afghan.b
new file mode 100644
index 00000000..0ca2ca49
--- /dev/null
+++ b/appl/spree/engines/afghan.b
@@ -0,0 +1,302 @@
+implement Gatherengine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ All, None: import Sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+ allow: Allow;
+include "cardlib.m";
+ cardlib: Cardlib;
+ Selection, Cmember: import cardlib;
+ dTOP, dLEFT, oLEFT, oRIGHT, EXPAND, FILLX, FILLY, aUPPERCENTRE,
+ Stackspec: import Cardlib;
+include "../gather.m";
+
+CLICK, REDEAL: con iota;
+
+clique: ref Clique;
+rows: array of ref Object; # [10]
+central: array of ref Object; # [4]
+chokey, deck: ref Object;
+direction := 0;
+nredeals := 0;
+
+Rowpilespec := Stackspec(
+ "display", # style
+ 10, # maxcards
+ 0, # conceal
+ nil # title
+);
+
+Centralpilespec := Stackspec(
+ "pile",
+ 13,
+ 0,
+ nil
+);
+
+clienttype(): string
+{
+ return "cards";
+}
+
+maxmembers(): int
+{
+ return 1;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+ return nil;
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+ allow = load Allow Allow->PATH;
+ if (allow == nil) {
+ sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+ return "bad module";
+ }
+ allow->init(spree, clique);
+ cardlib = load Cardlib Cardlib->PATH;
+ if (cardlib == nil) {
+ sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+ return "bad module";
+ }
+ cardlib->init(spree, clique);
+ return nil;
+}
+
+propose(members: array of string): string
+{
+ if (len members != 1)
+ return "one member only";
+ return nil;
+}
+
+archive()
+{
+ archiveobj := cardlib->archive();
+ allow->archive(archiveobj);
+ cardlib->archivearray(rows, "rows");
+ cardlib->archivearray(central, "central");
+ cardlib->setarchivename(chokey, "chokey");
+ cardlib->setarchivename(deck, "deck");
+ archiveobj.setattr("direction", string direction, None);
+ archiveobj.setattr("nredeals", string nredeals, None);
+}
+
+start(members: array of ref Member, archived: int)
+{
+ if (archived) {
+ archiveobj := cardlib->unarchive();
+ allow->unarchive(archiveobj);
+ rows = cardlib->getarchivearray("rows");
+ central = cardlib->getarchivearray("central");
+ chokey = cardlib->getarchiveobj("chokey");
+ deck = cardlib->getarchiveobj("deck");
+ direction = int archiveobj.getattr("direction");
+ nredeals = int archiveobj.getattr("nredeals");
+ } else {
+ p := members[0];
+ Cmember.join(p, -1).layout.lay.setvisibility(All);
+ startclique();
+ allow->add(CLICK, p, "click %o %d");
+ }
+}
+
+command(p: ref Member, cmd: string): string
+{
+ (err, tag, toks) := allow->action(p, cmd);
+ if (err != nil)
+ return err;
+ cp := Cmember.find(p);
+ if (cp == nil)
+ return "you are not playing";
+
+ case tag {
+ REDEAL =>
+ if (nredeals >= 3)
+ return "no more redeals";
+ redeal();
+ nredeals++;
+ CLICK =>
+ # click stack index
+ stack := clique.objects[int hd tl toks];
+ nc := len stack.children;
+ idx := int hd tl tl toks;
+ sel := cp.sel;
+ stype := stack.getattr("type");
+
+ if (sel.isempty() || sel.stack == stack) {
+ # selecting a card to move
+ if (idx < 0 || idx >= len stack.children)
+ return "invalid index";
+ case stype {
+ "row" or
+ "chokey" =>
+ select(cp, stack, (nc - 1, nc));
+ * =>
+ return "you can't move cards from there";
+ }
+ } else {
+ # selecting a stack to move to.
+ card := cardlib->getcard(sel.stack.children[sel.r.start]);
+ case stype {
+ "central" =>
+ top := cardlib->getcard(stack.children[nc - 1]);
+ if (direction == 0) {
+ if (card.number != (top.number + 1) % 13 &&
+ card.number != (top.number + 12) % 13)
+ return "out of sequence";
+ if (card.suit != top.suit)
+ return "wrong suit";
+ direction = card.number - top.number;
+ } else {
+ if (card.number != (top.number + direction + 13) % 13)
+ return "out of sequence";
+ if (card.suit != top.suit)
+ return "wrong suit";
+ }
+ "row" =>
+ if (nc == 0 || sel.stack.getattr("type") == "chokey")
+ return "you wish!";
+ top := cardlib->getcard(stack.children[nc - 1]);
+ if (card.suit != top.suit)
+ return "wrong suit";
+ if (card.number != (top.number + 1) % 13 &&
+ card.number != (top.number + 12) % 13)
+ return "out of sequence";
+ "chokey" =>
+ if (nc != 0)
+ return "only one card allowed there";
+ * =>
+ return "can't move there";
+ }
+ sel.transfer(stack, -1);
+ }
+ }
+ return nil;
+}
+
+startclique()
+{
+ addlayobj, addlayframe: import cardlib;
+
+ entry := clique.newobject(nil, All, "widget entry");
+ entry.setattr("command", "say", All);
+
+ but := clique.newobject(nil, All, "widget button");
+ but.setattr("text", "Redeal", All);
+ but.setattr("command", "redeal", All);
+ allow->add(REDEAL, Cmember.index(0).p, "redeal");
+
+ addlayframe("topf", nil, nil, dTOP|EXPAND|FILLX|aUPPERCENTRE, dTOP);
+ addlayobj(nil, "topf", nil, dLEFT, but);
+ addlayobj(nil, "topf", nil, dLEFT|EXPAND|FILLX, entry);
+
+ addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+
+ addlayframe("left", "arena", nil, dLEFT|EXPAND, dTOP);
+ addlayframe("central", "arena", nil, dLEFT|EXPAND, dTOP);
+ addlayframe("right", "arena", nil, dLEFT|EXPAND, dTOP);
+
+ rows = array[10] of {* => newstack(nil, Rowpilespec, "row")};
+ central = array[4] of {* => newstack(nil, Centralpilespec, "central")};
+ chokey = newstack(nil, Centralpilespec, "chokey");
+
+ deck = clique.newobject(nil, All, "stack");
+ cardlib->makecards(deck, (0, 13), nil);
+ cardlib->shuffle(deck);
+
+ for (i := 0; i < 5; i++)
+ addlayobj(nil, "left", nil, dTOP|oRIGHT, rows[i]);
+ for (i = 5; i < 10; i++)
+ addlayobj(nil, "right", nil, dTOP|oRIGHT, rows[i]);
+ for (i = 0; i < 4; i++)
+ addlayobj(nil, "central", nil, dTOP, central[i]);
+ addlayobj(nil, "central", nil, dTOP, chokey);
+
+ for (i = 0; i < 52; i++)
+ cardlib->setface(deck.children[i], 1);
+ # get top card from deck for central piles.
+ c := deck.children[len deck.children - 1];
+ v := cardlib->getcard(c);
+ j := 0;
+ for (i = len deck.children - 1; i >= 0; i--) {
+ w := cardlib->getcard(deck.children[i]);
+ if (w.number == v.number)
+ deck.transfer((i, i + 1), central[j++], -1);
+ }
+ for (i = 0; i < 10; i += 5) {
+ for (j = i; j < i + 4; j++)
+ deck.transfer((0, 5), rows[j], -1);
+ deck.transfer((0, 4), rows[j], -1);
+ }
+}
+
+redeal()
+{
+ for (i := 0; i < len rows; i++)
+ cardlib->discard(rows[i], deck, 0);
+ cardlib->shuffle(deck);
+
+ i = 0;
+ while ((n := len deck.children) > 0) {
+ l, r: int;
+ if (n >= 10)
+ l = r = 5;
+ else {
+ l = n / 2;
+ r = n - l;
+ }
+ deck.transfer((0, l), rows[i], 0);
+ deck.transfer((0, r), rows[i + 5], 0);
+ i++;
+ }
+
+ n = cardlib->nmembers();
+ for (i = 0; i < n; i++)
+ Cmember.index(i).sel.set(nil);
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype: string): ref Object
+{
+ stack := cardlib->newstack(parent, nil, spec);
+ stack.setattr("type", stype, None);
+ stack.setattr("actions", "click", All);
+ return stack;
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+ if (cp.sel.isempty()) {
+ cp.sel.set(stack);
+ cp.sel.setrange(r);
+ } else {
+ if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+ cp.sel.set(nil);
+ else
+ cp.sel.setrange(r);
+ }
+}
+
+archivearray(a: array of ref Object, name: string)
+{
+ for (i := 0; i < len a; i++)
+ cardlib->setarchivename(a[i], name + string i);
+}
+
+unarchivearray(a: array of ref Object, name: string)
+{
+ for (i := 0; i < len a; i++)
+ a[i] = cardlib->getarchiveobj(name + string i);
+}
diff --git a/appl/spree/engines/bounce.b b/appl/spree/engines/bounce.b
new file mode 100644
index 00000000..05e476d4
--- /dev/null
+++ b/appl/spree/engines/bounce.b
@@ -0,0 +1,258 @@
+implement Gatherengine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect: import draw;
+include "sets.m";
+ sets: Sets;
+ Set, All, None, A, B: import sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member: import spree;
+include "../gather.m";
+
+clique: ref Clique;
+
+W, H: con 500;
+INSET: con 20;
+D: con 30;
+BATLEN: con 100.0;
+GOALSIZE: con 0.1;
+
+MAXPLAYERS: con 32;
+nmembers := 0;
+
+Line: adt {
+ p1, p2: Point;
+ seg: fn(l: self Line, s1, s2: real): Line;
+};
+
+Dmember: adt {
+ p: ref Member;
+ score: int;
+ bat: ref Object;
+};
+
+Eusage: con "bad command usage";
+colours := array[4] of {"blue", "orange", "yellow", "white"};
+batpos: array of Line;
+borderpos: array of Line;
+
+members: array of Dmember;
+arena: ref Object;
+clienttype(): string
+{
+ return "bounce";
+}
+
+maxmembers(): int
+{
+ return 4;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+ return nil;
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+ sys = load Sys Sys->PATH;
+ draw = load Draw Draw->PATH;
+ clique = g;
+ spree = srvmod;
+
+ sets = load Sets Sets->PATH;
+ if (sets == nil) {
+ sys->print("spit: cannot load %s: %r\n", Sets->PATH);
+ return "bad module";
+ }
+ sets->init();
+
+ r := Rect((0, 0), (W, H));
+ walls := sides(r.inset(INSET));
+ addlines(segs(walls, 0.0, 0.5 - GOALSIZE), nil);
+ addlines(segs(walls, 0.5 + GOALSIZE, 1.0), nil);
+
+ batpos = l2a(segs(sides(r.inset(INSET + 50)), 0.1, 0.9));
+ borderpos = l2a(sides(r.inset(-1)));
+
+ arena = clique.newobject(nil, All, "arena");
+ arena.setattr("arenasize", string W + " " + string H, All);
+
+ return nil;
+}
+
+propose(members: array of string): string
+{
+ if (len members < 2)
+ return "need at least two members";
+ if (len members > 4)
+ return "too many members";
+ return nil;
+}
+
+archive()
+{
+}
+
+start(pl: array of ref Member, archived: int)
+{
+ if (archived) {
+ } else {
+ members = array[len pl] of Dmember;
+ for (i := 0; i < len pl; i++) {
+ p := pl[i];
+ bat := addline(batpos[i], nil);
+ bat.setattr("pos", "10 " + string (10.0 + BATLEN), All);
+ bat.setattr("owner", p.name, All);
+ addline(borderpos[i], ("owner", p.name) :: nil);
+ arena.setattr("member" + string i, p.name + " " + colours[i], All);
+ members[i] = (p, 0, bat);
+ }
+ r := Rect((0, 0), (W, H)).inset(INSET + 1);
+ goals := l2a(sides(r));
+ for (i = len members; i < len batpos; i++) {
+ addline(goals[i], nil);
+ addline(borderpos[i], ("owner", pl[0].name) :: nil);
+ }
+ }
+}
+
+addline(lp: (Point, Point), attrs: list of (string, string)): ref Object
+{
+ (p1, p2) := lp;
+ l := clique.newobject(nil, All, "line");
+ l.setattr("coords", p2s(p1) + " " + p2s(p2), All);
+ l.setattr("id", string l.id, All);
+ for (; attrs != nil; attrs = tl attrs) {
+ (attr, val) := hd attrs;
+ l.setattr(attr, val, All);
+ }
+ return l;
+}
+
+
+p2s(p: Point): string
+{
+ return string p.x + " " + string p.y;
+}
+
+command(member: ref Member, cmd: string): string
+{
+ ord := order(member);
+ sys->print("cmd: %s", cmd);
+ {
+ (n, toks) := sys->tokenize(cmd, " \n");
+ assert(n > 0, "unknown command");
+ case hd toks {
+ "newball" =>
+ # newball batid p.x p.y v.x v.y speed
+ assert(n == 7, Eusage);
+ bat := member.obj(int hd tl toks);
+ assert(bat != nil, "no such bat");
+ ball := clique.newobject(nil, All, "ball");
+ ball.setattr("state", string bat.id + " " + string ord +
+ " " + concat(tl tl toks) + " " + string sys->millisec(), All);
+ "lost" =>
+ # lost ballid
+ assert(n == 2, Eusage);
+ o := member.obj(int hd tl toks);
+ assert(o != nil, "bad object");
+ assert(o.getattr("state") != nil, "can only lose balls");
+ o.delete();
+ "state" =>
+ # state ballid lasthit owner p.x p.y v.x v.y s time
+ assert(n == 10, Eusage);
+ assert(ord >= 0, "you are not playing");
+ o := member.obj(int hd tl toks);
+ assert(o != nil, "object does not exist");
+ o.setattr("state", concat(tl tl toks), All);
+ members[ord].score++;
+ arena.setattr("score" + string ord, string members[ord].score, All);
+ "bat" =>
+ # bat pos
+ assert(n == 2, Eusage);
+ s1 := real hd tl toks;
+ members[ord].bat.setattr("pos", hd tl toks + " " + string (s1 + BATLEN), All);
+ "time" =>
+ # time millisec
+ assert(n == 2, Eusage);
+ tm := int hd tl toks;
+ offset := sys->millisec() - tm;
+ clique.action("time " + string offset + " " + string tm, nil, nil, None.add(member.id));
+ * =>
+ assert(0, "bad command");
+ }
+ } exception e {
+ "parse:*" =>
+ return e[6:];
+ }
+ return nil;
+}
+
+order(p: ref Member): int
+{
+ for (i := 0; i < len members; i++)
+ if (members[i].p == p)
+ return i;
+ return -1;
+}
+
+assert(b: int, err: string)
+{
+ if (b == 0)
+ raise "parse:" + err;
+}
+
+concat(v: list of string): string
+{
+ if (v == nil)
+ return nil;
+ s := hd v;
+ for (v = tl v; v != nil; v = tl v)
+ s += " " + hd v;
+ return s;
+}
+
+Line.seg(l: self Line, s1, s2: real): Line
+{
+ (dx, dy) := (l.p2.x - l.p1.x, l.p2.y - l.p1.y);
+ return (((l.p1.x + int (s1 * real dx)), l.p1.y + int (s1 * real dy)),
+ ((l.p1.x + int (s2 * real dx)), l.p1.y + int (s2 * real dy)));
+}
+
+sides(r: Rect): list of Line
+{
+ return ((r.min.x, r.min.y), (r.min.x, r.max.y)) ::
+ ((r.max.x, r.min.y), (r.max.x, r.max.y)) ::
+ ((r.min.x, r.min.y), (r.max.x, r.min.y)) ::
+ ((r.min.x, r.max.y), (r.max.x, r.max.y)) :: nil;
+}
+
+addlines(ll: list of Line, attrs: list of (string, string))
+{
+ for (; ll != nil; ll = tl ll)
+ addline(hd ll, attrs);
+}
+
+segs(ll: list of Line, s1, s2: real): list of Line
+{
+ nll: list of Line;
+ for (; ll != nil; ll = tl ll)
+ nll = (hd ll).seg(s1, s2) :: nll;
+ ll = nil;
+ for (; nll != nil; nll = tl nll)
+ ll = hd nll :: ll;
+ return ll;
+}
+
+l2a(ll: list of Line): array of Line
+{
+ a := array[len ll] of Line;
+ for (i := 0; ll != nil; ll = tl ll)
+ a[i++] = hd ll;
+ return a;
+}
diff --git a/appl/spree/engines/canfield.b b/appl/spree/engines/canfield.b
new file mode 100644
index 00000000..dbf3734f
--- /dev/null
+++ b/appl/spree/engines/canfield.b
@@ -0,0 +1,340 @@
+implement Gatherengine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ All, None: import Sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member: import spree;
+include "allow.m";
+ allow: Allow;
+include "cardlib.m";
+ cardlib: Cardlib;
+ Selection, Cmember, Card: import cardlib;
+ dTOP, dRIGHT, dLEFT, oDOWN,
+ aCENTRELEFT, aUPPERRIGHT,
+ EXPAND, FILLX, FILLY, Stackspec: import Cardlib;
+include "../gather.m";
+
+clique: ref Clique;
+
+sevens: array of ref Object; # [7]
+spare1, spare2: ref Object;
+acepiles: array of ref Object; # [4]
+top2botcount := 3;
+top2bot: ref Object;
+
+CLICK, TOP2BOT, REDEAL, SHOW: con iota;
+
+Openspec := Stackspec(
+ "display", # style
+ 19, # maxcards
+ 0, # conceal
+ "" # title
+);
+
+Pilespec := Stackspec(
+ "pile", # style
+ 19, # maxcards
+ 0, # conceal
+ "pile" # title
+);
+
+Untitledpilespec := Stackspec(
+ "pile", # style
+ 13, # maxcards
+ 0, # conceal
+ "" # title
+);
+
+clienttype(): string
+{
+ return "cards";
+}
+
+rank := array[] of {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
+
+maxmembers(): int
+{
+ return 1;
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+ allow = load Allow Allow->PATH;
+ if (allow == nil) {
+ sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+ return "bad module";
+ }
+ allow->init(spree, clique);
+ cardlib = load Cardlib Cardlib->PATH;
+ if (cardlib == nil) {
+ sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+ return "bad module";
+ }
+ cardlib->init(spree, clique);
+ return nil;
+}
+
+propose(members: array of string): string
+{
+ if (len members != 1)
+ return "one member only";
+ return nil;
+}
+
+start(members: array of ref Member, archived: int)
+{
+ allow->add(SHOW, nil, "show");
+ if (archived) {
+ archiveobj := cardlib->unarchive();
+ sevens = cardlib->getarchivearray("sevens");
+ acepiles = cardlib->getarchivearray("acepiles");
+ spare1 = cardlib->getarchiveobj("spare1");
+ spare2 = cardlib->getarchiveobj("spare2");
+ top2bot = cardlib->getarchiveobj("top2bot");
+ top2botcount = int archiveobj.getattr("top2botcount");
+
+ allow->unarchive(archiveobj);
+ archiveobj.delete();
+ } else {
+ p := members[0];
+ Cmember.join(p, -1).layout.lay.setvisibility(All);
+ startclique();
+ allow->add(CLICK, p, "click %o %d");
+ allow->add(TOP2BOT, p, "top2bot");
+ allow->add(REDEAL, p, "redeal");
+ }
+}
+
+archive()
+{
+ archiveobj := cardlib->archive();
+ cardlib->archivearray(sevens, "sevens");
+ cardlib->archivearray(acepiles, "acepiles");
+ cardlib->setarchivename(spare1, "spare1");
+ cardlib->setarchivename(spare2, "spare2");
+ cardlib->setarchivename(top2bot, "top2bot");
+ archiveobj.setattr("top2botcount", string top2botcount, None);
+ allow->archive(archiveobj);
+}
+
+command(p: ref Member, cmd: string): string
+{
+ (err, tag, toks) := allow->action(p, cmd);
+ if (err != nil)
+ return err;
+ cp := Cmember.find(p);
+ if (cp == nil)
+ return "you are not playing";
+
+ case tag {
+ CLICK =>
+ # click stack index
+ stack := clique.objects[int hd tl toks];
+ nc := len stack.children;
+ idx := int hd tl tl toks;
+ sel := cp.sel;
+ stype := stack.getattr("type");
+ if (sel.isempty() || sel.stack == stack) {
+ if (nc == 0 && stype == "spare1") {
+ cardlib->flip(spare2);
+ spare2.transfer((0, len spare2.children), spare1, 0);
+ return nil;
+ }
+ if (idx < 0 || idx >= len stack.children)
+ return "invalid index";
+ case stype {
+ "spare2" or
+ "open" =>
+ select(cp, stack, (idx, nc));
+ "spare1" =>
+ if ((n := nc) > 3)
+ n = 3;
+ for (i := 0; i < n; i++) {
+ cardlib->setface(stack.children[nc - 1], 1);
+ stack.transfer((nc - 1, nc), spare2, -1);
+ nc--;
+ }
+ * =>
+ return "you can't move cards from there";
+ }
+ } else {
+ from := sel.stack;
+ case stype {
+ "acepile" =>
+ if (sel.r.end != sel.r.start + 1)
+ return "only one card at a time!";
+ card := getcard(sel.stack.children[sel.r.start]);
+ if (nc == 0) {
+ if (card.number != 0)
+ return "aces only";
+ } else {
+ top := getcard(stack.children[nc - 1]);
+ if (card.number != top.number + 1)
+ return "out of sequence";
+ if (card.suit != top.suit)
+ return "wrong suit";
+ }
+ sel.transfer(stack, -1);
+ "open" =>
+ c := getcard(sel.stack.children[sel.r.start]);
+ col := !isred(c);
+ n := c.number + 1;
+ for (i := sel.r.start; i < sel.r.end; i++) {
+ c2 := getcard(sel.stack.children[i]);
+ if (c2.face == 0)
+ return "cannot move face-down cards";
+ if (isred(c2) == col)
+ return "bad colour sequence";
+ if (c2.number != n - 1)
+ return "bad number sequence";
+ n = c2.number;
+ col = isred(c2);
+ }
+ if (nc != 0) {
+ c2 := getcard(stack.children[nc - 1]);
+ if (isred(c2) == isred(c) || c2.number != c.number + 1)
+ return "invalid move";
+ } else if (c.number != 12)
+ return "only kings allowed there";
+ sel.transfer(stack, -1);
+ * =>
+ return "can't move there";
+ }
+ if (from.getattr("type") == "open" && len from.children > 0)
+ cardlib->setface(from.children[len from.children - 1], 1);
+ }
+ TOP2BOT =>
+ if (len spare2.children != 0)
+ return "can only top-to-bottom on the whole pile";
+ if (top2botcount <= 0)
+ return "too late";
+ nc := len spare1.children;
+ if (nc > 0) {
+ spare1.transfer((nc - 1, nc), spare1, 0);
+ top2botcount--;
+ settop2bottext();
+ }
+ REDEAL =>
+ clearup();
+ cardlib->shuffle(spare1);
+ deal();
+ top2botcount = 3;
+ settop2bottext();
+ SHOW =>
+ clique.show(nil);
+ }
+ return nil;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+ return nil;
+}
+
+settop2bottext()
+{
+ top2bot.setattr("text",
+ sys->sprint("top to bottom (%d left)", top2botcount), All);
+}
+
+startclique()
+{
+ addlayobj, addlayframe: import cardlib;
+
+ entry := clique.newobject(nil, All, "widget entry");
+ entry.setattr("command", "say", All);
+ addlayobj("entry", nil, nil, dTOP|FILLX, entry);
+ addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+
+ addlayframe("top", "arena", nil, dTOP|EXPAND, dTOP);
+ addlayframe("mid", "arena", nil, dTOP|EXPAND, dTOP);
+ addlayframe("bot", "arena", nil, dTOP|EXPAND, dTOP);
+
+ sevens = array[7] of {* => newstack(nil, Openspec, "open")};
+ acepiles = array[4] of {* => newstack(nil, Untitledpilespec, "acepile")};
+ spare1 = newstack(nil, Untitledpilespec, "spare1");
+ spare2 = newstack(nil, Untitledpilespec, "spare2");
+
+ cardlib->makecards(spare1, (0, 13), nil);
+
+ for (i := 0; i < 4; i++)
+ addlayobj(nil, "top", nil, dRIGHT, acepiles[i]);
+ for (i = 0; i < len sevens; i++)
+ addlayobj(nil, "mid", nil, dLEFT|oDOWN|EXPAND, sevens[i]);
+ addlayframe("buts", "bot", nil, dLEFT|EXPAND|aUPPERRIGHT, dTOP);
+ top2bot = newbutton("top2bot", "top to bottom");
+ addlayobj(nil, "buts", nil, dTOP, top2bot);
+ addlayobj(nil, "buts", nil, dTOP, newbutton("redeal", "redeal"));
+ addlayobj(nil, "bot", nil, dLEFT, spare1);
+ addlayobj(nil, "bot", nil, dLEFT|EXPAND|aCENTRELEFT, spare2);
+ deal();
+ settop2bottext();
+}
+
+clearup()
+{
+ for (i := 0; i < len sevens; i++)
+ cardlib->discard(sevens[i], spare1, 1);
+ for (i = 0; i < len acepiles; i++)
+ cardlib->discard(acepiles[i], spare1, 1);
+ cardlib->discard(spare2, spare1, 1);
+}
+
+deal()
+{
+ cardlib->shuffle(spare1);
+
+ for (i := 0; i < 7; i++) {
+ spare1.transfer((0, i + 1), sevens[i], 0);
+ cardlib->setface(sevens[i].children[i], 1);
+ }
+
+}
+
+newbutton(cmd, text: string): ref Object
+{
+ but := clique.newobject(nil, All, "widget button");
+ but.setattr("command", cmd, All);
+ but.setattr("text", text, All);
+ return but;
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype: string): ref Object
+{
+ stack := cardlib->newstack(parent, nil, spec);
+ stack.setattr("type", stype, None);
+ stack.setattr("actions", "click", All);
+ return stack;
+}
+
+getcard(card: ref Object): Card
+{
+ c := cardlib->getcard(card);
+ c.number = rank[c.number];
+ return c;
+}
+
+isred(c: Card): int
+{
+ return c.suit == Cardlib->DIAMONDS || c.suit == Cardlib->HEARTS;
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+ if (cp.sel.isempty()) {
+ cp.sel.set(stack);
+ cp.sel.setrange(r);
+ } else {
+ if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+ cp.sel.set(nil);
+ else
+ cp.sel.setrange(r);
+ }
+}
diff --git a/appl/spree/engines/chat.b b/appl/spree/engines/chat.b
new file mode 100644
index 00000000..c409b272
--- /dev/null
+++ b/appl/spree/engines/chat.b
@@ -0,0 +1,60 @@
+implement Engine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member: import spree;
+
+clique: ref Clique;
+
+clienttype(): string
+{
+ return "chat";
+}
+
+init(g: ref Clique, srvmod: Spree): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+ return nil;
+}
+
+join(nil: ref Member): string
+{
+ return nil;
+}
+
+leave(nil: ref Member)
+{
+}
+
+Eusage: con "bad command usage";
+
+command(member: ref Member, cmd: string): string
+{
+ e := ref Sys->Exception;
+ if (sys->rescue("parse:*", e) == Sys->EXCEPTION) {
+ sys->rescued(Sys->ONCE, nil);
+ return e.name[6:];
+ }
+ (n, toks) := sys->tokenize(cmd, " \n");
+ assert(n > 0, "unknown command");
+ case hd toks {
+ "say" =>
+ # say something
+ assert(n == 2, Eusage);
+ clique.action("say " + string member.id + " " + hd tl toks, nil, nil, ~0);
+ * =>
+ assert(0, "bad command");
+ }
+ return nil;
+}
+
+assert(b: int, err: string)
+{
+ if (b == 0)
+ sys->raise("parse:" + err);
+}
diff --git a/appl/spree/engines/debug.b b/appl/spree/engines/debug.b
new file mode 100644
index 00000000..96acede0
--- /dev/null
+++ b/appl/spree/engines/debug.b
@@ -0,0 +1,163 @@
+implement Engine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member: import spree;
+
+clique: ref Clique;
+
+init(g: ref Clique, srvmod: Spree): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+ return nil;
+}
+
+join(nil: ref Member): string
+{
+ return nil;
+}
+
+leave(nil: ref Member)
+{
+}
+
+number := 0;
+currmember: ref Member;
+
+obj(ext: int): ref Object
+{
+ o := currmember.obj(ext);
+ if (o == nil)
+ sys->raise("parse:bad object");
+ return o;
+}
+
+Eusage: con "bad command usage";
+
+assert(b: int, err: string)
+{
+ if (b == 0)
+ sys->raise("parse:" + err);
+}
+
+command(member: ref Member, cmd: string): string
+{
+ e := ref Sys->Exception;
+ if (sys->rescue("parse:*", e) == Sys->EXCEPTION) {
+ sys->rescued(Sys->ONCE, nil);
+ currmember = nil;
+ return e.name[6:];
+ }
+ currmember = member;
+ (nlines, lines) := sys->tokenize(cmd, "\n");
+ assert(nlines > 0, "unknown command");
+ (n, toks) := sys->tokenize(hd lines, " ");
+ assert(n > 0, "unknown command");
+ case hd toks {
+ "new" => # new parent visibility\nvisibility attr val\nvisibility attr val...
+ assert(n == 3, Eusage);
+ setattrs(clique.newobject(obj(int hd tl toks), int hd tl tl toks), tl lines);
+ "deck" =>
+ stack := clique.newobject(nil, ~0);
+ stack.setattr("type", "stack", ~0);
+ for (i := 0; i < 6; i++) {
+ o := clique.newobject(stack, ~0);
+ o.setattr("face", "down", ~0);
+ o.setattr("number", string number++, 0);
+ }
+ "flip" =>
+ # flip objid start [end]
+ assert(n == 2 || n == 3 || n == 4, Eusage);
+ o := obj(int hd tl toks);
+ if (n > 2) {
+ start := int hd tl tl toks;
+ end := start + 1;
+ if (n == 4)
+ end = int hd tl tl tl toks;
+ assert(start >= 0 && start < len o.children &&
+ end >= start && end >= 0 && end <= len o.children, "index out of range");
+ for (; start < end; start++)
+ flip(o.children[start]);
+ } else
+ flip(o);
+
+ "set" => # set objid attr val
+ assert(n == 4, Eusage);
+ obj(int hd tl toks).setattr(hd tl tl toks, hd tl tl tl toks, ~0);
+ "vis" => # vis objid flags
+ assert(n == 3, Eusage);
+ obj(int hd tl toks).setvisibility(int hd tl tl toks);
+ "attrvis" => # attrvis objid attr flags
+ assert(n == 4, Eusage);
+ o := obj(int hd tl toks);
+ name := hd tl tl toks;
+ attr := o.attrs.get(name);
+ assert(attr != nil, "attribute not found");
+ o.setattrvisibility(name, int hd tl tl tl toks);
+ "show" => # show [memberid]
+ p: ref Member = nil;
+ if (n == 2) {
+ memberid := int hd tl toks;
+ p = clique.member(memberid);
+ assert(p != nil, "bad memberid");
+ }
+ clique.show(p);
+ "del" or "delete" => # del obj
+ assert(n == 2, Eusage);
+ obj(int hd tl toks).delete();
+ "tx" => # tx src from to dest [index]
+ assert(n == 5 || n == 6, Eusage);
+ src, dest: ref Object;
+ r: Range;
+ (src, toks) = (obj(int hd tl toks), tl tl toks);
+ (r.start, toks) = (int hd toks, tl toks);
+ (r.end, toks) = (int hd toks, tl toks);
+ (dest, toks) = (obj(int hd toks), tl toks);
+ index := len dest.children;
+ if (n == 6)
+ index = int hd toks;
+ assert(r.start >= 0 && r.start < len src.children &&
+ r.end >= 0 && r.end <= len src.children && r.end >= r.start,
+ "bad range");
+ src.transfer(r, dest, index);
+ * =>
+ assert(0, "bad command");
+ }
+ currmember = nil;
+ return nil;
+}
+
+
+flip(o: ref Object)
+{
+ face := o.getattr("face");
+ if (face == "down") {
+ face = "up";
+ o.setattrvisibility("number", ~0);
+ } else {
+ face = "down";
+ o.setattrvisibility("number", 0);
+ }
+ o.setattr("face", face, ~0);
+}
+
+setattrs(o: ref Object, lines: list of string): string
+{
+ for (; lines != nil; lines = tl lines) {
+ # attr val [visibility]
+ (n, toks) := sys->tokenize(hd lines, " ");
+ if (n != 2 && n != 3)
+ return "bad attribute line";
+ vis := 0;
+ if (n == 3)
+ vis = int hd tl tl toks;
+ o.setattr(hd toks, hd tl toks, vis);
+ }
+ return nil;
+}
+
diff --git a/appl/spree/engines/freecell.b b/appl/spree/engines/freecell.b
new file mode 100644
index 00000000..8005926c
--- /dev/null
+++ b/appl/spree/engines/freecell.b
@@ -0,0 +1,428 @@
+implement Gatherengine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ sets: Sets;
+ Set, set, A, B, All, None: import sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+ allow: Allow;
+include "cardlib.m";
+ cardlib: Cardlib;
+ Selection, Cmember, Card: import cardlib;
+ getcard: import cardlib;
+ dTOP, dRIGHT, dLEFT, oRIGHT, oDOWN,
+ aCENTRERIGHT, aCENTRELEFT, aUPPERRIGHT,
+ EXPAND, FILLX, FILLY, Stackspec: import Cardlib;
+include "../gather.m";
+
+clique: ref Clique;
+
+open: array of ref Object; # [8]
+cells: array of ref Object; # [4]
+acepiles: array of ref Object; # [4]
+txpiles: array of ref Object; # [len open + len cells]
+deck: ref Object;
+
+fnames := array[] of {
+"qua",
+"quack",
+"quackery",
+"quad",
+"quadrangle",
+"quadrangular",
+"quadrant",
+"quadratic",
+"quadrature",
+"quadrennial",
+};
+dir(name: string, perm: int, owner: string): Sys->Dir
+{
+ d := Sys->zerodir;
+ d.name = name;
+ d.uid = owner;
+ d.gid = owner;
+ d.qid.qtype = (perm >> 24) & 16rff;
+ d.mode = perm;
+ # d.atime = now;
+ # d.mtime = now;
+ return d;
+}
+
+
+suitsout := array[4] of {* => -1};
+
+mainmember: ref Cmember;
+
+CLICK: con iota;
+
+Openspec := Stackspec(
+ "display", # style
+ 19, # maxcards
+ 0, # conceal
+ "" # title
+);
+
+Pilespec := Stackspec(
+ "pile", # style
+ 19, # maxcards
+ 0, # conceal
+ "pile" # title
+);
+
+Untitledpilespec := Stackspec(
+ "pile", # style
+ 13, # maxcards
+ 0, # conceal
+ "" # title
+);
+
+clienttype(): string
+{
+ return "cards";
+}
+
+maxmembers(): int
+{
+ return 1;
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+ allow = load Allow Allow->PATH;
+ if (allow == nil) {
+ sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+ return "bad module";
+ }
+ allow->init(spree, clique);
+ sets = load Sets Sets->PATH;
+ if (sets == nil) {
+ sys->print("whist: cannot load %s: %r\n", Sets->PATH);
+ return "bad module";
+ }
+ cardlib = load Cardlib Cardlib->PATH;
+ if (cardlib == nil) {
+ sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+ return "bad module";
+ }
+ cardlib->init(spree, clique);
+ g.fcreate(0, -1, dir("data", 8r555|Sys->DMDIR, "spree"));
+ for(i := 0; i < len fnames; i++)
+ g.fcreate(i + 1, 0, dir(fnames[i], 8r444, "arble"));
+ return nil;
+}
+
+propose(members: array of string): string
+{
+ if (len members != 1)
+ return "one member only";
+ return nil;
+}
+
+start(members: array of ref Member, archived: int)
+{
+sys->print("freecell: starting\n");
+ if (archived) {
+ archiveobj := cardlib->unarchive();
+ open = cardlib->getarchivearray("open");
+ cells = cardlib->getarchivearray("cells");
+ acepiles = cardlib->getarchivearray("acepiles");
+ txpiles = cardlib->getarchivearray("txpiles");
+ deck = cardlib->getarchiveobj("deck");
+ for (i := 0; i < len suitsout; i++)
+ suitsout[i] = int archiveobj.getattr("suitsout" + string i);
+ mainmember = Cmember.findid(int archiveobj.getattr("mainmember"));
+ allow->unarchive(archiveobj);
+ archiveobj.delete();
+ } else {
+ sys->print("freecell: starting afresh\n");
+ mainmember = Cmember.join(members[0], -1);
+ mainmember.layout.lay.setvisibility(All);
+ startclique();
+ movefree();
+ allow->add(CLICK, members[0], "click %o %d");
+ }
+}
+
+readfile(f: int, boffset: big, n: int): array of byte
+{
+ offset := int boffset;
+ f--;
+ if (f < 0 || f >= len fnames)
+ return nil;
+ data := array of byte fnames[f];
+ if (offset >= len data)
+ return nil;
+ if (offset + n > len data)
+ n = len data - offset;
+ return data[offset:offset + n];
+}
+
+archive()
+{
+ sys->print("freecell: archiving\n");
+ archiveobj := cardlib->archive();
+ cardlib->archivearray(open, "open");
+ cardlib->archivearray(cells, "cells");
+ cardlib->archivearray(acepiles, "acepiles");
+ cardlib->archivearray(txpiles, "txpiles");
+ cardlib->setarchivename(deck, "deck");
+ for (i := 0; i < len suitsout; i++)
+ archiveobj.setattr("suitsout" + string i, string suitsout[i], None);
+ archiveobj.setattr("mainmember", string mainmember.id, None);
+ allow->archive(archiveobj);
+}
+
+command(p: ref Member, cmd: string): string
+{
+ (err, tag, toks) := allow->action(p, cmd);
+ if (err != nil)
+ return err;
+ cp := Cmember.find(p);
+ if (cp == nil)
+ return "you are not playing";
+ case tag {
+ CLICK =>
+ # click stack index
+ stack := clique.objects[int hd tl toks];
+ nc := len stack.children;
+ idx := int hd tl tl toks;
+ sel := cp.sel;
+ stype := stack.getattr("type");
+ if (sel.isempty() || sel.stack == stack) {
+ if (idx < 0 || idx >= len stack.children)
+ return "invalid index";
+ case stype {
+ "cell" or
+ "open" =>
+ select(cp, stack, (idx, nc));
+ * =>
+ return "you can't move cards from there";
+ }
+ } else {
+ from := sel.stack;
+ case stype {
+ "acepile" =>
+ if (sel.r.end != sel.r.start + 1)
+ return "only one card at a time!";
+ addtoacepile(sel.stack);
+ sel.set(nil);
+ movefree();
+ "open" =>
+ c := getcard(sel.stack.children[sel.r.start]);
+ col := !isred(c.suit);
+ n := c.number + 1;
+ for (i := sel.r.start; i < sel.r.end; i++) {
+ c2 := getcard(sel.stack.children[i]);
+ if (isred(c2.suit) == col)
+ return "bad colour sequence";
+ if (c2.number != n - 1)
+ return "bad number sequence";
+ n = c2.number;
+ col = isred(c2.suit);
+ }
+ if (nc != 0) {
+ c2 := getcard(stack.children[nc - 1]);
+ if (isred(c2.suit) == isred(c.suit) || c2.number != c.number + 1)
+ return "opposite colours, descending, only";
+ }
+ r := sel.r;
+ selstack := sel.stack;
+ sel.set(nil);
+ fc := freecells(stack);
+ if (r.end - r.start - 1 > len fc)
+ return "not enough free cells";
+ n = 0;
+ for (i = r.end - 1; i >= r.start + 1; i--)
+ selstack.transfer((i, i + 1), fc[n++], -1);
+ selstack.transfer((i, i + 1), stack, -1);
+ while (--n >= 0)
+ fc[n].transfer((0, 1), stack, -1);
+ movefree();
+ "cell" =>
+ if (sel.r.end - sel.r.start > 1 || nc > 0)
+ return "only one card allowed there";
+ sel.transfer(stack, -1);
+ movefree();
+ * =>
+ return "can't move there";
+ }
+ }
+ }
+ return nil;
+}
+
+freecells(dest: ref Object): array of ref Object
+{
+ fc := array[len txpiles] of ref Object;
+ n := 0;
+ for (i := 0; i < len txpiles; i++)
+ if (len txpiles[i].children == 0 && txpiles[i] != dest)
+ fc[n++] = txpiles[i];
+ return fc[0:n];
+}
+
+# move any cards that can be moved.
+movefree()
+{
+ nmoved := 1;
+ while (nmoved > 0) {
+ nmoved = 0;
+ for (i := 0; i < len txpiles; i++) {
+ pile := txpiles[i];
+ nc := len pile.children;
+ if (nc == 0)
+ continue;
+ card := getcard(pile.children[nc - 1]);
+ if (suitsout[card.suit] != card.number - 1)
+ continue;
+ # card can be moved; now make sure there's no card out
+ # that might be moved onto this card
+ for (j := 0; j < len suitsout; j++)
+ if (isred(j) != isred(card.suit) && card.number > 1 && suitsout[j] < card.number - 1)
+ break;
+ if (j == len suitsout) {
+ addtoacepile(pile);
+ nmoved++;
+ }
+ }
+ }
+}
+
+addtoacepile(pile: ref Object)
+{
+ nc := len pile.children;
+ if (nc == 0)
+ return;
+ card := getcard(pile.children[nc - 1]);
+ for (i := 0; i < len acepiles; i++) {
+ anc := len acepiles[i].children;
+ if (anc == 0) {
+ if (card.number == 0)
+ break;
+ continue;
+ }
+ acard := getcard(acepiles[i].children[anc - 1]);
+ if (acard.suit == card.suit && acard.number == card.number - 1)
+ break;
+ }
+ if (i < len acepiles) {
+ pile.transfer((nc - 1, nc), acepiles[i], -1);
+ suitsout[card.suit] = card.number;
+ }
+}
+
+startclique()
+{
+ addlayobj, addlayframe: import cardlib;
+
+ open = array[8] of {* => newstack(nil, Openspec, "open", nil)};
+ acepiles = array[4] of {* => newstack(nil, Untitledpilespec, "acepile", nil)};
+ cells = array[4] of {* => newstack(nil, Untitledpilespec, "cell", "cell")};
+ for (i := 0; i < len cells; i++)
+ cells[i].setattr("showsize", "0", All);
+
+ txpiles = array[12] of ref Object;
+ txpiles[0:] = open;
+ txpiles[len open:] = cells;
+ deck = clique.newobject(nil, All, "stack");
+
+ cardlib->makecards(deck, (0, 13), nil);
+
+ addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+ addlayframe("top", "arena", nil, dTOP|EXPAND, dTOP);
+ addlayframe("bot", "arena", nil, dTOP|EXPAND, dTOP);
+ for (i = 0; i < 4; i++)
+ addlayobj(nil, "top", nil, dRIGHT, acepiles[i]);
+ for (i = 0; i < 4; i++)
+ addlayobj(nil, "top", nil, dLEFT, cells[i]);
+ for (i = 0; i < len open; i++)
+ addlayobj(nil, "bot", nil, dLEFT|oDOWN|EXPAND, open[i]);
+ deal();
+}
+
+deal()
+{
+ cardlib->shuffle(deck);
+ cardlib->deal(deck, 7, open, 0);
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype, title: string): ref Object
+{
+ stack := cardlib->newstack(parent, nil, spec);
+ stack.setattr("type", stype, None);
+ stack.setattr("actions", "click", All);
+ stack.setattr("title", title, All);
+ return stack;
+}
+
+isred(suit: int): int
+{
+ return suit == Cardlib->DIAMONDS || suit == Cardlib->HEARTS;
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+ if (cp.sel.isempty()) {
+ cp.sel.set(stack);
+ cp.sel.setrange(r);
+ } else {
+ if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+ cp.sel.set(nil);
+ else
+ cp.sel.setrange(r);
+ }
+}
+
+#randstate := 1;
+#srand(seed: int)
+#{
+# randstate = seed;
+#}
+#
+#rand(): int
+#{
+# randstate = randstate * 214013 + 2531011;
+# return (randstate >> 16) & 0x7fff;
+#}
+##From: jimh@MICROSOFT.com (Jim Horne)
+##
+##I'm happy to share the card shuffle algorithm, but I warn you,
+##it does depend on the rand() and srand() function built into MS
+##compilers. The good news is that I believe these work the same
+##for all our compilers.
+##
+##I use cards.dll which has it's own mapping of numbers (0-51) to
+##cards. The following will give you the idea. Play around with
+##this and you'll be able to generate all the cliques.
+##
+##Go ahead and post the code. People might as well have fun with it.
+##Please keep me posted on anything interesting that comes of it.
+##Thanks.
+#
+#msdeal(cliquenumber: int): array of array of Card
+#{
+# deck := array[52] of Card;
+# for (i := 0; i < len deck; i++) # put unique card in each deck loc.
+# deck[i] = Card(i % 4, i / 4, 0);
+# wleft := 52; # cards left to be chosen in shuffle
+# cards := array[8] of {* => array[7] of Card};
+# max := array[8] of {* => 0};
+# srand(cliquenumber);
+# for (i = 0; i < 52; i++) {
+# j := rand() % wleft;
+# card[i % 8][i / 8] = deck[j];
+# max[i % 8] = i / 8;
+# deck[j] = deck[--wleft];
+# }
+# for (i = 0; i < len cards; i++)
+# cards[i] = cards[i][0:max[i]];
+# return cards;
+#}
diff --git a/appl/spree/engines/gather.b b/appl/spree/engines/gather.b
new file mode 100644
index 00000000..59b7bfb2
--- /dev/null
+++ b/appl/spree/engines/gather.b
@@ -0,0 +1,267 @@
+implement Engine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ sets: Sets;
+ Set, set, A, B, All, None: import sets;
+include "../spree.m";
+ spree: Spree;
+ archives: Archives;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "daytime.m";
+ daytime: Daytime;
+include "../gather.m";
+
+clique: ref Clique;
+
+started := 0;
+halted := 0;
+suspended: Set; # set of members currently suspended from the clique.
+count := 0;
+nmembers := 0;
+title := "unknown";
+cliquemod: Gatherengine;
+
+members: Set;
+watchers: Set;
+
+invited: list of string;
+
+# options:
+# <n> cliquemodule opts
+init(srvmod: Spree, g: ref Clique, argv: list of string): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+ sets = load Sets Sets->PATH;
+ if (sets == nil) {
+ sys->print("gather: cannot load %s: %r\n", Sets->PATH);
+ return "bad module";
+ }
+ sets->init();
+ daytime = load Daytime Daytime->PATH;
+ if (daytime == nil) {
+ sys->print("gather: cannot load %s: %r\n", Daytime->PATH);
+ return "bad module";
+ }
+ archives = load Archives Archives->PATH;
+ if (archives == nil) {
+ sys->print("gather: cannot load %s: %r\n", Archives->PATH);
+ return "bad module";
+ }
+ archives->init(srvmod);
+ argv = tl argv;
+ n := len argv;
+ if (n < 2)
+ return "bad init options";
+ count = int hd argv;
+ if (count != -1 && count <= 0)
+ return "bad gather count";
+ argv = tl argv;
+ if (count < len clique.archive.members)
+ count = len clique.archive.members;
+ cliquemod = load Gatherengine "/dis/spree/engines/" + hd argv + ".dis";
+ if (cliquemod == nil)
+ return sys->sprint("bad module: %r");
+ title = concat(argv);
+ e := cliquemod->init(srvmod, clique, tl argv, len clique.archive.members > 0);
+ if (e != nil)
+ return e;
+ if (len clique.archive.members > 0) {
+ for (i := 0; i < len clique.archive.members; i++)
+ invited = clique.archive.members[i] :: invited;
+ } else
+ invited = clique.owner() :: nil;
+ for (inv := invited; inv != nil; inv = tl inv)
+ clique.notify(clique.parentid, "invite " + hd inv);
+ clique.notify(clique.parentid, "title (" + title + ")");
+ return nil;
+}
+
+join(p: ref Member, cmd: string, susp: int): string
+{
+sys->print("gather: %s[%d] joining '%s' (suspended: %d)\n", p.name, p.id, cmd, susp);
+ case cmd {
+ "join" =>
+ if (started) {
+ if (!susp || !halted)
+ return "clique has already started";
+ suspended = suspended.del(p.id);
+ if (suspended.eq(None)) {
+ halted = 0;
+ # XXX inform participants that clique is starting again
+ }
+ pset := None.add(p.id);
+ clique.action("clienttype " + cliquemod->clienttype(), nil, nil, pset);
+ clique.breakmsg(pset);
+ return nil;
+ }
+ for (inv := invited; inv != nil; inv = tl inv)
+ if (hd inv == p.name || hd inv == "all")
+ break;
+ if (inv == nil)
+ return "you have not been invited";
+ if (nmembers >= cliquemod->maxmembers() || (count != -1 && nmembers >= count))
+ return "too many members already";
+ if (len clique.archive.members > 0) {
+ for (i := 0; i < len clique.archive.members; i++)
+ if (p.name == clique.archive.members[i])
+ break;
+ if (i == len clique.archive.members)
+ return "you are not part of that clique";
+ }
+ nmembers++;
+ members = members.add(p.id);
+ clique.notify(clique.parentid, "join " + p.name);
+ s := None.add(p.id);
+ # special case for single member cliques: don't need a gather client as we can start right now.
+ if (cliquemod->maxmembers() == 1)
+ return startclique();
+ clique.action("clienttype gather", nil, nil, s);
+ clique.breakmsg(s);
+ clique.action("title " + title, nil, nil, s);
+ clique.action("join " + p.name, nil, nil, All);
+ "watch" =>
+ if (susp)
+ return "you cannot watch if you are playing";
+ watchers = watchers.add(p.id);
+ s := None.add(p.id);
+ if (started)
+ clique.action("clienttype " + cliquemod->clienttype(), nil, nil, s);
+ else
+ clique.action("clienttype gather", nil, nil, s);
+ clique.breakmsg(s);
+ if (!started)
+ clique.action("watch " + p.name, nil, nil, All);
+ * =>
+ return "unknown join request";
+ }
+ return nil;
+}
+
+leave(p: ref Member): int
+{
+ if (members.holds(p.id)) {
+ if (started) {
+ suspended = suspended.add(p.id);
+ if (suspended.eq(members)) {
+ cliquemod->archive();
+ name := spree->newarchivename();
+ e := archives->write(clique,
+ ("title", concat(tl tl clique.archive.argv)) ::
+ ("date", string daytime->now()) :: nil,
+ name, members);
+ if (e != nil)
+ sys->print("warning: cannot archive clique: %s\n", e);
+ else
+ clique.notify(clique.parentid, "archived " + name);
+ clique.hangup();
+ return 1;
+ } else {
+ halted = 1;
+ return 0;
+ }
+ }
+
+ members = members.del(p.id);
+ nmembers--;
+ clique.notify(clique.parentid, "leave " + p.name);
+ if (nmembers == 0)
+ clique.hangup();
+ } else {
+ watchers = watchers.del(p.id);
+ clique.action("unwatch " + p.name, nil, nil, All);
+ }
+ return 1;
+}
+
+notify(nil: int, note: string)
+{
+ (n, toks) := sys->tokenize(note, " ");
+ case hd toks {
+ "invite" =>
+ invited = hd tl toks :: invited;
+ "uninvite" =>
+ inv := invited;
+ for (invited = nil; inv != nil; inv = tl inv)
+ if (hd inv != hd tl toks)
+ invited = hd inv :: invited;
+ * =>
+ sys->print("gather: unknown notification '%s'\n", note);
+ }
+}
+
+command(p: ref Member, cmd: string): string
+{
+ if (halted)
+ return "clique is halted for the time being";
+ if (started) {
+ if (!members.holds(p.id)) {
+sys->print("members (%s) doesn't hold %s[%d]\n", members.str(), p.name, p.id);
+ return "you are only watching";
+ }
+ return cliquemod->command(p, cmd);
+ }
+
+ (n, toks) := sys->tokenize(cmd, " \n");
+ if (n == 0)
+ return "bad command";
+ case hd toks {
+ "start" =>
+ if (len clique.archive.members == 0 && p.name != clique.owner())
+ return "only the owner can start a clique";
+ if (count != -1 && nmembers != count)
+ return "need " + string count + " members";
+ return startclique();
+ "chat" =>
+ clique.action("chat " + p.name + " " + concat(tl toks), nil, nil, All);
+ * =>
+ return "unknown command";
+ }
+ return nil;
+}
+
+startclique(): string
+{
+ # XXX could randomly shuffle members here
+
+ pa := array[nmembers] of ref Member;
+ names := array[nmembers] of string;
+ j := nmembers;
+ for (i := members.limit(); i >= 0; i--)
+ if (members.holds(i)) {
+ pa[--j] = clique.member(i);
+ names[j] = pa[j].name;
+ }
+ e := cliquemod->propose(names);
+ if (e != nil)
+ return e;
+ clique.action("clienttype " + cliquemod->clienttype(), nil, nil, All);
+ clique.breakmsg(All);
+ cliquemod->start(pa, len clique.archive.members > 0);
+ clique.start();
+ started = 1;
+ clique.notify(clique.parentid, "started");
+ clique.notify(clique.parentid, "title " + concat(tl tl clique.archive.argv));
+ return nil;
+}
+
+readfile(f: int, offset: big, n: int): array of byte
+{
+ if (!started)
+ return nil;
+ return cliquemod->readfile(f, offset, n);
+}
+
+concat(l: list of string): string
+{
+ if (l == nil)
+ return nil;
+ s := hd l;
+ for (l = tl l; l != nil; l = tl l)
+ s += " " + hd l;
+ return s;
+}
diff --git a/appl/spree/engines/hearts.b b/appl/spree/engines/hearts.b
new file mode 100644
index 00000000..759f07e2
--- /dev/null
+++ b/appl/spree/engines/hearts.b
@@ -0,0 +1,300 @@
+implement Engine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+ allow: Allow;
+include "cardlib.m";
+ cardlib: Cardlib;
+ dTOP, dLEFT, oLEFT, oRIGHT, EXPAND, FILLX, FILLY, Stackspec: import Cardlib;
+include "tricks.m";
+ tricks: Tricks;
+ Trick: import tricks;
+clique: ref Clique;
+CLICK, START, SAY: con iota;
+
+started := 0;
+
+buttons: ref Object;
+scores: ref Object;
+deck, pile: ref Object;
+hands, taken, passon: array of ref Object;
+
+MINPLAYERS: con 2;
+MAXPLAYERS: con 4;
+
+leader, turn: int;
+trick: ref Trick;
+
+Trickpilespec := Stackspec(
+ "display", # style
+ 4, # maxcards
+ 0, # conceal
+ "trick pile" # title
+);
+
+Handspec := Stackspec(
+ "display",
+ 13,
+ 1,
+ ""
+);
+
+Passonspec := Stackspec(
+ "display",
+ 3,
+ 0,
+ "pass on"
+);
+
+Takenspec := Stackspec(
+ "pile",
+ 52,
+ 0,
+ "tricks"
+);
+
+clienttype(): string
+{
+ return "cards";
+}
+
+init(g: ref Clique, srvmod: Spree): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+
+ allow = load Allow Allow->PATH;
+ if (allow == nil) {
+ sys->print("hearts: cannot load %s: %r\n", Allow->PATH);
+ return "bad module";
+ }
+ allow->init(spree, clique);
+ allow->add(SAY, nil, "say &");
+
+ cardlib = load Cardlib Cardlib->PATH;
+ if (cardlib == nil) {
+ sys->print("hearts: cannot load %s: %r\n", Cardlib->PATH);
+ return "bad module";
+ }
+ cardlib->init(spree, clique);
+
+ tricks = load Tricks Tricks->PATH;
+ if (tricks == nil) {
+ sys->print("hearts: cannot load %s: %r\n", Tricks->PATH);
+ return "bad module";
+ }
+ tricks->init(spree, clique, cardlib);
+
+ deck = clique.newobject(nil, ~0, "stack");
+ cardlib->makecards(deck, (0, 13), 1);
+ cardlib->shuffle(deck);
+ buttons = clique.newobject(nil, ~0, "buttons");
+ scores = clique.newobject(nil, ~0, "scoretable");
+
+ return nil;
+}
+
+join(p: ref Member): string
+{
+ sys->print("%s(%d) joining\n", p.name(), p.id);
+ if (!started && cardlib->nmembers() < MAXPLAYERS) {
+ (nil, err) := cardlib->join(p, -1);
+ if (err == nil) {
+ if (cardlib->nmembers() == MINPLAYERS) {
+ mkbutton("Start", "start");
+ allow->add(START, nil, "start");
+ }
+ } else
+ sys->print("error on join: %s\n", err);
+ }
+ return nil;
+}
+
+leave(p: ref Member)
+{
+ cardlib->leave(p);
+ started == 0;
+ if (cardlib->nmembers() < MINPLAYERS) {
+ buttons.deletechildren((0, len buttons.children));
+ allow->del(START, nil);
+ }
+}
+
+command(p: ref Member, cmd: string): string
+{
+ e := ref Sys->Exception;
+ if (sys->rescue("parse:*", e) == Sys->EXCEPTION) {
+ sys->rescued(Sys->ONCE, nil);
+ return e.name[6:];
+ }
+ (err, tag, toks) := allow->action(p, cmd);
+ if (err != nil)
+ return err;
+ ord := cardlib->order(p);
+ case tag {
+ START =>
+ buttons.deletechildren((0, len buttons.children));
+ allow->del(START, nil);
+ startclique();
+ n := cardlib->nmembers();
+ leader = rand(n);
+ starthand();
+ titles := "";
+ for (i := 0; i < n; i++)
+ titles += cardlib->info(i).p.name() + " ";
+ clique.newobject(scores, ~0, "score").setattr("score", titles, ~0);
+
+ CLICK =>
+ # click stackid index
+ hand := hands[ord];
+ if (int hd tl toks != hand.id)
+ return "can't click there";
+ index := int hd tl tl toks;
+ if (index < 0 || index >= len hand.children)
+ return "index out of range";
+ cardlib->setsel(hands[ord], (index, len hands[ord].children), p);
+ break;
+ err := trick.play(cardlib->order(p), int hd tl toks);
+ if (err != nil)
+ return err;
+
+ turn = next(turn); # clockwise
+ if (turn == leader) { # come full circle
+ winner := trick.winner;
+ inf := cardlib->info(winner);
+ remark(sys->sprint("%s won the trick", inf.p.name()));
+ cardlib->discard(pile, taken[winner], 0);
+ taken[winner].setattr("title",
+ string (len taken[winner].children / cardlib->nmembers()) +
+ " " + "tricks", ~0);
+ o := cardlib->info(winner).obj;
+ trick = nil;
+ s := "";
+ for (i := 0; i < cardlib->nmembers(); i++) {
+ if (i == winner)
+ s += "1 ";
+ else
+ s += "0 ";
+ }
+ clique.newobject(scores, ~0, "score").setattr("score", s, ~0);
+ if (len hands[winner].children > 0) {
+ leader = turn = winner;
+ trick = Trick.new(pile, -1, hands);
+ } else {
+ remark("one round down, some to go");
+ leader = turn = -1; # XXX this round over
+ }
+ }
+ canplay(turn);
+ SAY =>
+ clique.action("say member " + string p.id + ": '" + joinwords(tl toks) + "'", nil, nil, ~0);
+ }
+ return nil;
+}
+
+startclique()
+{
+ cardlib->startclique();
+ entry := clique.newobject(nil, ~0, "widget entry");
+ entry.setattr("command", "say", ~0);
+ cardlib->addlayobj("entry", nil, nil, dTOP|FILLX, entry);
+ cardlib->addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+ cardlib->maketable("arena");
+
+ pile = cardlib->newstack(nil, nil, Trickpilespec);
+ cardlib->addlayobj(nil, "public", nil, dTOP|oLEFT, pile);
+ n := cardlib->nmembers();
+ hands = array[n] of ref Object;
+ taken = array[n] of ref Object;
+ passon = array[n] of ref Object;
+ tt := clique.newobject(nil, ~0, "widget menu");
+ tt.setattr("text", "hello", ~0);
+ for (ml := "one" :: "two" :: "three" :: nil; ml != nil; ml = tl ml) {
+ o := clique.newobject(tt, ~0, "menuentry");
+ o.setattr("text", hd ml, ~0);
+ o.setattr("command", hd ml, ~0);
+ }
+ for (i := 0; i < n; i++) {
+ inf := cardlib->info(i);
+ hands[i] = cardlib->newstack(inf.obj, inf.p, Handspec);
+ taken[i] = cardlib->newstack(inf.obj, inf.p, Takenspec);
+ passon[i] = cardlib->newstack(inf.obj, inf.p, Passonspec);
+ p := "p" + string i;
+ cardlib->addlayframe(p + ".f", p, nil, dLEFT|oLEFT, dTOP);
+ cardlib->addlayobj(nil, p + ".f", inf.layout, dTOP, tt);
+ cardlib->addlayobj(nil, p + ".f", nil, dTOP|oLEFT, hands[i]);
+ cardlib->addlayobj(nil, p, nil, dLEFT|oLEFT, taken[i]);
+ cardlib->addlayobj(nil, p, nil, dLEFT|oLEFT, passon[i]);
+ }
+}
+
+joinwords(v: list of string): string
+{
+ if (v == nil)
+ return nil;
+ s := hd v;
+ for (v = tl v; v != nil; v = tl v)
+ s += " " + hd v;
+ return s;
+}
+
+starthand()
+{
+ cardlib->deal(deck, 13, hands, 0);
+ trick = Trick.new(pile, -1, hands);
+ turn = leader;
+ canplay(turn);
+}
+
+canplay(ord: int)
+{
+ allow->del(CLICK, nil);
+ for (i := 0; i < cardlib->nmembers(); i++) {
+ inf := cardlib->info(i);
+ inf.obj.setattr("status", nil, 1<<inf.p.id);
+ hands[i].setattr("actions", nil, 1<<inf.p.id);
+ }
+ if (ord != -1) {
+ allow->add(CLICK, member(ord), "click %o %d");
+ inf := cardlib->info(ord);
+ inf.obj.setattr("status", "It's your turn to play", 1<<inf.p.id);
+ hands[ord].setattr("actions", "click", 1<<inf.p.id);
+ }
+}
+
+memberobj(p: ref Member): ref Object
+{
+ return cardlib->info(cardlib->order(p)).obj;
+}
+
+member(ord: int): ref Member
+{
+ return cardlib->info(ord).p;
+}
+
+next(i: int): int
+{
+ i++;
+ if (i >= cardlib->nmembers())
+ i = 0;
+ return i;
+}
+
+remark(s: string)
+{
+ clique.action("remark " + s, nil, nil, ~0);
+}
+
+mkbutton(text, cmd: string): ref Object
+{
+ but := clique.newobject(buttons, ~0, "button");
+ but.setattr("text", text, ~0);
+ but.setattr("command", cmd, ~0);
+ return but;
+}
diff --git a/appl/spree/engines/liars.b b/appl/spree/engines/liars.b
new file mode 100644
index 00000000..488e993b
--- /dev/null
+++ b/appl/spree/engines/liars.b
@@ -0,0 +1,490 @@
+implement Engine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+
+MAXPLAYERS: con 32;
+
+clique: ref Clique;
+
+# each member is described by a state machine.
+# a member progresses through the following states:
+#
+# Notplaying
+# istart -> Havedice
+# otherstarts -> Waiting
+# Havedice
+# declare -> Waiting
+# look -> Looking
+# Looking
+# expose -> Looking
+# unexpose -> Looking
+# declare -> Waiting
+# roll -> Rolled
+# Rolled
+# expose -> Rolled
+# unexpose -> Rolled
+# declare -> Waiting
+# Waiting
+# queried -> Queried
+# lost -> Havedice
+# Queried
+# reject,win -> Waiting
+# reject,lose -> Havedice
+# accept -> Havedice
+
+
+plate, cup, space, members: ref Object;
+dice := array[5] of ref Object;
+
+declared: int;
+
+# member states
+Notplaying, Havedice, Looking, Rolled, Waiting, Queried: con iota;
+
+# info on a particular member
+Info: adt {
+ state: int;
+ id: int;
+ member: ref Object;
+ action: ref Object;
+};
+
+info := array[MAXPLAYERS] of ref Info;
+plorder := array[MAXPLAYERS] of int; # map member id to their place around the table
+nplaying := 0;
+nmembers := 0;
+turn := 0;
+
+clienttype(): string
+{
+ return "none";
+}
+
+init(g: ref Clique, srvmod: Spree): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+
+ plate = clique.newobject(nil, ~0, "plate");
+ cup = clique.newobject(plate, 0, "cup");
+ space = clique.newobject(plate, ~0, "space");
+ members = clique.newobject(nil, ~0, "members");
+
+ for (i := 0; i < len dice; i++) {
+ dice[i] = clique.newobject(cup, ~0, "die");
+ dice[i].setattr("number", string rand(6), ~0);
+ }
+
+ return nil;
+}
+
+join(member: ref Member): string
+{
+ check();
+ pmask := 1 << member.id;
+
+ ord := nmembers++;
+ inf := info[ord] = ref Info;
+ inf.state = -1;
+ inf.id = member.id;
+ inf.action = clique.newobject(nil, pmask, "actions" + string member.id);
+ plorder[member.id] = ord;
+ setstate(ord, Notplaying);
+ check();
+ return nil;
+}
+
+leave(member: ref Member)
+{
+ check();
+
+ ord := plorder[member.id];
+ state := info[ord].state;
+ info[ord] = nil;
+ for (i := 0; i < nmembers; i++)
+ if (i != ord)
+ setstate(i, Notplaying);
+ nmembers--;
+ nplaying = 0;
+ clique.action("say member " + string ord + " has left. the clique stops.", nil, nil, ~0);
+ check();
+}
+
+currmember: ref Member;
+currcmd: string;
+command(member: ref Member, cmd: string): string
+{
+ check();
+ e := ref Sys->Exception;
+ if (sys->rescue("parse:*", e) == Sys->EXCEPTION) {
+ sys->rescued(Sys->ONCE, nil);
+ check();
+ currmember = nil;
+ currcmd = nil;
+ return e.name[6:];
+ }
+ currmember = member;
+ currcmd = cmd;
+ (nlines, lines) := sys->tokenize(cmd, "\n");
+ assert(nlines > 0, "unknown command");
+ (n, toks) := sys->tokenize(hd lines, " ");
+ assert(n > 0, "unknown command");
+ pmask := 1 << member.id;
+ ord := plorder[member.id];
+ state := info[ord].state;
+ case hd toks {
+ "say" or
+ "show" or
+ "showme" =>
+ case hd toks {
+ "say" =>
+ clique.action("say member " + string member.id + ": '" + (hd lines)[4:] + "'", nil, nil, ~0);
+ "show" => # show [memberid]
+ p: ref Member = nil;
+ if (n == 2) {
+ memberid := int hd tl toks;
+ p = clique.member(memberid);
+ assert(p != nil, "bad memberid");
+ }
+ clique.show(p);
+ "showme" =>
+ clique.show(member);
+ }
+ currmember = nil;
+ currcmd = nil;
+ return nil;
+ }
+ case state {
+ Notplaying =>
+ case hd toks {
+ "start" =>
+ assert(nplaying == 0, "clique is in progress");
+ assert(nmembers > 1, "need at least two members");
+ newinfo := array[len info] of ref Info;
+ members.deletechildren((0, len members.children));
+ j := 0;
+ for (i := 0; i < len info; i++)
+ if (info[i] != nil)
+ newinfo[j++] = info[i];
+ info = newinfo;
+ nplaying = nmembers;
+ for (i = 0; i < nplaying; i++) {
+ info[i].member = clique.newobject(members, ~0, nil);
+ info[i].member.setattr("id", string info[i].id, ~0);
+ }
+ turn = rand(nplaying);
+ start();
+ * =>
+ assert(0, "you are not playing");
+ }
+ Havedice =>
+ case hd toks {
+ "declare" =>
+ # declare hand
+ declare(ord, tl toks);
+ "look" =>
+ cup.setattr("raised", "1", ~0);
+ cup.setvisibility(pmask);
+ setstate(ord, Looking);
+ * =>
+ assert(0, "bad command");
+ }
+ Looking =>
+ case hd toks {
+ "expose" or
+ "unexpose" =>
+ expose(n, toks);
+ "declare" =>
+ declare(ord, tl toks);
+ "roll" =>
+ # roll index...
+ # XXX should be able to roll in the open too
+ for (toks = tl toks; toks != nil; toks = tl toks) {
+ index := int hd toks;
+ checkrange((index, index), cup);
+ cup.children[index].setattr("number", string rand(6), ~0);
+ }
+ setstate(ord, Rolled);
+ * =>
+ assert(0, "bad command");
+ }
+ Rolled =>
+ case hd toks {
+ "expose" or
+ "unexpose" =>
+ expose(n, toks);
+ "declare" =>
+ declare(ord, tl toks);
+ * =>
+ assert(0, "bad command");
+ }
+ Waiting =>
+ assert(0, "not your turn");
+ Queried =>
+ case hd toks {
+ "reject" =>
+ # lift the cup!
+ cup.transfer((0, len cup.children), space, len space.children);
+ assert(len space.children == 5, "lost a die somewhere!");
+ dvals := array[5] of int;
+ for (i := 0; i < 5; i++)
+ dvals[i] = int space.children[i].getattr("number");
+ actval := value(dvals);
+ if (actval >= declared) {
+ # declaration was correct; rejector loses
+ clique.action("say member " + string ord + " loses.", nil, nil, ~0);
+ turn = ord;
+ start();
+ } else {
+ # liar caught out. rejector wins.
+ clique.action("say member " + string turn + " was lying...", nil, nil, ~0);
+ start();
+ }
+ "accept" =>
+ # dice accepted, turn moves on
+ # XXX should allow for anticlockwise play
+ newturn := (turn + 1) % nplaying;
+ plate.setattr("owner", string newturn, ~0);
+ setstate(ord, Havedice);
+ setstate(turn, Waiting);
+ }
+ }
+ check();
+ currmember = nil;
+ currcmd = nil;
+ return nil;
+}
+
+expose(n: int, toks: list of string)
+{
+ # (un)expose index
+ assert(n == 2, Eusage);
+ (src, dest) := (cup, space);
+ if (hd toks == "unexpose")
+ (src, dest) = (space, cup);
+ index := int hd tl toks;
+ checkrange((index, index+1), cup);
+ src.transfer((index, index+1), dest, len dest.children);
+}
+
+start()
+{
+ clique.action("start", nil, nil, ~0);
+ space.transfer((0, len space.children), cup, len cup.children);
+ cup.setvisibility(0);
+ for (i := 0; i < len dice; i++)
+ dice[i].setattr("number", string rand(6), ~0);
+
+ plate.setattr("owner", string turn, ~0);
+ for (i = 0; i < nplaying; i++) {
+ if (i == turn)
+ setstate(i, Havedice);
+ else
+ setstate(i, Waiting);
+ }
+ declared = 0;
+}
+
+declare(ord: int, toks: list of string)
+{
+ cup.setvisibility(0);
+ assert(len toks == 1 && len hd toks == 5, "bad declaration");
+ d := hd toks;
+ v := array[5] of {* => 0};
+ for (i := 0; i < 5; i++) {
+ v[i] = (hd toks)[i] - '0';
+ assert(v[i] >= 0 && v[i] <= 5, "bad declaration");
+ }
+ newval := value(v);
+ assert(newval > declared, "declaration not high enough");
+ declared = newval;
+
+ setstate(turn, Waiting);
+ setstate((turn + 1) % nplaying, Queried);
+}
+
+# check that range is valid for object's children
+checkrange(r: Range, o: ref Object)
+{
+ assert(r.start >= 0 && r.start < len o.children &&
+ r.end >= r.start && r.end >= 0 &&
+ r.end <= len o.children,
+ "index out of range");
+}
+
+setstate(ord: int, state: int)
+{
+ poss: string;
+ case state {
+ Notplaying =>
+ poss = "start";
+ Havedice =>
+ poss = "declare look";
+ Looking =>
+ poss = "expose unexpose declare roll";
+ Rolled =>
+ poss = "expose unexpose declare";
+ Waiting =>
+ poss = "";
+ Queried =>
+ poss = "accept reject";
+ * =>
+ sys->print("liarclique: unknown state %d, member %d\n", state, ord);
+ sys->raise("panic");
+ }
+ info[ord].action.setattr("actions", poss, 1<<info[ord].id);
+ info[ord].state = state;
+}
+
+obj(ext: int): ref Object
+{
+ assert((o := currmember.obj(ext)) != nil, "bad object");
+ return o;
+}
+
+Eusage: con "bad command usage";
+
+assert(b: int, err: string)
+{
+ if (b == 0) {
+ sys->print("cardclique: error '%s' on %s", err, currcmd);
+ sys->raise("parse:" + err);
+ }
+}
+
+checkobj(o: ref Object, what: string)
+{
+ if (o != nil && o.id == -1) {
+ clique.show(currmember);
+ sys->print("object %d has been deleted unexpectedly (%s)\n", o.id, what);
+ sys->raise("panic");
+ }
+}
+
+check()
+{
+}
+
+NOTHING, PAIR, TWOPAIRS, THREES, LOWSTRAIGHT,
+FULLHOUSE, HIGHSTRAIGHT, FOURS, FIVES: con iota;
+
+what := array[] of {
+NOTHING => "nothing",
+PAIR => "pair",
+TWOPAIRS => "twopairs",
+THREES => "threes",
+LOWSTRAIGHT => "lowstraight",
+FULLHOUSE => "fullhouse",
+HIGHSTRAIGHT => "highstraight",
+FOURS => "fours",
+FIVES => "fives"
+};
+
+same(dice: array of int): int
+{
+ x := dice[0];
+ for (i := 0; i < len dice; i++)
+ if (dice[i] != x)
+ return 0;
+ return 1;
+}
+
+val(hi, lo: int): int
+{
+ return hi * 100000 + lo;
+}
+
+D: con 10;
+
+value(dice: array of int): int
+{
+ mergesort(dice, array[5] of int);
+
+ for (i := 0; i < 5; i++)
+ sys->print("%d ", dice[i]);
+ sys->print("\n");
+
+ # five of a kind
+ x := dice[0];
+ if (same(dice))
+ return val(FIVES, dice[0]);
+
+ # four of a kind
+ if (same(dice[1:]))
+ return val(FOURS, dice[0] + dice[1]*D);
+ if (same(dice[0:4]))
+ return val(FOURS, dice[4] + dice[0]*D);
+
+ # high straight
+ if (dice[0] == 1 && dice[1] == 2 && dice[2] == 3 &&
+ dice[3] == 4 && dice[4] == 5)
+ return val(HIGHSTRAIGHT, 0);
+
+ # full house
+ if (same(dice[0:3]) && same(dice[3:5]))
+ return val(FULLHOUSE, dice[0]*D + dice[4]);
+ if (same(dice[0:2]) && same(dice[2:5]))
+ return val(FULLHOUSE, dice[4]*D + dice[0]);
+
+ # low straight
+ if (dice[0] == 0 && dice[1] == 1 && dice[2] == 2 &&
+ dice[3] == 3 && dice[4] == 4)
+ return val(LOWSTRAIGHT, 0);
+ # three of a kind
+ if (same(dice[0:3]))
+ return val(THREES, dice[3] + dice[4]*D + dice[0]*D*D);
+ if (same(dice[1:4]))
+ return val(THREES, dice[0] + dice[4]*D + dice[1]*D*D);
+ if (same(dice[2:5]))
+ return val(THREES, dice[0] + dice[1]*D + dice[2]*D*D);
+
+ for (i = 0; i < 4; i++)
+ if (same(dice[i:i+2]))
+ break;
+ case i {
+ 4 =>
+ return val(NOTHING, dice[0] + dice[1]*D + dice[2]*D*D +
+ dice[3]*D*D*D + dice[4]*D*D*D*D);
+ 3 =>
+ return val(PAIR, dice[0] + dice[1]*D + dice[2]*D*D + dice[3]*D*D*D);
+ 2 =>
+ return val(PAIR, dice[0] + dice[1]*D + dice[4]*D*D + dice[2]*D*D*D);
+ }
+ h := array[5] of int;
+ h[0:] = dice;
+ if (i == 1)
+ (h[0], h[2]) = (h[2], h[0]);
+ # pair is in first two dice
+ if (same(h[2:4]))
+ return val(TWOPAIRS, h[4] + h[2]*D + h[0]*D*D);
+ if (same(h[3:5]))
+ return val(TWOPAIRS, h[2] + h[0]*D + h[4]*D*D);
+ return val(PAIR, dice[2] + dice[3]*D + dice[4]*D*D + dice[0]*D*D*D);
+}
+
+mergesort(a, b: array of int)
+{
+ r := len a;
+ if (r > 1) {
+ m := (r-1)/2 + 1;
+ mergesort(a[0:m], b[0:m]);
+ mergesort(a[m:], b[m:]);
+ b[0:] = a;
+ for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+ if (b[i] > b[j])
+ a[k] = b[j++];
+ else
+ a[k] = b[i++];
+ }
+ if (i < m)
+ a[k:] = b[i:m];
+ else if (j < r)
+ a[k:] = b[j:r];
+ }
+}
diff --git a/appl/spree/engines/liars.y b/appl/spree/engines/liars.y
new file mode 100644
index 00000000..1e191076
--- /dev/null
+++ b/appl/spree/engines/liars.y
@@ -0,0 +1,132 @@
+%{
+
+
+YYSTYPE: adt {
+};
+
+YYLEX: adt {
+ lval: YYSTYPE;
+ lex: fn(l: self ref YYLEX): int;
+ error: fn(l: self ref YYLEX, err: string);
+ toks: list of string;
+};
+%}
+
+%module Sh {
+ # module definition is in shell.m
+}
+%token A ALL AND BROKEN FIVE FOUR FUCK FULL HIGH
+%token HOUSE KIND LOW NOTHING OF ON PAIR PAIRS STRAIGHT THREE TWO VALUE
+
+%start phrase
+%%
+phrase: nothing
+ | pair
+ | twopairs
+ | threes
+ | lowstraight
+ | fullhouse
+ | highstraight
+ | fours
+ | fives
+
+pair: PAIR
+ | PAIR ofsomething ',' extras
+
+nothing: NOTHING
+ | BROKEN STRAIGHT
+ | FUCK ALL
+
+twopairs: TWO PAIRS moretuppers
+ | TWO VALUE optcomma TWO VALUE and_a VALUE
+ | PAIR OF VALUE ',' PAIR OF VALUE and_a VALUE
+
+moretuppers:
+ | ',' VALUE ',' VALUE and_a VALUE
+
+threes: THREE OF A KIND extras
+ | THREE VALUE extras
+
+lowstraight: LOW STRAIGHT
+
+fullhouse: FULL HOUSE
+ | FULL HOUSE optcomma VALUE
+ | FULL HOUSE optcomma VALUE ON VALUE
+ | FULL HOUSE optcomma VALUE HIGH
+
+highstraight: HIGH STRAIGHT
+
+fours: FOUR OF A KIND extras
+ | FOUR VALUE extras
+
+fives: FIVE OF A KIND
+ | FIVE VALUE
+and_a: # null
+ | AND A
+optcomma:
+ | ','
+extras: VALUE
+ | extras VALUE
+%%
+
+Tok: adt {
+ s: string;
+ tok: int;
+ val: int;
+};
+
+known := array of {
+Tok("an", A, -1),
+Tok("a", A, -1),
+Tok("all", ALL, -1),
+Tok("and", AND, -1),
+Tok("broken", BROKEN, -1),
+Tok(",", ',', -1),
+Tok("five", FIVE, -1),
+Tok("5", FIVE, -1),
+Tok("four", FOUR, -1),
+Tok("4", FOUR, -1),
+Tok("fuck", FUCK, -1),
+Tok("full", FULL, -1),
+Tok("high", HIGH, -1),
+Tok("house", HOUSE, -1),
+Tok("kind", KIND, -1),
+Tok("low", LOW, -1),
+Tok("nothing", NOTHING, -1),
+Tok("of", OF, -1),
+Tok("on", ON, -1),
+Tok("pair", PAIR, -1),
+Tok("pairs", PAIRS, -1),
+Tok("straight", STRAIGHT, -1),
+Tok("three", THREE, -1),
+Tok("3", THREE, -1),
+Tok("two", TWO, -1),
+Tok("2", TWO, -1),
+
+Tok("A", VALUE, 5),
+Tok("K", VALUE, 4),
+Tok("Q", VALUE, 3),
+Tok("J", VALUE, 2),
+Tok("10", VALUE, 1),
+Tok("9", VALUE, 0),
+
+Tok("ace"
+};
+
+YYLEX.lex(l: self ref YYLEX): int
+{
+ if (l.toks == nil)
+ return -1;
+ t := hd l.toks;
+ for (i := 0; i < len known; i++) {
+ if (known[i].t0 == t)
+ return known[i].t1;
+
+ case hd l.toks {
+
+
+%token A ALL AND BROKEN FIVE FOUR FUCK FULL HIGH
+%token HOUSE KIND LOW NOTHING OF ON PAIR PAIRS STRAIGHT THREE TWO VALUE
+%token END
+
+} \ No newline at end of file
diff --git a/appl/spree/engines/lobby.b b/appl/spree/engines/lobby.b
new file mode 100644
index 00000000..119922e2
--- /dev/null
+++ b/appl/spree/engines/lobby.b
@@ -0,0 +1,389 @@
+implement Engine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ sets: Sets;
+ Set, set, A, B, All, None: import sets;
+include "../spree.m";
+ spree: Spree;
+ archives: Archives;
+ Archive: import Archives;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "readdir.m";
+ readdir: Readdir;
+
+# what the lobby provides:
+# a list of cliques it's started
+# name of clique
+# current members
+# list of members inside the lobby.
+# name
+# invites
+# how does a gather engine know who's been invited?
+# as the lobby's the only place with the knowledge of who's around to invite.
+# could allow lobby to communicate with the cliques it's started...
+# but clique also needs to communicate with the lobby
+# (e.g. to say clique has started, no more invites necessary or allowed)
+#
+# list of available engines
+# title
+# clienttype(s?)
+#
+# understands commands:
+# chat message
+# invite
+# new name params
+#
+# question: how do we know about archives?
+# answer: maybe we don't... could have another module
+# that does, or maybe an option to gather ("gather unarchive"?)
+#
+# the one that's started the clique is always invited.
+# start clique.
+# clique says to parent "invite x, y and z" (perhaps they were in the archive)
+# how should we deal with recursive invocation?
+# could queue up requests to other clique engines,
+# and deliver them after the current request has been processed.
+# no return available (one way channel) but maybe that's good,
+# as if sometime in the future engines do run in parallel, we will
+# need to avoid deadlock.
+# Clique.notify(clique: self ref Clique, cliqueid: int, note: string);
+# when a request has been completed, we run notify requests
+# for all the cliques that have been notified, and repeat
+# until no more. (could keep a count to check for infinite loop).
+# don't allow communication between unrelated cliques.
+
+clique: ref Clique;
+
+members: ref Object;
+sessions: ref Object;
+available: ref Object;
+archiveobj: ref Object;
+
+ARCHIVEDIR: con "./archive";
+
+init(srvmod: Spree, g: ref Clique, nil: list of string): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+ sets = load Sets Sets->PATH;
+ if (sets == nil) {
+ sys->print("lobby: cannot load %s: %r\n", Sets->PATH);
+ return "bad module";
+ }
+ readdir = load Readdir Readdir->PATH;
+ if (readdir == nil) {
+ sys->print("lobby: cannot load %s: %r\n", Readdir->PATH);
+ return "bad module";
+ }
+ archives = load Archives Archives->PATH;
+ if (archives == nil) {
+ sys->print("lobby: cannot load %s: %r\n", Archives->PATH);
+ return "bad module";
+ }
+ archives->init(srvmod);
+ members = clique.newobject(nil, All, "members");
+ sessions = clique.newobject(nil, All, "sessions");
+ available = clique.newobject(nil, All, "available");
+ o := clique.newobject(available, All, "sessiontype");
+ o.setattr("name", "freecell", All);
+ o.setattr("title", "Freecell", All);
+ o.setattr("clienttype", "cards", All);
+ o.setattr("start", "gather 1 freecell", All);
+
+ o = clique.newobject(available, All, "sessiontype");
+ o.setattr("title", "Lobby", All);
+ o.setattr("name", "lobby", All);
+ o.setattr("clienttype", "lobby", All);
+ o.setattr("start", "lobby", All);
+
+ o = clique.newobject(available, All, "sessiontype");
+ o.setattr("title", "Spit", All);
+ o.setattr("name", "spit", All);
+ o.setattr("clienttype", "cards", All);
+ o.setattr("start", "gather 2 spit", All);
+
+ o = clique.newobject(available, All, "sessiontype");
+ o.setattr("title", "Canfield", All);
+ o.setattr("name", "canfield", All);
+ o.setattr("clienttype", "cards", All);
+ o.setattr("start", "gather 1 canfield", All);
+
+ o = clique.newobject(available, All, "sessiontype");
+ o.setattr("title", "Afghan", All);
+ o.setattr("name", "afghan", All);
+ o.setattr("clienttype", "cards", All);
+ o.setattr("start", "gather 1 afghan", All);
+
+ o = clique.newobject(available, All, "sessiontype");
+ o.setattr("title", "Spider", All);
+ o.setattr("name", "spider", All);
+ o.setattr("clienttype", "cards", All);
+ o.setattr("start", "gather 1 spider", All);
+
+ o = clique.newobject(available, All, "sessiontype");
+ o.setattr("title", "Racing Demon", All);
+ o.setattr("name", "racingdemon", All);
+ o.setattr("clienttype", "cards", All);
+ o.setattr("start", "gather 3 racingdemon", All);
+
+ o = clique.newobject(available, All, "sessiontype");
+ o.setattr("title", "Othello", All);
+ o.setattr("name", "othello", All);
+ o.setattr("clienttype", "othello", All);
+ o.setattr("start", "gather 2 othello", All);
+
+ o = clique.newobject(available, All, "sessiontype");
+ o.setattr("title", "Whist", All);
+ o.setattr("name", "whist", All);
+ o.setattr("clienttype", "whist", All);
+ o.setattr("start", "gather 4 whist", All);
+
+ getarchives();
+
+ clique.start();
+
+ return nil;
+}
+
+join(p: ref Member, cmd: string, nil: int): string
+{
+ sys->print("%s joins '%s'\n", p.name, cmd);
+ clique.notify(clique.parentid, "join " + p.name);
+ s := None.add(p.id);
+ clique.action("clienttype lobby", nil, nil, s);
+ clique.breakmsg(s);
+ clique.action("name " + p.name, nil, nil, s);
+ o := clique.newobject(members, All, "member");
+ o.setattr("name", p.name, All);
+ return nil;
+}
+
+leave(p: ref Member): int
+{
+ clique.notify(clique.parentid, "leave " + p.name);
+ deletename(members, p.name, "member");
+ sys->print("%s leaves\n", p.name);
+ return 1;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+ return nil;
+}
+
+command(p: ref Member, cmd: string): string
+{
+ sys->print("%s: '%s'\n", p.name, cmd);
+ (n, toks) := sys->tokenize(cmd, " \n");
+ if (n == 0)
+ return "bad command";
+ case hd toks {
+ "kick" =>
+ getarchives();
+ return nil;
+ "chat" =>
+ clique.action("chat " + p.name + " " + concat(tl toks), nil, nil, All);
+ return nil;
+ "start" =>
+ # start engine [params]
+ if (n >= 2) {
+ (gid, fname, err) := clique.new(
+ ref Archive(tl toks, nil, nil, nil),
+ p.name);
+ if (gid == -1)
+ return err;
+ s := addname(sessions, string gid, "session");
+ s.setattr("title", concat(tl toks), All);
+ s.setattr("filename", fname, All);
+ s.setattr("cliqueid", string gid, None);
+ s.setattr("owner", p.name, All);
+ return nil;
+ }
+ return "bad start params";
+ "invite" or
+ "uninvite"=>
+ # invite sessionid name
+ if (n == 3) {
+ (what, sessionid, name) := (hd toks, int hd tl toks, hd tl tl toks);
+ if ((s := p.obj(sessionid)) == nil)
+ return "bad object id";
+ if (s.objtype != "session")
+ return "bad session type " + s.objtype;
+ if (s.getattr("owner") != p.name)
+ return "permission denied";
+ clique.notify(int s.getattr("cliqueid"), what + " " + name);
+ if (hd toks == "invite")
+ addname(s, name, "invite");
+ else
+ deletename(s, name, "invite");
+ return nil;
+ }
+ return "bad invite params";
+ "unarchive" =>
+ # unarchive object
+ if (n == 2) {
+ o := p.obj(int hd tl toks);
+ if (o == nil || o.objtype != "archive")
+ return "bad archive object";
+ # archive object contains:
+ # name name of clique
+ # members members of the clique
+ # file filename of archive
+
+ aname := o.getattr("file");
+ (archive, err) := archives->read(aname);
+ if (archive == nil)
+ return sys->sprint("cannot load archive: %s", err);
+ for (i := 0; i < len archive.members; i++)
+ if (p.name == archive.members[i])
+ break;
+ if (i == len archive.members)
+ return "you did not participate in that session";
+ (gid, fname, err2) := clique.new(archive, p.name);
+ if (gid == -1)
+ return err2;
+ s := addname(sessions, string gid, "session");
+ s.setattr("title", concat(archive.argv), All);
+ s.setattr("filename", fname, All);
+ s.setattr("cliqueid", string gid, None);
+ s.setattr("owner", p.name, All);
+
+ o.delete();
+ (ok, d) := sys->stat(aname);
+ if (ok != -1) {
+ d.name += ".old";
+ sys->wstat(aname, d);
+ }
+ # XXX delete old archive file?
+ return nil;
+ }
+ return "bad unarchive params";
+ * =>
+ return "bad command";
+ }
+}
+
+notify(srcid: int, note: string)
+{
+ sys->print("lobby: note from %d: %s\n", srcid, note);
+ s := findname(sessions, string srcid);
+ if (s == nil) {
+ sys->print("cannot find srcid %d\n", srcid);
+ return;
+ }
+ if (note == nil) {
+ s.delete();
+ return;
+ }
+ if (srcid == clique.parentid)
+ return;
+ (n, toks) := sys->tokenize(note, " ");
+ case hd toks {
+ "join" =>
+ p := addname(s, hd tl toks, "member");
+ "leave" =>
+ deletename(s, hd tl toks, "member");
+ "invite" =>
+ addname(s, hd tl toks, "invite");
+ "uninvite" =>
+ deletename(s, hd tl toks, "invite");
+ "title" =>
+ s.setattr("title", concat(tl toks), All);
+ "archived" =>
+ # archived filename
+ arch := clique.newobject(archiveobj, All, "archive");
+ arch.setattr("name", s.getattr("title"), All);
+ pnames := "";
+ for (i := 0; i < len s.children; i++)
+ if (s.children[i].objtype == "member")
+ pnames += " " + s.children[i].getattr("name");
+ if (pnames != nil)
+ pnames = pnames[1:];
+ arch.setattr("members", pnames, All);
+ arch.setattr("file", hd tl toks, None);
+ * =>
+ sys->print("unknown note from %d: %s\n", srcid, note);
+ }
+}
+
+addname(o: ref Object, name: string, otype: string): ref Object
+{
+ x := clique.newobject(o, All, otype);
+ x.setattr("name", name, All);
+ return x;
+}
+
+findname(o: ref Object, name: string): ref Object
+{
+ c := o.children;
+ for (i := 0; i < len c; i++)
+ if (c[i].getattr("name") == name)
+ return c[i];
+ return nil;
+}
+
+deletename(o: ref Object, name: string, objtype: string)
+{
+ c := o.children;
+ for (i := 0; i < len c; i++)
+ if (c[i].objtype == objtype && c[i].getattr("name") == name) {
+ o.deletechildren((i, i+1));
+ break;
+ }
+}
+
+getarchives()
+{
+ if (archiveobj == nil)
+ archiveobj = clique.newobject(nil, All, "archives");
+ else
+ archiveobj.deletechildren((0, len archiveobj.children));
+ for (names := spree->archivenames(); names != nil; names = tl names) {
+ fname := hd names;
+ (a, err) := archives->readheader(fname);
+ if (a == nil) {
+ sys->print("lobby: cannot read archive header on %s: %s\n", fname, err);
+ continue;
+ }
+ title := "";
+ for (inf := a.info; inf != nil; inf = tl inf) {
+ if ((hd inf).t0 == "title") {
+ title = (hd inf).t1;
+ break;
+ }
+ }
+ if (title == nil)
+ title = concat(a.argv);
+ arch := clique.newobject(archiveobj, All, "archive");
+ arch.setattr("name", title, All);
+ arch.setattr("members", concatarray(a.members), All);
+ arch.setattr("file", fname, None);
+ j := 0;
+ for (info := a.info; info != nil; info = tl info)
+ arch.setattr("info" + string j++, (hd info).t0 + " " + (hd info).t1, All);
+ }
+}
+
+concat(l: list of string): string
+{
+ if (l == nil)
+ return nil;
+ s := hd l;
+ for (l = tl l; l != nil; l = tl l)
+ s += " " + hd l;
+ return s;
+}
+
+concatarray(a: array of string): string
+{
+ if (len a == 0)
+ return nil;
+ s := a[0];
+ for (i := 1; i < len a; i++)
+ s += " " + a[i];
+ return s;
+}
diff --git a/appl/spree/engines/othello.b b/appl/spree/engines/othello.b
new file mode 100644
index 00000000..2f36c47b
--- /dev/null
+++ b/appl/spree/engines/othello.b
@@ -0,0 +1,242 @@
+implement Gatherengine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ All, None: import Sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member: import spree;
+include "objstore.m";
+ objstore: Objstore;
+include "../gather.m";
+
+clique: ref Clique;
+
+Black, White, Nocolour: con iota; # first two must be 0 and 1.
+N: con 8;
+
+boardobj: ref Object;
+board: array of array of int;
+pieces: array of int;
+turn := Nocolour;
+members := array[2] of ref Member; # member ids of those playing
+
+Point: adt {
+ x, y: int;
+ add: fn(p: self Point, p1: Point): Point;
+ inboard: fn(p: self Point): int;
+};
+
+clienttype(): string
+{
+ return "othello";
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+
+ objstore = load Objstore Objstore->PATH;
+ if (objstore == nil) {
+ sys->print("othello: cannot load %s: %r", Objstore->PATH);
+ return "bad module";
+ }
+ objstore->init(srvmod, g);
+
+ return nil;
+}
+
+maxmembers(): int
+{
+ return 2;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+ return nil;
+}
+
+propose(members: array of string): string
+{
+ if (len members != 2)
+ return "need exactly two members";
+ return nil;
+}
+
+archive()
+{
+ objstore->setname(boardobj, "board");
+}
+
+start(pl: array of ref Member, archived: int)
+{
+ members = pl;
+ board = array[N] of {* => array[N] of {* => Nocolour}};
+ pieces = array[2] of {* => 0};
+ if (archived) {
+ objstore->unarchive();
+ boardobj = objstore->get("board");
+ for (i := 0; i < N; i++) {
+ for (j := 0; j < N; j++) {
+ a := boardobj.getattr(pt2attr((j, i)));
+ if (a != nil) {
+ piece := int a;
+ board[j][i] = piece;
+ if (piece != Nocolour)
+ pieces[piece]++;
+ }
+ }
+ }
+ turn = int boardobj.getattr("turn");
+ } else {
+ boardobj = clique.newobject(nil, All, nil);
+ boardobj.setattr("members", string members[Black].name + " " + string members[White].name, All);
+ for (ps := (Black, (3, 3)) :: (Black, (4, 4)) :: (White, (3, 4)) :: (White, Point(4, 3)) :: nil;
+ ps != nil;
+ ps = tl ps) {
+ (colour, p) := hd ps;
+ setpiece(colour, p);
+ }
+ turn = Black;
+ boardobj.setattr("turn", string Black, All);
+ }
+}
+
+cliqueover()
+{
+ turn = Nocolour;
+ boardobj.setattr("winner", string winner(), All);
+ boardobj.setattr("turn", string turn, All);
+}
+
+command(member: ref Member, cmd: string): string
+{
+ {
+ (n, toks) := sys->tokenize(cmd, " \n");
+ assert(n > 0, "unknown command");
+
+ case hd toks {
+ "move" =>
+ assert(n == 3, "bad command usage");
+ assert(turn != Nocolour, "clique has finished");
+ assert(member == members[White] || member == members[Black], "you are not playing");
+ assert(member == members[turn], "it is not your turn");
+ p := Point(int hd tl toks, int hd tl tl toks);
+ assert(p.x >= 0 && p.x < N && p.y >= 0 && p.y < N, "invalid move position");
+ assert(board[p.x][p.y] == Nocolour, "position is already occupied");
+ assert(newmove(turn, p, 1), "cannot move there");
+
+ turn = reverse(turn);
+ if (!canplay()) {
+ turn = reverse(turn);
+ if (!canplay())
+ cliqueover();
+ }
+ boardobj.setattr("turn", string turn, All);
+ return nil;
+ }
+ sys->print("othello: unknown client command '%s'\n", hd toks);
+ return "who knows";
+ } exception e {
+ "parse:*" =>
+ return e[6:];
+ }
+}
+
+Directions := array[] of {Point(0, 1), (1, 1), (1, 0), (1, -1), (0, -1), (-1, -1), (-1, 0), (-1, 1)};
+
+setpiece(colour: int, p: Point)
+{
+ v := board[p.x][p.y];
+ if (v != Nocolour)
+ pieces[v]--;
+ board[p.x][p.y] = colour;
+ pieces[colour]++;
+ boardobj.setattr(pt2attr(p), string colour, All);
+}
+
+pt2attr(pt: Point): string
+{
+ s := " ";
+ s[0] = pt.x + 'a';
+ s[1] = pt.y + 'a';
+ return s;
+}
+
+# member colour has tried to place a piece at mp.
+# return -1 if it's an illegal move, 0 otherwise.
+# (in which case appropriate updates are sent out all round).
+# if update is 0, just check for the move's validity
+# (no change to the board, no updates sent)
+newmove(colour: int, mp: Point, update: int): int
+{
+ totchanged := 0;
+ for (i := 0; i < len Directions; i++) {
+ d := Directions[i];
+ n := 0;
+ for (p := mp.add(d); p.inboard(); p = p.add(d)) {
+ n++;
+ if (board[p.x][p.y] == colour || board[p.x][p.y] == Nocolour)
+ break;
+ }
+ if (p.inboard() && board[p.x][p.y] == colour && n > 1) {
+ if (!update)
+ return 1;
+ totchanged += n - 1;
+ for (p = mp.add(d); --n > 0; p = p.add(d))
+ setpiece(reverse(board[p.x][p.y]), p);
+ }
+ }
+ if (totchanged > 0) {
+ setpiece(colour, mp);
+ return 1;
+ }
+ return 0;
+}
+
+# who has most pieces?
+winner(): int
+{
+ if (pieces[White] > pieces[Black])
+ return White;
+ else if (pieces[Black] > pieces[White])
+ return Black;
+ return Nocolour;
+}
+
+# is there any possible legal move?
+canplay(): int
+{
+ for (y := 0; y < N; y++)
+ for (x := 0; x < N; x++)
+ if (board[x][y] == Nocolour && newmove(turn, (x, y), 0))
+ return 1;
+ return 0;
+}
+
+reverse(colour: int): int
+{
+ if (colour == Nocolour)
+ return Nocolour;
+ return !colour;
+}
+
+Point.add(p: self Point, p1: Point): Point
+{
+ return (p.x + p1.x, p.y + p1.y);
+}
+
+Point.inboard(p: self Point): int
+{
+ return p.x >= 0 && p.x < N && p.y >= 0 && p.y < N;
+}
+
+assert(b: int, err: string)
+{
+ if (b == 0)
+ raise "parse:" + err;
+}
diff --git a/appl/spree/engines/racingdemon.b b/appl/spree/engines/racingdemon.b
new file mode 100644
index 00000000..f839e29a
--- /dev/null
+++ b/appl/spree/engines/racingdemon.b
@@ -0,0 +1,464 @@
+implement Gatherengine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ sets: Sets;
+ Set, set, A, B, All, None: import sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+ allow: Allow;
+include "cardlib.m";
+ cardlib: Cardlib;
+ Selection, Cmember, Card: import cardlib;
+ dTOP, dLEFT, oDOWN, EXPAND, FILLX, FILLY, aCENTRELEFT, Stackspec: import Cardlib;
+include "../gather.m";
+
+clique: ref Clique;
+
+CLICK, SAY, SHOW: con iota;
+KING: con 12;
+NACES: con 7; # number of ace piles to fit across the board.
+
+Dmember: adt {
+ pile,
+ spare1,
+ spare2: ref Object;
+ open: array of ref Object; # [4]
+ acepiles: array of ref Object;
+};
+scores: array of int;
+scorelabel: ref Object;
+
+dmembers: array of ref Dmember;
+
+Openspec := Stackspec(
+ "display", # style
+ 4, # maxcards
+ 0, # conceal
+ "" # title
+);
+
+Pilespec := Stackspec(
+ "pile", # style
+ 13, # maxcards
+ 0, # conceal
+ "pile" # title
+);
+
+Untitledpilespec := Stackspec(
+ "pile", # style
+ 13, # maxcards
+ 0, # conceal
+ "" # title
+);
+
+clienttype(): string
+{
+ return "cards";
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+
+ allow = load Allow Allow->PATH;
+ if (allow == nil) {
+ sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+ return "bad module";
+ }
+ allow->init(spree, clique);
+
+ sets = load Sets Sets->PATH;
+ if (sets == nil) {
+ sys->print("whist: cannot load %s: %r\n", Sets->PATH);
+ return "bad module";
+ }
+ sets->init();
+
+ cardlib = load Cardlib Cardlib->PATH;
+ if (cardlib == nil) {
+ sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+ return "bad module";
+ }
+ cardlib->init(spree, clique);
+
+ return nil;
+}
+
+maxmembers(): int
+{
+ return 100;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+ return nil;
+}
+
+propose(members: array of string): string
+{
+ if (len members < 3)
+ return "need at least 3 members";
+ return nil;
+}
+
+archive()
+{
+ archiveobj := cardlib->archive();
+ allow->archive(archiveobj);
+ for (i := 0; i < len dmembers; i++) {
+ dp := dmembers[i];
+ s := "d" + string i + "_";
+ cardlib->setarchivename(dp.spare1, s + "spare1");
+ cardlib->setarchivename(dp.spare2, s + "spare2");
+ cardlib->setarchivename(dp.pile, s + "pile");
+ cardlib->archivearray(dp.open, s + "open");
+ cardlib->archivearray(dp.acepiles, s + "acepiles");
+ }
+ cardlib->setarchivename(scorelabel, "scorelabel");
+ s := "";
+ for (i = 0; i < len scores; i++)
+ s += " " + string scores[i];
+ archiveobj.setattr("scores", s, None);
+
+}
+
+start(members: array of ref Member, archived: int)
+{
+ if (archived) {
+ archiveobj := cardlib->unarchive();
+ allow->unarchive(archiveobj);
+ dmembers = array[len members] of ref Dmember;
+ for (i := 0; i < len dmembers; i++) {
+ dp := dmembers[i] = ref Dmember;
+ s := "d" + string i + "_";
+ dp.spare1 = cardlib->getarchiveobj(s + "spare1");
+ dp.spare2 = cardlib->getarchiveobj(s + "spare2");
+ dp.pile = cardlib->getarchiveobj(s + "pile");
+ dp.open = cardlib->getarchivearray(s + "open");
+ dp.acepiles = cardlib->getarchivearray(s + "acepiles");
+ }
+ scorelabel = cardlib->getarchiveobj("scorelabel");
+ s := archiveobj.getattr("scores");
+ (n, toks) := sys->tokenize(s, " ");
+ scores = array[len members] of int;
+ for (i = 0; toks != nil; toks = tl toks)
+ scores[i++] = int hd toks;
+ } else {
+ pset := None;
+ for (i := 0; i < len members; i++) {
+ p := members[i];
+ Cmember.join(p, i);
+ pset = pset.add(p.id);
+ allow->add(CLICK, p, "click %o %d");
+ }
+ Cmember.index(0).layout.lay.setvisibility(All.X(A&~B, pset).add(members[0].id));
+
+ layout();
+ deal();
+ allow->add(SAY, nil, "say &");
+ }
+}
+
+command(p: ref Member, cmd: string): string
+{
+ (err, tag, toks) := allow->action(p, cmd);
+ if (err != nil)
+ return err;
+ cp := Cmember.find(p);
+ if (cp == nil)
+ return "bad member";
+ case tag {
+ CLICK =>
+ # click stack index
+ stack := clique.objects[int hd tl toks];
+ nc := len stack.children;
+ idx := int hd tl tl toks;
+ sel := cp.sel;
+ stype := stack.getattr("type");
+ d := dmembers[cp.ord];
+ if (sel.isempty() || sel.stack == stack) {
+ # selecting a card to move
+ if (nc == 0 && stype == "spare1") {
+ cardlib->flip(d.spare2);
+ d.spare2.transfer((0, len d.spare2.children), d.spare1, 0);
+ return nil;
+ }
+ if (idx < 0 || idx >= len stack.children)
+ return "invalid index";
+ if (owner(stack) != cp)
+ return "not yours, don't touch!";
+ case stype {
+ "spare2" or
+ "pile" =>
+ select(cp, stack, (nc - 1, nc));
+ "open" =>
+ select(cp, stack, (idx, nc));
+ "spare1" =>
+ if ((n := nc) > 3)
+ n = 3;
+ for (i := 0; i < n; i++) {
+ cardlib->setface(stack.children[nc - 1], 1);
+ stack.transfer((nc - 1, nc), d.spare2, -1);
+ nc--;
+ }
+ * =>
+ return "you can't move cards from there";
+ }
+ } else {
+ # selecting a stack to move to.
+ frompile := sel.stack.getattr("type") == "pile";
+ case stype {
+ "acepile" =>
+ if (sel.r.end != sel.r.start + 1)
+ return "only one card at a time!";
+ card := getcard(sel.stack.children[sel.r.start]);
+ if (nc == 0) {
+ if (card.number != 0)
+ return "aces only";
+ } else {
+ top := getcard(stack.children[nc - 1]);
+ if (card.number != top.number + 1)
+ return "out of sequence";
+ if (card.suit != top.suit)
+ return "wrong suit";
+ }
+ sel.transfer(stack, -1);
+ if (card.number == KING) # kings get flipped
+ cardlib->setface(stack.children[len stack.children - 1], 0);
+ "open" =>
+ if (owner(stack) != cp)
+ return "not yours, don't touch!";
+ c := getcard(sel.stack.children[sel.r.start]);
+ col := !isred(c);
+ n := c.number + 1;
+ for (i := sel.r.start; i < sel.r.end; i++) {
+ c2 := getcard(sel.stack.children[i]);
+ if (isred(c2) == col)
+ return "bad colour sequence";
+ if (c2.number != n - 1)
+ return "bad number sequence";
+ n = c2.number;
+ col = isred(c2);
+ }
+ if (nc != 0) {
+ c2 := getcard(stack.children[nc - 1]);
+ if (isred(c2) == isred(c) || c2.number != c.number + 1)
+ return "invalid move";
+ }
+ sel.transfer(stack, -1);
+ * =>
+ return "can't move there";
+ }
+ if (frompile) {
+ nc = len d.pile.children;
+ if (nc == 0) {
+ endround();
+ deal();
+ } else {
+ cardlib->setface(d.pile.children[nc - 1], 1);
+ d.pile.setattr("title", "pile [" + string nc + "]", All);
+ }
+ }
+ }
+ SAY =>
+ clique.action("say member " + string p.id + ": '" + joinwords(tl toks) + "'", nil, nil, All);
+
+ SHOW =>
+ clique.show(nil);
+ }
+ return nil;
+}
+
+getcard(card: ref Object): Card
+{
+ return cardlib->getcard(card);
+}
+
+isred(c: Card): int
+{
+ return c.suit == Cardlib->DIAMONDS || c.suit == Cardlib->HEARTS;
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+ if (cp.sel.isempty()) {
+ cp.sel.set(stack);
+ cp.sel.setrange(r);
+ } else {
+ if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+ cp.sel.set(nil);
+ else
+ cp.sel.setrange(r);
+ }
+}
+
+owner(stack: ref Object): ref Cmember
+{
+ parent := clique.objects[stack.parentid];
+ n := cardlib->nmembers();
+ for (i := 0; i < n; i++) {
+ cp := Cmember.index(i);
+ if (cp.obj == parent)
+ return cp;
+ }
+ return nil;
+}
+
+layout()
+{
+ n := cardlib->nmembers();
+ dmembers = array[n] of ref Dmember;
+ for (i := 0; i < n; i++) {
+ cp := Cmember.index(i);
+ d := dmembers[i] = ref Dmember;
+ d.spare1 = newstack(cp.obj, Untitledpilespec, "spare1");
+ d.spare2 = newstack(cp.obj, Untitledpilespec, "spare2");
+ d.pile = newstack(cp.obj, Pilespec, "pile");
+ d.open = array[4] of {* => newstack(cp.obj, Openspec, "open")};
+ d.acepiles = array[4] of {* => newstack(cp.obj, Untitledpilespec, "acepile")};
+ cardlib->makecards(d.spare1, (0, 13), string i);
+ }
+
+ entry := clique.newobject(nil, All, "widget entry");
+ entry.setattr("command", "say", All);
+ cardlib->addlayobj(nil, nil, nil, dTOP|FILLX, entry);
+
+ scores = array[n] of {* => 0};
+ scorelabel = clique.newobject(nil, All, "widget label");
+ setscores();
+ cardlib->addlayobj(nil, nil, nil, dTOP|FILLX, scorelabel);
+
+ cardlib->addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+ row := 0;
+ col := 0;
+ maketable("arena");
+ for (i = 0; i < n; i++) {
+ d := dmembers[i];
+ f := "p" + string i;
+ cardlib->addlayobj(nil, f, nil, dLEFT, d.spare1);
+ cardlib->addlayobj(nil, f, nil, dLEFT, d.spare2);
+ cardlib->addlayobj(nil, f, nil, dLEFT, d.pile);
+ for (j := 0; j < len d.open; j++)
+ cardlib->addlayobj(nil, f, nil, dLEFT|EXPAND|oDOWN, d.open[j]);
+ for (j = 0; j < len d.acepiles; j++) {
+ cardlib->addlayobj(nil, "a" + string row, nil, dLEFT|EXPAND, d.acepiles[j]);
+ if (++col >= NACES) {
+ col = 0;
+ row++;
+ }
+ }
+ }
+}
+
+setscores()
+{
+ s := "Scores: ";
+ n := cardlib->nmembers();
+ for (i := 0; i < n; i++) {
+ s += Cmember.index(i).p.name + ": " + string scores[i];
+ if (i < n - 1)
+ s[len s] = ' ';
+ }
+ scorelabel.setattr("text", s, All);
+}
+
+deal()
+{
+ n := cardlib->nmembers();
+ for (i := 0; i < n; i++) {
+ cp := Cmember.index(i);
+ d := dmembers[i];
+ deck := d.spare1;
+ cardlib->shuffle(deck);
+ deck.transfer((0, 13), d.pile, 0);
+ cardlib->setface(d.pile.children[12], 1);
+ d.pile.setattr("title", "pile [13]", All);
+ for (j := 0; j < len d.open; j++) {
+ deck.transfer((0, 1), d.open[j], 0);
+ cardlib->setface(d.open[j].children[0], 1);
+ }
+ }
+}
+
+endround()
+{
+ # go through all the ace piles, moving cards back to the appropriate deck
+ # and counting appropriately.
+ # move all other cards back too.
+ n := cardlib->nmembers();
+ for (i := 0; i < n; i++) {
+ d := dmembers[i];
+ Cmember.index(i).sel.set(nil);
+ for (j := 0; j < len d.acepiles; j++) {
+ acepile := d.acepiles[j];
+ nc := len acepile.children;
+ for (k := nc - 1; k >= 0; k--) {
+ card := acepile.children[k];
+ back := int card.getattr("rear");
+ scores[back]++;
+ if (getcard(card).number == KING)
+ scores[back] += 5;
+ cardlib->setface(card, 0);
+ acepile.transfer((k, k + 1), dmembers[back].spare1, -1);
+ }
+ }
+ if (len d.pile.children == 0)
+ scores[i] += 10; # bonus for going out
+ else
+ scores[i] -= len d.pile.children;
+ cardlib->discard(d.pile, d.spare1, 1);
+ cardlib->discard(d.spare2, d.spare1, 1);
+ for (j = 0; j < len d.open; j++)
+ cardlib->discard(d.open[j], d.spare1, 1);
+ }
+ setscores();
+}
+
+maketable(parent: string)
+{
+ addlayframe: import cardlib;
+
+ n := cardlib->nmembers();
+ na := ((n * 4) + (NACES - 1)) / NACES;
+ for (i := 0; i < n; i++) {
+ layout := Cmember.index(i).layout;
+ # one frame for each member other than self;
+ # then all the ace piles; then self.
+ for (j := 0; j < n; j++)
+ if (j != i)
+ addlayframe("p" + string j, parent, layout, dTOP|EXPAND, dTOP);
+ for (j = 0; j < na; j++)
+ addlayframe("a" + string j, parent, layout, dTOP|EXPAND|aCENTRELEFT, dTOP);
+ addlayframe("p" + string i, parent, layout, dTOP|EXPAND, dTOP);
+ }
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype: string): ref Object
+{
+ stack := cardlib->newstack(parent, nil, spec);
+ stack.setattr("type", stype, None);
+ stack.setattr("actions", "click", All);
+ return stack;
+}
+
+joinwords(v: list of string): string
+{
+ if (v == nil)
+ return nil;
+ s := hd v;
+ for (v = tl v; v != nil; v = tl v)
+ s += " " + hd v;
+ return s;
+}
+
+remark(s: string)
+{
+ clique.action("remark " + s, nil, nil, All);
+}
diff --git a/appl/spree/engines/snap.b b/appl/spree/engines/snap.b
new file mode 100644
index 00000000..ff7166cb
--- /dev/null
+++ b/appl/spree/engines/snap.b
@@ -0,0 +1,241 @@
+implement Engine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+ allow: Allow;
+include "cardlib.m";
+ cardlib: Cardlib;
+ publicstack: import cardlib;
+ VERT, HORIZ, TOP, BOTTOM, LEFT, RIGHT, Stackspec: import Cardlib;
+
+clique: ref Clique;
+PLAY, START, SAY, SNAP: con iota;
+
+started := 0;
+
+buttons: ref Object;
+scores: ref Object;
+deck: ref Object;
+
+HAND, PILE: con iota;
+
+hands := array[2] of ref Object;
+piles := array[2] of ref Object;
+
+publicspec: array of Stackspec;
+
+privatespec := array[] of {
+ HAND => Stackspec(Cardlib->sPILE,
+ 52,
+ 0,
+ "hand",
+ HORIZ,
+ BOTTOM),
+ PILE => Stackspec(Cardlib->sPILE,
+ 52,
+ 0,
+ "pile",
+ HORIZ,
+ TOP),
+};
+
+oneplayed := 0; # true if only one member's put down a card so far
+
+MINPLAYERS: con 2;
+MAXPLAYERS: con 2;
+
+clienttype(): string
+{
+ return "cards";
+}
+
+init(g: ref Clique, srvmod: Spree): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+
+ allow = load Allow Allow->PATH;
+ if (allow == nil) {
+ sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+ return "bad module";
+ }
+ allow->init(spree, clique);
+ allow->add(SAY, nil, "say &");
+
+ cardlib = load Cardlib Cardlib->PATH;
+ if (cardlib == nil) {
+ sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+ return "bad module";
+ }
+
+ cardlib->init(clique, spree);
+ deck = clique.newobject(nil, ~0, "stack");
+ cardlib->makepack(deck, (0, 52), 1);
+ cardlib->shuffle(deck);
+ buttons = clique.newobject(nil, ~0, "buttons");
+ scores = clique.newobject(nil, ~0, "scoretable");
+
+ return nil;
+}
+
+join(p: ref Member): string
+{
+ sys->print("%s(%d) joining\n", p.name(), p.id);
+ if (!started && cardlib->nmembers() < MAXPLAYERS) {
+ (nil, err) := cardlib->join(p, -1);
+ if (err == nil) {
+ if (cardlib->nmembers() == MINPLAYERS) {
+ mkbutton("Start", "start");
+ allow->add(START, nil, "start");
+ }
+ } else
+ sys->print("error on join: %s\n", err);
+ }
+ return nil;
+}
+
+leave(p: ref Member)
+{
+ cardlib->leave(p);
+ started == 0;
+ if (cardlib->nmembers() < MINPLAYERS) {
+ buttons.deletechildren((0, len buttons.children));
+ allow->del(START, nil);
+ }
+}
+
+command(p: ref Member, cmd: string): string
+{
+ e := ref Sys->Exception;
+ if (sys->rescue("parse:*", e) == Sys->EXCEPTION) {
+ sys->rescued(Sys->ONCE, nil);
+ return e.name[6:];
+ }
+ (err, tag, toks) := allow->action(p, cmd);
+ if (err != nil)
+ return err;
+ case tag {
+ START =>
+ buttons.deletechildren((0, len buttons.children));
+ allow->del(START, nil);
+ allow->add(SNAP, nil, "snap");
+ mkbutton("Snap!", "snap");
+ cardlib->startclique(publicspec, privatespec);
+ for (i := 0; i < 2; i++) {
+ hands[i] = cardlib->info(i).stacks[HAND];
+ piles[i] = cardlib->info(i).stacks[PILE];
+ }
+ deck.transfer((0, 26), hands[0], 0);
+ deck.transfer((0, 26), hands[1], 0);
+ canplay(0);
+ canplay(1);
+
+ PLAY =>
+ # click index
+ ord := cardlib->order(p);
+ inf := cardlib->info(ord);
+
+ hand := hands[ord];
+ pile := piles[ord];
+ hand.transfer((len hand.children - 1, len hand.children), pile, len pile.children);
+ cardlib->setface(pile.children[len pile.children - 1], 1);
+ cantplay(ord);
+ oneplayed = !oneplayed;
+ if (!oneplayed || len hands[!ord].children == 0) {
+ for (i := 0; i < 2; i++)
+ if (len hands[i].children > 0)
+ canplay(i);
+ }
+ SNAP =>
+ # snap
+ ord := cardlib->order(p);
+ inf := cardlib->info(ord);
+ if (oneplayed) # XXX allow for case where one person has no cards.
+ return "must wait for two cards to be put down";
+ if (len piles[0].children == 0 || len piles[1].children == 0)
+ return "no cards";
+ c0 := cardlib->getcard(piles[0].children[len piles[0].children - 1]);
+ c1 := cardlib->getcard(piles[1].children[len piles[0].children - 1]);
+ if (c0.number != c1.number) {
+ remark(p.name() + " said snap wrongly!");
+ return "cards must be the same";
+ } else {
+ transferall(piles[!ord], piles[ord], len piles[ord].children);
+ flipstack(piles[ord]);
+ transferall(piles[ord], hands[ord], 0);
+ if (len hands[!ord].children == 0)
+ remark(p.name() + " has won!");
+ oneplayed = 0;
+ for (i := 0; i < 2; i++)
+ if (len hands[i].children > 0)
+ canplay(i);
+ else
+ cantplay(i);
+ }
+ SAY =>
+ clique.action("say member " + string p.id + ": '" + joinwords(tl toks) + "'", nil, nil, ~0);
+ }
+ return nil;
+}
+
+transferall(stack, into: ref Object, idx: int)
+{
+ stack.transfer((0, len stack.children), into, idx);
+}
+
+flipstack(stack: ref Object)
+{
+ for (i := 0; i < len stack.children; i++) {
+ card := stack.children[i];
+ cardlib->setface(card, ! int card.getattr("face"));
+ }
+}
+
+joinwords(v: list of string): string
+{
+ if (v == nil)
+ return nil;
+ s := hd v;
+ for (v = tl v; v != nil; v = tl v)
+ s += " " + hd v;
+ return s;
+}
+
+canplay(ord: int)
+{
+ inf := cardlib->info(ord);
+ allow->del(PLAY, inf.p);
+ allow->add(PLAY, inf.p, "click %d");
+ inf.stacks[HAND].setattr("actions", "click", 1<<inf.p.id);
+}
+
+cantplay(ord: int)
+{
+ inf := cardlib->info(ord);
+ allow->del(PLAY, inf.p);
+ inf.stacks[HAND].setattr("actions", nil, 1<<inf.p.id);
+}
+
+member(ord: int): ref Member
+{
+ return cardlib->info(ord).p;
+}
+
+remark(s: string)
+{
+ clique.action("remark " + s, nil, nil, ~0);
+}
+
+mkbutton(text, cmd: string): ref Object
+{
+ but := clique.newobject(buttons, ~0, "button");
+ but.setattr("text", text, ~0);
+ but.setattr("command", cmd, ~0);
+ return but;
+}
diff --git a/appl/spree/engines/spider.b b/appl/spree/engines/spider.b
new file mode 100644
index 00000000..08576ff0
--- /dev/null
+++ b/appl/spree/engines/spider.b
@@ -0,0 +1,259 @@
+implement Gatherengine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ All, None: import Sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+ allow: Allow;
+include "cardlib.m";
+ cardlib: Cardlib;
+ Selection, Cmember, Card: import cardlib;
+ getcard: import cardlib;
+ dTOP, dRIGHT, dLEFT, oRIGHT, oDOWN,
+ aCENTRERIGHT, aCENTRELEFT, aUPPERRIGHT, aUPPERCENTRE,
+ EXPAND, FILLX, FILLY, Stackspec: import Cardlib;
+include "../gather.m";
+
+clique: ref Clique;
+
+open: array of ref Object; # [10]
+deck: ref Object;
+discard: ref Object;
+dealbutton: ref Object;
+
+CLICK, MORECARDS: con iota;
+
+Openspec := Stackspec(
+ "display", # style
+ 19, # maxcards
+ 0, # conceal
+ "" # title
+);
+
+clienttype(): string
+{
+ return "cards";
+}
+
+maxmembers(): int
+{
+ return 1;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+ return nil;
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+ allow = load Allow Allow->PATH;
+ if (allow == nil) {
+ sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+ return "bad module";
+ }
+ allow->init(spree, clique);
+ cardlib = load Cardlib Cardlib->PATH;
+ if (cardlib == nil) {
+ sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+ return "bad module";
+ }
+ return nil;
+}
+
+propose(members: array of string): string
+{
+ if (len members != 1)
+ return "one member only";
+ return nil;
+}
+
+archive()
+{
+ archiveobj := cardlib->archive();
+ allow->archive(archiveobj);
+ cardlib->archivearray(open, "open");
+ cardlib->setarchivename(deck, "deck");
+ cardlib->setarchivename(discard, "discard");
+ cardlib->setarchivename(dealbutton, "dealbutton");
+}
+
+start(members: array of ref Member, archived: int)
+{
+ cardlib->init(spree, clique);
+ if (archived) {
+ archiveobj := cardlib->unarchive();
+ allow->unarchive(archiveobj);
+ open = cardlib->getarchivearray("open");
+ discard = cardlib->getarchiveobj("discard");
+ deck = cardlib->getarchiveobj("deck");
+ dealbutton = cardlib->getarchiveobj("dealbutton");
+ } else {
+ p := members[0];
+ Cmember.join(p, -1).layout.lay.setvisibility(All);
+ startclique();
+ allow->add(CLICK, p, "click %o %d");
+ allow->add(MORECARDS, p, "morecards");
+ }
+}
+
+command(p: ref Member, cmd: string): string
+{
+ (err, tag, toks) := allow->action(p, cmd);
+ if (err != nil)
+ return err;
+ cp := Cmember.find(p);
+ if (cp == nil)
+ return "you are not playing";
+ case tag {
+ CLICK =>
+ # click stack index
+ stack := clique.objects[int hd tl toks];
+ nc := len stack.children;
+ idx := int hd tl tl toks;
+ sel := cp.sel;
+ stype := stack.getattr("type");
+ if (sel.isempty() || sel.stack == stack) {
+ if (idx < 0 || idx >= len stack.children)
+ return "invalid index";
+ case stype {
+ "open" =>
+ select(cp, stack, (idx, nc));
+ * =>
+ return "you can't move cards from there";
+ }
+ } else {
+ from := sel.stack;
+ case stype {
+ "open" =>
+ c := getcard(sel.stack.children[sel.r.start]);
+ n := c.number + 1;
+ for (i := sel.r.start; i < sel.r.end; i++) {
+ c2 := getcard(sel.stack.children[i]);
+ if (c2.face == 0)
+ return "cannot move face down cards";
+ if (c2.number != n - 1)
+ return "bad number sequence";
+ n = c2.number;
+ }
+ if (nc != 0) {
+ c2 := getcard(stack.children[nc - 1]);
+ if (c2.number != c.number + 1)
+ return "descending, only";
+ }
+ srcstack := sel.stack;
+ sel.transfer(stack, -1);
+ turntop(srcstack);
+
+ nc = len stack.children;
+ if (nc >= 13) {
+ c = getcard(stack.children[nc - 1]);
+ suit := c.suit;
+ for (i = 0; i < 13; i++) {
+ c = getcard(stack.children[nc - i - 1]);
+ if (c.suit != suit || c.number != i)
+ break;
+ }
+ if (i == 13) {
+ stack.transfer((nc - 13, nc), discard, -1);
+ turntop(stack);
+ }
+ }
+ * =>
+ return "can't move there";
+ }
+ }
+ MORECARDS =>
+ for (i := 0; i < 10; i++)
+ if (len open[i].children == 0)
+ return "spaces must be filled before redeal";
+ for (i = 0; i < 10; i++) {
+ if (len deck.children == 0)
+ break;
+ cp.sel.set(nil);
+ cardlib->setface(deck.children[0], 1);
+ deck.transfer((0, 1), open[i], -1);
+ }
+ setdealbuttontext();
+ }
+ return nil;
+}
+
+setdealbuttontext()
+{
+ dealbutton.setattr("text", sys->sprint("deal more (%d left)", len deck.children), All);
+}
+
+turntop(stack: ref Object)
+{
+ if (len stack.children > 0)
+ cardlib->setface(stack.children[len stack.children - 1], 1);
+}
+
+startclique()
+{
+ addlayobj, addlayframe: import cardlib;
+ open = array[10] of {* => newstack(nil, Openspec, "open", nil)};
+ deck = clique.newobject(nil, All, "stack");
+ discard = clique.newobject(nil, All, "stack");
+ cardlib->makecards(deck, (0, 13), "0");
+ cardlib->makecards(deck, (0, 13), "1");
+ addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+ addlayframe("top", "arena", nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+
+ for (i := 0; i < 10; i++)
+ addlayobj(nil, "top", nil, dLEFT|oDOWN|EXPAND|aUPPERCENTRE, open[i]);
+ addlayframe("bot", "arena", nil, dTOP, dTOP);
+ dealbutton = newbutton("morecards", "deal more");
+ addlayobj(nil, "bot", nil, dLEFT, dealbutton);
+ deal();
+ setdealbuttontext();
+}
+
+deal()
+{
+ cardlib->shuffle(deck);
+ for (i := 0; i < 10; i++) {
+ deck.transfer((0, 4), open[i], 0);
+ turntop(open[i]);
+ }
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype, title: string): ref Object
+{
+ stack := cardlib->newstack(parent, nil, spec);
+ stack.setattr("type", stype, None);
+ stack.setattr("actions", "click", All);
+ stack.setattr("title", title, All);
+ return stack;
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+ if (cp.sel.isempty()) {
+ cp.sel.set(stack);
+ cp.sel.setrange(r);
+ } else {
+ if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+ cp.sel.set(nil);
+ else
+ cp.sel.setrange(r);
+ }
+}
+
+newbutton(cmd, text: string): ref Object
+{
+ but := clique.newobject(nil, All, "widget button");
+ but.setattr("command", cmd, All);
+ but.setattr("text", text, All);
+ return but;
+}
+
diff --git a/appl/spree/engines/spit.b b/appl/spree/engines/spit.b
new file mode 100644
index 00000000..2c42cb95
--- /dev/null
+++ b/appl/spree/engines/spit.b
@@ -0,0 +1,483 @@
+implement Gatherengine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ sets: Sets;
+ Set, All, None, A, B: import sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+ allow: Allow;
+include "cardlib.m";
+ cardlib: Cardlib;
+ Selection, Cmember, Card: import cardlib;
+ dTOP, dLEFT, dBOTTOM, oDOWN, EXPAND, FILLX, FILLY, aCENTRELEFT, Stackspec: import Cardlib;
+include "../gather.m";
+
+clique: ref Clique;
+CLICK, SPIT, SAY, SHOW: con iota;
+playing := 0;
+dealt := 0;
+deck: ref Object;
+buttons: ref Object;
+winner: ref Member;
+
+Dmember: adt {
+ spare: ref Object;
+ row: array of ref Object;
+ centre: ref Object;
+};
+
+dmembers := array[2] of ref Dmember;
+
+Openspec := Stackspec(
+ "display", # style
+ 4, # maxcards
+ 0, # conceal
+ "" # title
+);
+
+Pilespec := Stackspec(
+ "pile", # style
+ 13, # maxcards
+ 0, # conceal
+ "pile" # title
+);
+
+Untitledpilespec := Stackspec(
+ "pile", # style
+ 13, # maxcards
+ 0, # conceal
+ "" # title
+);
+
+clienttype(): string
+{
+ return "cards";
+}
+
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+
+ allow = load Allow Allow->PATH;
+ if (allow == nil) {
+ sys->print("spit: cannot load %s: %r\n", Allow->PATH);
+ return "bad module";
+ }
+ allow->init(spree, clique);
+ sets = load Sets Sets->PATH;
+ if (sets == nil) {
+ sys->print("spit: cannot load %s: %r\n", Sets->PATH);
+ return "bad module";
+ }
+ sets->init();
+
+ cardlib = load Cardlib Cardlib->PATH;
+ if (cardlib == nil) {
+ sys->print("spit: cannot load %s: %r\n", Cardlib->PATH);
+ return "bad module";
+ }
+ cardlib->init(spree, clique);
+
+ return nil;
+}
+
+maxmembers(): int
+{
+ return 2;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+ return nil;
+}
+
+propose(members: array of string): string
+{
+ if (len members != 2)
+ return "need exactly two members";
+ return nil;
+}
+
+archive()
+{
+ archiveobj := cardlib->archive();
+ allow->archive(archiveobj);
+ for (i := 0; i < len dmembers; i++) {
+ dp := dmembers[i];
+ s := "d" + string i + "_";
+ cardlib->setarchivename(dp.spare, s + "spare");
+ cardlib->setarchivename(dp.centre, s + "centre");
+ for (j := 0; j < len dp.row; j++)
+ cardlib->setarchivename(dp.row[j], s + "row" + string j);
+ }
+ archiveobj.setattr("playing", string playing, None);
+ archiveobj.setattr("dealt", string dealt, None);
+ cardlib->setarchivename(deck, "deck");
+}
+
+start(members: array of ref Member, archived: int)
+{
+ cardlib->init(spree, clique);
+ if (archived) {
+ archiveobj := cardlib->unarchive();
+ allow->unarchive(archiveobj);
+ playing = int archiveobj.getattr("playing");
+ dealt = int archiveobj.getattr("dealt");
+ deck = cardlib->getarchiveobj("deck");
+ for (i := 0; i < len dmembers; i++) {
+ dp := dmembers[i] = ref Dmember;
+ s := "d" + string i + "_";
+ dp.spare = cardlib->getarchiveobj(s + "spare");
+ dp.centre = cardlib->getarchiveobj(s + "centre");
+ dp.row = array[4] of ref Object;
+ for (j := 0; j < len dp.row; j++)
+ dp.row[j] = cardlib->getarchiveobj(s + "row" + string j);
+ }
+ } else {
+ buttons = clique.newobject(nil, All, "buttons");
+ pset := None;
+ for (i := 0; i < len members; i++) {
+ Cmember.join(members[i], i);
+ pset = pset.add(members[i].id);
+ }
+ # member 0 layout visible to member 0 and everyone else but other member.
+ # could be All.del(members[1].id) but doing it this way extends to many-member cliques.
+ Cmember.index(0).layout.lay.setvisibility(All.X(A&~B, pset).add(members[0].id));
+ layout();
+ deal();
+ dealt = 1;
+ playing = 0;
+ allow->add(SPIT, nil, "spit");
+ allow->add(SAY, nil, "say &");
+ allow->add(SHOW, nil, "show");
+ }
+}
+
+command(p: ref Member, cmd: string): string
+{
+ (err, tag, toks) := allow->action(p, cmd);
+ if (err != nil){
+ if(winner != nil){
+ if(winner == p)
+ return "game has finished: you have won";
+ return "game has finished: you have lost";
+ }
+ return err;
+ }
+ cp := Cmember.find(p);
+ if (cp == nil)
+ return "you're only watching";
+ case tag {
+ SPIT =>
+ if (!dealt) {
+ deal();
+ dealt = 1;
+ } else if (!playing) {
+ go();
+ allow->add(CLICK, nil, "click %o %d");
+ playing = 1;
+ } else if (!canplay(!cp.ord)) {
+ go();
+ } else
+ return "it is possible to play";
+
+ CLICK =>
+ stack := clique.objects[int hd tl toks];
+ nc := len stack.children;
+ idx := int hd tl tl toks;
+ sel := cp.sel;
+ stype := stack.getattr("type");
+ d := dmembers[cp.ord];
+ if (sel.isempty() || sel.stack == stack) {
+ # selecting a card to move
+ if (idx < 0 || idx >= len stack.children)
+ return "invalid index";
+ if (owner(stack) != cp)
+ return "not yours, don't touch!";
+ case stype {
+ "row" =>
+ card := getcard(stack.children[nc - 1]);
+ if (card.face == 0)
+ cardlib->setface(stack.children[nc - 1], 1);
+ else
+ select(cp, stack, (nc - 1, nc));
+ * =>
+ return "you can't move cards from there";
+ }
+ } else {
+ # selecting a stack to move to.
+ case stype {
+ "centre" =>
+ card := getcard(sel.stack.children[sel.r.start]);
+ onto := getcard(stack.children[nc - 1]);
+ if ((card.number + 1) % 13 != onto.number &&
+ (card.number + 12) % 13 != onto.number) {
+ sel.set(nil);
+ return "out of sequence";
+ }
+ sel.transfer(stack, -1);
+ for (i := 0; i < len d.row; i++)
+ if (len d.row[i].children > 0)
+ break;
+ if (i == len d.row) {
+ if (len d.spare.children == 0) {
+ remark(p.name + " has won");
+ winner = p;
+ allow->del(CLICK, nil);
+ allow->del(SPIT, nil);
+ clearsel();
+ } else
+ finish(cp);
+ }
+ "row" =>
+ if (owner(stack) != cp) {
+ sel.set(nil);
+ return "not yours, don't touch!";
+ }
+ if (nc != 0) {
+ sel.set(nil);
+ return "cannot stack cards";
+ }
+ sel.transfer(stack, -1);
+ * =>
+ sel.set(nil);
+ return "can't move there";
+ }
+ }
+
+ SAY =>
+ clique.action("say member " + string p.id + ": '" + concat(tl toks) + "'", nil, nil, All);
+
+ SHOW =>
+ clique.show(nil);
+ }
+ return nil;
+}
+
+canplay(ord: int): int
+{
+ d := dmembers[ord];
+ nmulti := nfree := 0;
+ for (j := 0; j < len d.row; j++) {
+ s1 := d.row[j];
+ if (len s1.children > 0) {
+ nmulti += len s1.children > 1;
+ card1 := getcard(s1.children[len s1.children - 1]);
+ for (k := 0; k < 2; k++) {
+ s2 := dmembers[k].centre;
+ if (len s2.children > 0) {
+ card2 := getcard(s2.children[len s2.children - 1]);
+ if ((card1.number + 1) % 13 == card2.number ||
+ (card1.number + 12) % 13 == card2.number)
+ return 1;
+ }
+ }
+ } else
+ nfree++;
+ }
+ return nmulti > 0 && nfree > 0;
+}
+
+bottomdiscard(src, dst: ref Object)
+{
+ cardlib->flip(src);
+ for (i := 0; i < len src.children; i++)
+ cardlib->setface(src.children[i], 0);
+ src.transfer((0, len src.children), dst, 0);
+}
+
+finish(winner: ref Cmember)
+{
+ loser := dmembers[!winner.ord];
+ for (i := 0; i < 2; i++) {
+ d := dmembers[i];
+ bottomdiscard(d.centre, loser.spare);
+ for (j := 0; j < len d.row; j++)
+ bottomdiscard(d.row[j], loser.spare);
+ }
+ playing = 0;
+ dealt = 0;
+ allow->del(CLICK, nil);
+ allow->add(SPIT, nil, "spit");
+ clearsel();
+}
+
+go()
+{
+ for (i := 0; i < 2; i++) {
+ d := dmembers[i];
+ n := len d.spare.children;
+ if (n > 0)
+ d.spare.transfer((n - 1, n), d.centre, -1);
+ else if ((m := len dmembers[!i].spare.children) > 0)
+ dmembers[!i].spare.transfer((m - 1, m), d.centre, -1);
+ else {
+ # both members' spare piles are used up; use central piles instead
+ for (j := 0; j < 2; j++) {
+ cardlib->discard(dmembers[j].centre, dmembers[j].spare, 0);
+ cardlib->flip(dmembers[j].spare);
+ }
+ go();
+ return;
+ }
+ cardlib->setface(d.centre.children[len d.centre.children - 1], 1);
+ }
+}
+
+getcard(card: ref Object): Card
+{
+ return cardlib->getcard(card);
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+ if (cp.sel.isempty()) {
+ cp.sel.set(stack);
+ cp.sel.setrange(r);
+ } else {
+ if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+ cp.sel.set(nil);
+ else
+ cp.sel.setrange(r);
+ }
+}
+
+owner(stack: ref Object): ref Cmember
+{
+ parent := clique.objects[stack.parentid];
+ n := cardlib->nmembers();
+ for (i := 0; i < n; i++) {
+ cp := Cmember.index(i);
+ if (cp.obj == parent)
+ return cp;
+ }
+ return nil;
+}
+
+layout()
+{
+ for (i := 0; i < 2; i++) {
+ cp := Cmember.index(i);
+ d := dmembers[i] = ref Dmember;
+ d.spare = newstack(cp.obj, Untitledpilespec, "spare");
+ d.row = array[4] of {* => newstack(cp.obj, Openspec, "row")};
+ d.centre = newstack(cp.obj, Untitledpilespec, "centre");
+ }
+ deck = clique.newobject(nil, All, "stack");
+ cardlib->makecards(deck, (0, 13), "0");
+ cardlib->shuffle(deck);
+
+ entry := clique.newobject(nil, All, "widget entry");
+ entry.setattr("command", "say", All);
+ cardlib->addlayobj(nil, nil, nil, dTOP|FILLX, entry);
+
+ cardlib->addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+ maketable("arena");
+ spitbutton := newbutton("spit", "Spit!");
+ for (i = 0; i < 2; i++) {
+ d := dmembers[i];
+ f := "p" + string i;
+
+ subf := "f" + string i;
+ cardlib->addlayframe(subf, f, nil, dLEFT, dTOP);
+ cardlib->addlayobj(nil, subf, Cmember.index(i).layout, dTOP, spitbutton);
+ cardlib->addlayobj(nil, subf, nil, dTOP, d.spare);
+ for (j := 0; j < len d.row; j++)
+ cardlib->addlayobj(nil, f, nil, dLEFT|EXPAND|oDOWN, d.row[j]);
+ cardlib->addlayobj(nil, "centre", nil, dLEFT|EXPAND, d.centre);
+ }
+}
+
+newbutton(cmd, text: string): ref Object
+{
+ but := clique.newobject(nil, All, "widget button");
+ but.setattr("command", cmd, All);
+ but.setattr("text", text, All);
+ return but;
+}
+
+settopface(stack: ref Object, face: int)
+{
+ n := len stack.children;
+ if (n > 0)
+ cardlib->setface(stack.children[n - 1], face);
+}
+
+transfertop(src, dst: ref Object, index: int)
+{
+ n := len src.children;
+ src.transfer((n - 1, n), dst, index);
+}
+
+deal()
+{
+ clearsel();
+ n := len deck.children;
+ if (n > 0) {
+ deck.transfer((0, n / 2), dmembers[0].spare, 0);
+ deck.transfer((0, len deck.children), dmembers[1].spare, 0);
+ }
+
+ for (i := 0; i < 2; i++) {
+ d := dmembers[i];
+loop: for (j := 0; j < len d.row; j++) {
+ for (k := j; k < len d.row; k++) {
+ if (len d.spare.children == 0)
+ break loop;
+ transfertop(d.spare, d.row[k], -1);
+ }
+ }
+ for (j = 0; j < len d.row; j++)
+ settopface(d.row[j], 1);
+ }
+}
+
+maketable(parent: string)
+{
+ addlayframe: import cardlib;
+
+ for (i := 0; i < 2; i++) {
+ layout := Cmember.index(i).layout;
+ addlayframe("p" + string !i, parent, layout, dTOP|EXPAND, dBOTTOM);
+ addlayframe("p" + string i, parent, layout, dBOTTOM|EXPAND, dTOP);
+ addlayframe("centre", parent, layout, dTOP|EXPAND, dTOP);
+ }
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype: string): ref Object
+{
+ stack := cardlib->newstack(parent, nil, spec);
+ stack.setattr("type", stype, None);
+ stack.setattr("actions", "click", All);
+ return stack;
+}
+
+concat(v: list of string): string
+{
+ if (v == nil)
+ return nil;
+ s := hd v;
+ for (v = tl v; v != nil; v = tl v)
+ s += " " + hd v;
+ return s;
+}
+
+remark(s: string)
+{
+ clique.action("remark " + s, nil, nil, All);
+}
+
+clearsel()
+{
+ n := cardlib->nmembers();
+ for (i := 0; i < n; i++)
+ Cmember.index(i).sel.set(nil);
+}
diff --git a/appl/spree/engines/whist.b b/appl/spree/engines/whist.b
new file mode 100644
index 00000000..ca0c26f9
--- /dev/null
+++ b/appl/spree/engines/whist.b
@@ -0,0 +1,305 @@
+implement Gatherengine;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ sets: Sets;
+ Set, All, None, A, B: import sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+ allow: Allow;
+include "cardlib.m";
+ cardlib: Cardlib;
+ Selection, Cmember: import cardlib;
+ dTOP, dLEFT, oRIGHT, EXPAND, FILLX, FILLY, Stackspec: import Cardlib;
+include "tricks.m";
+ tricks: Tricks;
+ Trick: import tricks;
+include "../gather.m";
+
+clique: ref Clique;
+CLICK, SAY: con iota;
+
+scores: ref Object;
+deck, pile: ref Object;
+hands, taken: array of ref Object;
+leader, turn: ref Cmember;
+trick: ref Trick;
+
+Trickpilespec := Stackspec(
+ "display", # style
+ 4, # maxcards
+ 0, # conceal
+ "trick pile" # title
+);
+
+Handspec := Stackspec(
+ "display",
+ 13,
+ 1,
+ ""
+);
+
+Takenspec := Stackspec(
+ "pile",
+ 52,
+ 0,
+ "tricks"
+);
+
+clienttype(): string
+{
+ return "cards";
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+ sys = load Sys Sys->PATH;
+ clique = g;
+ spree = srvmod;
+
+ allow = load Allow Allow->PATH;
+ if (allow == nil) {
+ sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+ return "bad module";
+ }
+ allow->init(spree, clique);
+
+ sets = load Sets Sets->PATH;
+ if (sets == nil) {
+ sys->print("spit: cannot load %s: %r\n", Sets->PATH);
+ return "bad module";
+ }
+ sets->init();
+
+ cardlib = load Cardlib Cardlib->PATH;
+ if (cardlib == nil) {
+ sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+ return "bad module";
+ }
+
+ tricks = load Tricks Tricks->PATH;
+ if (tricks == nil) {
+ sys->print("hearts: cannot load %s: %r\n", Tricks->PATH);
+ return "bad module";
+ }
+
+ return nil;
+}
+
+maxmembers(): int
+{
+ return 4;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+ return nil;
+}
+
+propose(members: array of string): string
+{
+ if (len members < 2)
+ return "need at least two members";
+ if (len members > 4)
+ return "too many members";
+ return nil;
+}
+
+archive()
+{
+ archiveobj := cardlib->archive();
+ allow->archive(archiveobj);
+
+ cardlib->setarchivename(scores, "scores");
+ cardlib->setarchivename(deck, "deck");
+ cardlib->setarchivename(pile, "pile");
+ cardlib->archivearray(hands, "hands");
+ cardlib->archivearray(taken, "taken");
+ if (leader != nil)
+ archiveobj.setattr("leader", string leader.ord, None);
+ if (turn != nil)
+ archiveobj.setattr("turn", string turn.ord, None);
+ trick.archive(archiveobj, "trick");
+}
+
+start(members: array of ref Member, archived: int)
+{
+ cardlib->init(spree, clique);
+ tricks->init(spree, clique, cardlib);
+ if (archived) {
+ archiveobj := cardlib->unarchive();
+ allow->unarchive(archiveobj);
+
+ scores = cardlib->getarchiveobj("scores");
+ deck = cardlib->getarchiveobj("deck");
+ pile = cardlib->getarchiveobj("pile");
+ hands = cardlib->getarchivearray("hands");
+ taken = cardlib->getarchivearray("taken");
+
+ o := archiveobj.getattr("leader");
+ if (o != nil)
+ leader = Cmember.index(int o);
+ o = archiveobj.getattr("turn");
+ if (o != nil)
+ turn = Cmember.index(int o);
+ trick = Trick.unarchive(archiveobj, "trick");
+ } else {
+ pset := None;
+ for (i := 0; i < len members; i++) {
+ Cmember.join(members[i], i);
+ pset = pset.add(members[i].id);
+ }
+ # member 0 layout visible to member 0 and everyone else but other member.
+ # could be All.del(members[1].id) but doing it this way extends to many-member cliques.
+ Cmember.index(0).layout.lay.setvisibility(All.X(A&~B, pset).add(members[0].id));
+ deck = clique.newobject(nil, All, "stack");
+ cardlib->makecards(deck, (0, 13), nil);
+ cardlib->shuffle(deck);
+ scores = clique.newobject(nil, All, "scoretable");
+ startclique();
+ n := cardlib->nmembers();
+ leader = Cmember.index(rand(n));
+ starthand();
+ titles := "";
+ for (i = 0; i < n; i++)
+ titles += members[i].name + " ";
+ clique.newobject(scores, All, "score").setattr("score", titles, All);
+ }
+}
+
+command(p: ref Member, cmd: string): string
+{
+ (err, tag, toks) := allow->action(p, cmd);
+ if (err != nil)
+ return err;
+ cp := Cmember.find(p);
+ if (cp == nil)
+ return "you're only watching";
+ case tag {
+ CLICK =>
+ # click stackid index
+ stack := p.obj(int hd tl toks);
+ if (stack != trick.hands[cp.ord])
+ return "not yours";
+ err = trick.play(cp.ord, int hd tl tl toks);
+ if (err != nil)
+ return err;
+
+ turn = turn.next(1);
+ if (turn == leader) { # come full circle
+ winner := Cmember.index(trick.winner);
+ remark(sys->sprint("%s won the trick", winner.p.name));
+ cardlib->discard(pile, taken[winner.ord], 0);
+ nmembers := cardlib->nmembers();
+ taken[winner.ord].setattr("title",
+ string (len taken[winner.ord].children / nmembers) +
+ " tricks", All);
+ o := winner.obj;
+ trick = nil;
+ s := "";
+ for (i := 0; i < nmembers; i++) {
+ if (i == winner.ord)
+ s += "1 ";
+ else
+ s += "0 ";
+ }
+ clique.newobject(scores, All, "score").setattr("score", s, All);
+ if (len hands[winner.ord].children > 0) {
+ leader = turn = winner;
+ trick = Trick.new(pile, -1, hands, nil);
+ } else {
+ remark("one round down, some to go");
+ leader = turn = nil; # XXX this round over
+ }
+ }
+ canplay(turn);
+ SAY =>
+ clique.action("say member " + string p.id + ": '" + joinwords(tl toks) + "'", nil, nil, All);
+ }
+ return nil;
+}
+
+startclique()
+{
+ entry := clique.newobject(nil, All, "widget entry");
+ entry.setattr("command", "say", All);
+ cardlib->addlayobj("entry", nil, nil, dTOP|FILLX, entry);
+ cardlib->addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+ cardlib->maketable("arena");
+
+ pile = cardlib->newstack(nil, nil, Trickpilespec);
+ cardlib->addlayobj(nil, "public", nil, dTOP|oRIGHT, pile);
+ n := cardlib->nmembers();
+ hands = array[n] of ref Object;
+ taken = array[n] of ref Object;
+ tt := clique.newobject(nil, All, "widget menu");
+ tt.setattr("text", "hello", All);
+ for (ml := "one" :: "two" :: "three" :: nil; ml != nil; ml = tl ml) {
+ o := clique.newobject(tt, All, "menuentry");
+ o.setattr("text", hd ml, All);
+ o.setattr("command", hd ml, All);
+ }
+ for (i := 0; i < n; i++) {
+ cp := Cmember.index(i);
+ hands[i] = cardlib->newstack(cp.obj, cp.p, Handspec);
+ taken[i] = cardlib->newstack(cp.obj, cp.p, Takenspec);
+ p := "p" + string i;
+ cardlib->addlayframe(p + ".f", p, nil, dLEFT|oRIGHT, dTOP);
+ cardlib->addlayobj(nil, p + ".f", cp.layout, dTOP, tt);
+ cardlib->addlayobj(nil, p + ".f", nil, dTOP, hands[i]);
+ cardlib->addlayobj(nil, "p" + string i, nil, dLEFT|oRIGHT, taken[i]);
+ }
+}
+
+joinwords(v: list of string): string
+{
+ if (v == nil)
+ return nil;
+ s := hd v;
+ for (v = tl v; v != nil; v = tl v)
+ s += " " + hd v;
+ return s;
+}
+
+suitrank := array[] of {
+ Cardlib->CLUBS => 0,
+ Cardlib->DIAMONDS => 1,
+ Cardlib->SPADES => 2,
+ Cardlib->HEARTS => 3
+};
+
+starthand()
+{
+ cardlib->deal(deck, 13, hands, 0);
+ for (i := 0; i < len hands; i++)
+ cardlib->sort(hands[i], nil, suitrank);
+ trick = Trick.new(pile, -1, hands, nil);
+ turn = leader;
+ canplay(turn);
+}
+
+canplay(cp: ref Cmember)
+{
+ allow->del(CLICK, nil);
+ for (i := 0; i < cardlib->nmembers(); i++) {
+ ccp := Cmember.index(i);
+ v := None.add(ccp.p.id);
+ ccp.obj.setattr("status", nil, v);
+ hands[i].setattr("actions", nil, v);
+ }
+ if (cp != nil && cp.ord != -1) {
+ allow->add(CLICK, cp.p, "click %d %d");
+ v := None.add(cp.p.id);
+ cp.obj.setattr("status", "Your turn", v);
+ hands[cp.ord].setattr("actions", "click", v);
+ }
+}
+
+remark(s: string)
+{
+ clique.action("remark " + s, nil, nil, All);
+}
diff --git a/appl/spree/gather.m b/appl/spree/gather.m
new file mode 100644
index 00000000..eb7363a3
--- /dev/null
+++ b/appl/spree/gather.m
@@ -0,0 +1,10 @@
+Gatherengine: module {
+ init: fn(srvmod: Spree, clique: ref Spree->Clique, argv: list of string, archived: int): string;
+ propose: fn(members: array of string): string;
+ start: fn(members: array of ref Spree->Member, archived: int);
+ command: fn(member: ref Spree->Member, e: string): string;
+ readfile: fn(f: int, offset: big, n: int): array of byte;
+ archive: fn();
+ clienttype: fn(): string;
+ maxmembers: fn(): int;
+};
diff --git a/appl/spree/join.b b/appl/spree/join.b
new file mode 100644
index 00000000..8d4d54fe
--- /dev/null
+++ b/appl/spree/join.b
@@ -0,0 +1,115 @@
+implement Join;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sh.m";
+include "arg.m";
+include "join.m";
+
+usage()
+{
+ sys->fprint(stderr(), "usage: joinsession [-d mntdir] [-j joinrequest] name\n");
+ raise "fail:usage";
+}
+
+CLIENTDIR: con "/dis/spree/clients";
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+ sys = load Sys Sys->PATH;
+ arg := load Arg Arg->PATH;
+ arg->init(argv);
+ mnt := "/n/remote";
+ joinmsg := "join";
+ while ((opt := arg->opt()) != 0) {
+ case opt {
+ 'd' =>
+ if ((mnt = arg->arg()) == nil)
+ usage();
+ 'j' =>
+ joinmsg = arg->arg();
+ * =>
+ usage();
+ }
+ }
+ argv = arg->argv();
+ if (len argv != 1)
+ usage();
+ arg = nil;
+ e := join(ctxt, mnt, hd argv, joinmsg);
+ if (e != nil) {
+ sys->fprint(stderr(), "startclient: %s\n", e);
+ raise "fail:error";
+ }
+}
+
+join(ctxt: ref Draw->Context, mnt: string, dir: string, joinmsg: string): string
+{
+ if (sys == nil)
+ sys = load Sys Sys->PATH;
+
+ fd := sys->open(mnt + "/" + dir + "/ctl", Sys->ORDWR);
+ if (fd == nil)
+ return sys->sprint("cannot open %s: %r", mnt + "/" + dir + "/ctl");
+ if (joinmsg != nil)
+ if (sys->fprint(fd, "%s", joinmsg) == -1)
+ return sys->sprint("cannot join: %r");
+
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(fd, buf, len buf)) > 0) {
+ (nil, lines) := sys->tokenize(string buf[0:n], "\n");
+ for (; lines != nil; lines = tl lines) {
+ (nil, toks) := sys->tokenize(hd lines, " ");
+ if (len toks > 1 && hd toks == "clienttype") {
+ sync := chan of string;
+ spawn startclient(ctxt, hd tl toks :: mnt :: dir :: tl tl toks, fd, sync);
+ fd = nil;
+ return <-sync;
+ }
+ sys->fprint(stderr(), "startclient: unknown lobby message %s\n", hd lines);
+ }
+ }
+ return "premature EOF";
+}
+
+startclient(ctxt: ref Draw->Context, argv: list of string, fd: ref Sys->FD, sync: chan of string)
+{
+ sys->pctl(Sys->FORKNS|Sys->FORKFD|Sys->NEWPGRP, nil);
+ sys->dup(fd.fd, 0);
+ fd = nil;
+ sys->pctl(Sys->NEWFD, 0 :: 1 :: 2 :: nil);
+
+ # XXX security: weed out slashes
+ path := CLIENTDIR + "/" + hd argv + ".dis";
+ mod := load Command path;
+ if (mod == nil) {
+ sync <-= sys->sprint("cannot load %s: %r\n", path);
+ return;
+ }
+ spawn clientmod(mod, ctxt, argv);
+ sync <-= nil;
+}
+
+clientmod(mod: Command, ctxt: ref Draw->Context, argv: list of string)
+{
+ wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+ spawn mod->init(ctxt, argv);
+ buf := array[Sys->ATOMICIO] of byte;
+ n := sys->read(wfd, buf, len buf);
+ sys->print("client process (%s) exited: %s\n", concat(argv), string buf[0:n]);
+}
+
+concat(l: list of string): string
+{
+ if (l == nil)
+ return nil;
+ s := hd l;
+ for (l = tl l; l != nil; l = tl l)
+ s += " " + hd l;
+ return s;
+}
+
+stderr(): ref Sys->FD
+{
+ return sys->fildes(2);
+}
diff --git a/appl/spree/join.m b/appl/spree/join.m
new file mode 100644
index 00000000..0660eb83
--- /dev/null
+++ b/appl/spree/join.m
@@ -0,0 +1,5 @@
+Join: module {
+ PATH: con "/dis/spree/join.dis";
+ join: fn(ctxt: ref Draw->Context, mnt: string, dir: string, joinstr: string): string;
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
diff --git a/appl/spree/joinsession.b b/appl/spree/joinsession.b
new file mode 100644
index 00000000..b953c317
--- /dev/null
+++ b/appl/spree/joinsession.b
@@ -0,0 +1,115 @@
+implement Joinsession;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sh.m";
+include "arg.m";
+include "joinsession.m";
+
+usage()
+{
+ sys->fprint(stderr(), "usage: joinsession [-d mntdir] [-j joinrequest] name\n");
+ raise "fail:usage";
+}
+
+CLIENTDIR: con "/dis/spree/clients";
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+ sys = load Sys Sys->PATH;
+ arg := load Arg Arg->PATH;
+ arg->init(argv);
+ mnt := "/n/remote";
+ joinmsg := "join";
+ while ((opt := arg->opt()) != 0) {
+ case opt {
+ 'd' =>
+ if ((mnt = arg->arg()) == nil)
+ usage();
+ 'j' =>
+ joinmsg = arg->arg();
+ * =>
+ usage();
+ }
+ }
+ argv = arg->argv();
+ if (len argv != 1)
+ usage();
+ arg = nil;
+ e := join(ctxt, mnt, hd argv, joinmsg);
+ if (e != nil) {
+ sys->fprint(stderr(), "startclient: %s\n", e);
+ raise "fail:error";
+ }
+}
+
+join(ctxt: ref Draw->Context, mnt: string, dir: string, joinmsg: string): string
+{
+ if (sys == nil)
+ sys = load Sys Sys->PATH;
+
+ fd := sys->open(mnt + "/" + dir + "/ctl", Sys->ORDWR);
+ if (fd == nil)
+ return sys->sprint("cannot open %s: %r", mnt + "/" + dir + "/ctl");
+ if (joinmsg != nil)
+ if (sys->fprint(fd, "%s", joinmsg) == -1)
+ return sys->sprint("cannot join: %r");
+
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(fd, buf, len buf)) > 0) {
+ (nil, lines) := sys->tokenize(string buf[0:n], "\n");
+ for (; lines != nil; lines = tl lines) {
+ (nil, toks) := sys->tokenize(hd lines, " ");
+ if (len toks > 1 && hd toks == "clienttype") {
+ sync := chan of string;
+ spawn startclient(ctxt, hd tl toks :: mnt :: dir :: tl tl toks, fd, sync);
+ fd = nil;
+ return <-sync;
+ }
+ sys->fprint(stderr(), "startclient: unknown lobby message %s\n", hd lines);
+ }
+ }
+ return "premature EOF";
+}
+
+startclient(ctxt: ref Draw->Context, argv: list of string, fd: ref Sys->FD, sync: chan of string)
+{
+ sys->pctl(Sys->FORKNS|Sys->FORKFD|Sys->NEWPGRP, nil);
+ sys->dup(fd.fd, 0);
+ fd = nil;
+ sys->pctl(Sys->NEWFD, 0 :: 1 :: 2 :: nil);
+
+ # XXX security: weed out slashes
+ path := CLIENTDIR + "/" + hd argv + ".dis";
+ mod := load Command path;
+ if (mod == nil) {
+ sync <-= sys->sprint("cannot load %s: %r\n", path);
+ return;
+ }
+ spawn clientmod(mod, ctxt, argv);
+ sync <-= nil;
+}
+
+clientmod(mod: Command, ctxt: ref Draw->Context, argv: list of string)
+{
+ wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+ spawn mod->init(ctxt, argv);
+ buf := array[Sys->ATOMICIO] of byte;
+ n := sys->read(wfd, buf, len buf);
+ sys->print("client process (%s) exited: %s\n", concat(argv), string buf[0:n]);
+}
+
+concat(l: list of string): string
+{
+ if (l == nil)
+ return nil;
+ s := hd l;
+ for (l = tl l; l != nil; l = tl l)
+ s += " " + hd l;
+ return s;
+}
+
+stderr(): ref Sys->FD
+{
+ return sys->fildes(2);
+}
diff --git a/appl/spree/joinsession.m b/appl/spree/joinsession.m
new file mode 100644
index 00000000..549d20dd
--- /dev/null
+++ b/appl/spree/joinsession.m
@@ -0,0 +1,7 @@
+
+Joinsession: module {
+ PATH: con "/dis/spree/joinsession.dis";
+ join: fn(ctxt: ref Draw->Context, mnt: string, dir: string, join: string): string;
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
diff --git a/appl/spree/lib/allow.b b/appl/spree/lib/allow.b
new file mode 100644
index 00000000..ef088b08
--- /dev/null
+++ b/appl/spree/lib/allow.b
@@ -0,0 +1,194 @@
+implement Allow;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ sets: Sets;
+ Set, set, None: import sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+
+Action: adt {
+ tag: int;
+ member: ref Member;
+ action: string;
+};
+
+actions: list of Action;
+clique: ref Clique;
+
+init(srvmod: Spree, g: ref Clique)
+{
+ sys = load Sys Sys->PATH;
+ sets = load Sets Sets->PATH;
+ (clique, spree) = (g, srvmod);
+}
+
+ILLEGALNAME: con "/"; # illegal char in member names, ahem.
+archive(archiveobj: ref Object)
+{
+ i := 0;
+ for (al := actions; al != nil; al = tl al) {
+ a := hd al;
+ pname: string;
+ if (a.member != nil)
+ pname = a.member.name;
+ else
+ pname = ILLEGALNAME;
+ archiveobj.setattr(
+ "allow" + string i++,
+ sys->sprint("%d %s %s", a.tag, pname, a.action),
+ None
+ );
+ }
+}
+
+unarchive(archiveobj: ref Object)
+{
+ for (i := 0; (s := archiveobj.getattr("allow" + string i)) != nil; i++) {
+ (n, toks) := sys->tokenize(s, " ");
+ p: ref Member = nil;
+ if (hd tl toks != ILLEGALNAME) {
+ # if the member is no longer around, ignore the action. XXX do we still need to do this?
+ if ((p = clique.membernamed(hd tl toks)) == nil)
+ continue;
+ }
+ sys->print("allow: adding action %d, %ux, %s\n", int hd toks, p, concat(tl tl toks));
+ actions = Action(int hd toks, p, concat(tl tl toks)) :: actions;
+ }
+}
+
+add(tag: int, member: ref Member, action: string)
+{
+# sys->print("allow: add %d, member %ux, action: %s\n", tag, member, action);
+ actions = (tag, member, action) :: actions;
+}
+
+del(tag: int, member: ref Member)
+{
+# sys->print("allow: del %d\n", tag);
+ na: list of Action;
+ for (a := actions; a != nil; a = tl a) {
+ action := hd a;
+ if (action.tag == tag && (member == nil || action.member == member))
+ continue;
+ na = action :: na;
+ }
+ actions = na;
+}
+
+action(member: ref Member, cmd: string): (string, int, list of string)
+{
+ for (al := actions; al != nil; al = tl al) {
+ a := hd al;
+ if (a.member == nil || a.member == member) {
+ (e, v) := match(member, a.action, cmd);
+ if (e != nil || v != nil)
+ return (e, a.tag, v);
+ }
+ }
+ return ("you can't do that", -1, nil);
+}
+
+match(member: ref Member, pat, action: string): (string, list of string)
+{
+# sys->print("allow: matching pat: '%s' against action '%s'\n", pat, action);
+ toks: list of string;
+ na := len action;
+ if (na > 0 && action[na - 1] == '\n')
+ na--;
+
+ (i, j) := (0, 0);
+ for (;;) {
+ for (; i < len pat; i++)
+ if (pat[i] != ' ')
+ break;
+ for (; j < na; j++)
+ if (action[j] != ' ')
+ break;
+ for (i1 := i; i1 < len pat; i1++)
+ if (pat[i1] == ' ')
+ break;
+ for (j1 := j; j1 < na; j1++)
+ if (action[j1] == ' ')
+ break;
+ if (i == i1) {
+ if (j == j1)
+ break;
+ return (nil, nil);
+ }
+ if (j == j1) {
+ if (pat == "&")
+ break;
+ return (nil, nil);
+ }
+ pw := pat[i : i1];
+ w := action[j : j1];
+ case pw[0] {
+ '*' =>
+ toks = w :: toks;
+ '&' =>
+ toks = w :: toks;
+ pat = "&";
+ i1 = 0;
+ '%' =>
+ (ok, nw) := checkformat(member, pw[1], w);
+ if (!ok)
+ return ("invalid field value", nil);
+ toks = nw :: toks;
+ * =>
+ if (w != pw)
+ return (nil, nil);
+ toks = w :: toks;
+ }
+ (i, j) = (i1, j1);
+ }
+ return (nil, revs(toks));
+}
+
+revs(l: list of string): list of string
+{
+ m: list of string;
+ for (; l != nil; l = tl l)
+ m = hd l :: m;
+ return m;
+}
+
+checkformat(p: ref Member, fmt: int, w: string): (int, string)
+{
+ case fmt {
+ 'o' =>
+ # object id
+ if (isnum(w) && (o := p.obj(int w)) != nil)
+ return (1, string o.id);
+ 'd' =>
+ # integer
+ if (isnum(w))
+ return (1, w);
+ 'p' =>
+ # member id
+ if (isnum(w) && (member := clique.member(int w)) != nil)
+ return (1, w);
+ }
+ return (0, nil);
+}
+
+isnum(w: string): int
+{
+ # XXX lazy for the time being...
+ if (w != nil && ((w[0] >= '0' && w[0] <= '9') || w[0] == '-'))
+ return 1;
+ return 0;
+}
+
+concat(v: list of string): string
+{
+ if (v == nil)
+ return nil;
+ s := hd v;
+ for (v = tl v; v != nil; v = tl v)
+ s += " " + hd v;
+ return s;
+}
diff --git a/appl/spree/lib/allow.m b/appl/spree/lib/allow.m
new file mode 100644
index 00000000..98882091
--- /dev/null
+++ b/appl/spree/lib/allow.m
@@ -0,0 +1,9 @@
+Allow: module {
+ PATH: con "/dis/spree/lib/allow.dis";
+ init: fn(srvmod: Spree, g: ref Spree->Clique);
+ add: fn(tag: int, member: ref Spree->Member, action: string);
+ del: fn(tag: int, member: ref Spree->Member);
+ action: fn(member: ref Spree->Member, cmd: string): (string, int, list of string);
+ archive: fn(archiveobj: ref Object);
+ unarchive: fn(archiveobj: ref Object);
+};
diff --git a/appl/spree/lib/base64.b b/appl/spree/lib/base64.b
new file mode 100644
index 00000000..c8381467
--- /dev/null
+++ b/appl/spree/lib/base64.b
@@ -0,0 +1,72 @@
+implement Base64;
+include "base64.m";
+
+PADCH: con '=';
+encode(b: array of byte): string
+{
+ chmap := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
+ "abcdefghijklmnopqrstuvwxyz0123456789+/";
+ r := "";
+ blen := len b;
+ full := (blen + 2)/ 3;
+ rplen := (4*blen + 2) / 3;
+ ip := 0;
+ rp := 0;
+ for (i:=0; i<full; i++) {
+ word := 0;
+ for (j:=2; j>=0; j--)
+ if (ip < blen)
+ word = word | int b[ip++] << 8*j;
+ for (l:=3; l>=0; l--)
+ if (rp < rplen)
+ r[rp++] = chmap[(word >> (6*l)) & 16r3f];
+ else
+ r[rp++] = PADCH;
+ }
+ return r;
+}
+
+# Decode a base 64 string to a byte stream
+# Must be a multiple of 4 characters in length
+decode(s: string): array of byte
+{
+
+ tch: int;
+ slen := len s;
+ rlen := (3*slen+3)/4;
+ if (slen >= 4 && s[slen-1] == PADCH)
+ rlen--;
+ if (slen >= 4 && s[slen-2] == PADCH)
+ rlen--;
+ r := array[rlen] of byte;
+ full := slen / 4;
+ sp := 0;
+ rp := 0;
+ for (i:=0; i<full; i++) {
+ word := 0;
+ for (j:=0; j<4; j++) {
+ ch := s[sp++];
+ case ch {
+ 'A' to 'Z' =>
+ tch = ch - 'A';
+ 'a' to 'z' =>
+ tch = ch - 'a' + 26;
+ '0' to '9' =>
+ tch = ch - '0' + 52;
+ '+' =>
+ tch = 62;
+ '/' =>
+ tch = 63;
+ * =>
+ tch = 0;
+ }
+ word = (word << 6) | tch;
+ }
+ for (l:=2; l>=0; l--)
+ if (rp < rlen)
+ r[rp++] = byte( (word >> 8*l) & 16rff);
+
+ }
+ return r;
+}
+
diff --git a/appl/spree/lib/base64.m b/appl/spree/lib/base64.m
new file mode 100644
index 00000000..1325cae2
--- /dev/null
+++ b/appl/spree/lib/base64.m
@@ -0,0 +1,5 @@
+Base64: module {
+ PATH : con "/dis/spree/lib/base64.dis";
+ encode : fn(b : array of byte) : string;
+ decode : fn(s : string) : array of byte;
+};
diff --git a/appl/spree/lib/cardlib.b b/appl/spree/lib/cardlib.b
new file mode 100644
index 00000000..67c4918b
--- /dev/null
+++ b/appl/spree/lib/cardlib.b
@@ -0,0 +1,917 @@
+implement Cardlib;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ sets: Sets;
+ Set, set, A, B, All, None: import sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member, rand: import spree;
+include "objstore.m";
+ objstore: Objstore;
+include "cardlib.m";
+
+MAXPLAYERS: con 4;
+
+Layobject: adt {
+ lay: ref Object;
+ name: string;
+ packopts: int;
+ pick {
+ Obj =>
+ obj: ref Object; # nil if it's a frame
+ Frame =>
+ facing: int; # only valid if for frames
+ }
+};
+
+clique: ref Clique;
+cmembers: array of ref Cmember;
+cpids := array[8] of list of ref Cmember;
+
+# XXX first string is unnecessary as it's held in the Layobject anyway?
+layouts := array[17] of list of (string, ref Layout, ref Layobject);
+maxlayid := 1;
+cmemberid := 1;
+
+archiveobjs: array of list of (string, ref Object);
+
+defaultrank := array[13] of {12, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+defaultsuitrank := array[] of {CLUBS => 0, DIAMONDS => 1, HEARTS => 2, SPADES => 3};
+
+table := array[] of {
+ 0 => array[] of {
+ (-1, dTOP|EXPAND, dBOTTOM, dTOP),
+ },
+ 1 => array [] of {
+ (0, dBOTTOM|FILLX, dBOTTOM, dTOP),
+ (-1, dTOP|EXPAND, dBOTTOM, dTOP),
+ },
+ 2 => array[] of {
+ (0, dBOTTOM|FILLX, dBOTTOM, dTOP),
+ (1, dTOP|FILLX, dTOP, dBOTTOM),
+ (-1, dTOP|EXPAND, dBOTTOM, dTOP)
+ },
+ 3 => array[] of {
+ (2, dRIGHT|FILLY, dRIGHT, dLEFT),
+ (0, dBOTTOM|FILLX, dBOTTOM, dTOP),
+ (1, dTOP|FILLX, dTOP, dBOTTOM),
+ (-1, dRIGHT|EXPAND, dBOTTOM, dTOP)
+ },
+ 4 => array[] of {
+ (1, dLEFT|FILLY, dLEFT, dRIGHT),
+ (3, dRIGHT|FILLY, dRIGHT, dLEFT),
+ (0, dBOTTOM|FILLX, dBOTTOM, dTOP),
+ (2, dTOP|FILLX, dTOP, dBOTTOM),
+ (-1, dRIGHT|EXPAND, dBOTTOM, dTOP)
+ },
+};
+
+
+init(mod: Spree, g: ref Clique)
+{
+ sys = load Sys Sys->PATH;
+ sets = load Sets Sets->PATH;
+ if (sets == nil)
+ panic(sys->sprint("cannot load %s: %r", Sets->PATH));
+ objstore = load Objstore Objstore->PATH;
+ if (objstore == nil)
+ panic(sys->sprint("cannot load %s: %r", Objstore->PATH));
+ objstore->init(mod, g);
+ clique = g;
+ spree = mod;
+}
+
+archive(): ref Object
+{
+ for (i := 0; i < len cmembers; i++) {
+ cp := cmembers[i];
+ setarchivename(cp.obj, "member" + string i);
+ setarchivename(cp.layout.lay, "layout" + string i);
+ sel := cp.sel;
+ if (sel.stack != nil)
+ setarchivename(sel.stack, "sel" + string i);
+ }
+ for (i = 0; i < len layouts; i++) {
+ for (ll := layouts[i]; ll != nil; ll = tl ll) {
+ (name, lay, layobj) := hd ll;
+ if (name != nil)
+ layobj.lay.setattr("layname", name, None);
+ pick l := layobj {
+ Frame =>
+ l.lay.setattr("facing", sides[l.facing], None);
+ Obj =>
+ setarchivename(l.obj, "layid" + l.obj.getattr("layid"));
+ }
+ }
+ }
+ # XXX should archive layouts that aren't particular to a member.
+ archiveobj := clique.newobject(nil, None, "archive");
+ setarchivename(archiveobj, "archive");
+ archiveobj.setattr("maxlayid", string maxlayid, None);
+ archiveobj.setattr("cmemberid", string cmemberid, None);
+ return archiveobj;
+}
+
+setarchivename(o: ref Object, name: string)
+{
+ objstore->setname(o, name);
+}
+
+getarchiveobj(name: string): ref Object
+{
+ return objstore->get(name);
+}
+
+archivearray(a: array of ref Object, name: string)
+{
+ for (i := 0; i < len a; i++)
+ objstore->setname(a[i], name + string i);
+}
+
+getarchivearray(name: string): array of ref Object
+{
+ l: list of ref Object;
+ for (i := 0; ; i++) {
+ o := objstore->get(name + string i);
+ if (o == nil)
+ break;
+ l = o :: l;
+ }
+ a := array[i] of ref Object;
+ for (; l != nil; l = tl l)
+ a[--i] = hd l;
+ return a;
+}
+
+unarchive(): ref Object
+{
+ objstore->unarchive();
+ archiveobj := getarchiveobj("archive");
+ cpl: list of ref Cmember;
+ for (i := 0; (o := getarchiveobj("member" + string i)) != nil; i++) {
+ cp := ref Cmember(
+ i,
+ int o.getattr("id"),
+ clique.membernamed(o.getattr("name")),
+ o,
+ ref Layout(getarchiveobj("layout" + string i)),
+ ref Selection(getarchiveobj("sel" + string i), -1, 1, (0, 0), nil)
+ );
+ cp.sel.ownerid = cp.id;
+ sel := cp.sel;
+ if (sel.stack != nil && (selstr := sel.stack.getattr("sel")) != nil) {
+ (n, val) := sys->tokenize(selstr, " ");
+ if (tl val != nil && hd tl val == "-")
+ (sel.r.start, sel.r.end) = (int hd val, int hd tl tl val);
+ else {
+ idxl: list of int;
+ sel.isrange = 0;
+ for (; val != nil; val = tl val)
+ idxl = int hd val :: idxl;
+ sel.idxl = idxl;
+ }
+ }
+ lay := cp.layout.lay;
+ # there should be exactly one child, of type "layframe"
+ if (len lay.children != 1 || lay.children[0].objtype != "layframe")
+ panic("invalid layout");
+ x := strhash(nil, len layouts);
+ layouts[x] = (nil, cp.layout, obj2layobj(lay.children[0])) :: layouts[x];
+ unarchivelayoutobj(cp.layout, lay.children[0]);
+ cpl = cp :: cpl;
+ }
+ cmembers = array[len cpl] of ref Cmember;
+ for (; cpl != nil; cpl = tl cpl) {
+ cp := hd cpl;
+ cmembers[cp.ord] = cp;
+ idx := cp.id % len cpids;
+ cpids[idx] = cp :: cpids[idx];
+ }
+
+ maxlayid = int archiveobj.getattr("maxlayid");
+ cmemberid = int archiveobj.getattr("cmemberid");
+ return archiveobj;
+}
+
+unarchivelayoutobj(layout: ref Layout, o: ref Object)
+{
+ for (i := 0; i < len o.children; i++) {
+ child := o.children[i];
+ layobj := obj2layobj(child);
+ if (layobj.name != nil) {
+ x := strhash(layobj.name, len layouts);
+ layouts[x] = (layobj.name, layout, layobj) :: layouts[x];
+ }
+ if (tagof(layobj) == tagof(Layobject.Frame))
+ unarchivelayoutobj(layout, child);
+ }
+}
+
+obj2layobj(o: ref Object): ref Layobject
+{
+ case o.objtype {
+ "layframe" =>
+ return ref Layobject.Frame(
+ o,
+ o.getattr("layname"),
+ s2packopts(o.getattr("opts")),
+ searchopt(sides, o.getattr("facing"))
+ );
+ "layobj" =>
+ return ref Layobject.Obj(
+ o,
+ o.getattr("layname"),
+ s2packopts(o.getattr("opts")),
+ getarchiveobj("layid" + o.getattr("layid"))
+ );
+ * =>
+ panic("invalid layobject found, of type '" + o.objtype + "'");
+ return nil;
+ }
+}
+
+Cmember.join(member: ref Member, ord: int): ref Cmember
+{
+ cmembers = (array[len cmembers + 1] of ref Cmember)[0:] = cmembers;
+ if (ord == -1)
+ ord = len cmembers - 1;
+ else {
+ cmembers[ord + 1:] = cmembers[ord:len cmembers - 1];
+ for (i := ord + 1; i < len cmembers; i++)
+ cmembers[i].ord = i;
+ }
+ cp := cmembers[ord] = ref Cmember(ord, cmemberid++, member, nil, nil, nil);
+ cp.obj = clique.newobject(nil, All, "member");
+ cp.obj.setattr("id", string cp.id, All);
+ cp.obj.setattr("name", member.name, All);
+ cp.obj.setattr("you", string cp.id, None.add(member.id));
+ cp.obj.setattr("cliquetitle", clique.fname, All);
+ cp.layout = newlayout(cp.obj, None.add(member.id));
+ cp.sel = ref Selection(nil, cp.id, 1, (0, 0), nil);
+
+ idx := cp.id % len cpids;
+ cpids[idx] = cp :: cpids[idx];
+ return cp;
+}
+
+Cmember.find(p: ref Member): ref Cmember
+{
+ id := p.id;
+ for (i := 0; i < len cmembers; i++)
+ if (cmembers[i].p.id == id)
+ return cmembers[i];
+ return nil;
+}
+
+Cmember.index(ord: int): ref Cmember
+{
+ if (ord < 0 || ord >= len cmembers)
+ return nil;
+ return cmembers[ord];
+}
+
+Cmember.next(cp: self ref Cmember, fwd: int): ref Cmember
+{
+ if (!fwd)
+ return cp.prev(1);
+ x := cp.ord + 1;
+ if (x >= len cmembers)
+ x = 0;
+ return cmembers[x];
+}
+
+Cmember.prev(cp: self ref Cmember, fwd: int): ref Cmember
+{
+ if (!fwd)
+ return cp.next(1);
+ x := cp.ord - 1;
+ if (x < 0)
+ x = len cmembers - 1;
+ return cmembers[x];
+}
+
+Cmember.leave(cp: self ref Cmember)
+{
+ ord := cp.ord;
+ cmembers[ord] = nil;
+ cmembers[ord:] = cmembers[ord + 1:];
+ cmembers[len cmembers - 1] = nil;
+ cmembers = cmembers[0:len cmembers - 1];
+ for (i := ord; i < len cmembers; i++)
+ cmembers[i].ord = i;
+ cp.obj.delete();
+ dellayout(cp.layout);
+ cp.layout = nil;
+ idx := cp.id % len cpids;
+ l: list of ref Cmember;
+ ll := cpids[idx];
+ for (; ll != nil; ll = tl ll)
+ if (hd ll != cp)
+ l = hd ll :: l;
+ cpids[idx] = l;
+ cp.ord = -1;
+}
+
+Cmember.findid(id: int): ref Cmember
+{
+ for (l := cpids[id % len cpids]; l != nil; l = tl l)
+ if ((hd l).id == id)
+ return hd l;
+ return nil;
+}
+
+newstack(parent: ref Object, owner: ref Member, spec: Stackspec): ref Object
+{
+ vis := All;
+ if (spec.conceal) {
+ vis = None;
+ if (owner != nil)
+ vis = vis.add(owner.id);
+ }
+ o := clique.newobject(parent, vis, "stack");
+ o.setattr("maxcards", string spec.maxcards, All);
+ o.setattr("style", spec.style, All);
+
+ # XXX provide some means for this to contain the member's name?
+ o.setattr("title", spec.title, All);
+ return o;
+}
+
+makecard(deck: ref Object, c: Card, rear: string): ref Object
+{
+ card := clique.newobject(deck, None, "card");
+ card.setattr("face", string c.face, All);
+ vis := None;
+ if(c.face)
+ vis = All;
+ card.setattr("number", string (c.number * 4 + c.suit), vis);
+ if (rear != nil)
+ card.setattr("rear", rear, All);
+ return card;
+}
+
+makecards(deck: ref Object, r: Range, rear: string)
+{
+ for (i := r.start; i < r.end; i++)
+ for(suit := 0; suit < 4; suit++)
+ makecard(deck, (suit, i, 0), rear);
+}
+
+# deal n cards to each member, if possible.
+# deal in chunks for efficiency.
+# if accuracy is required (e.g. dealing from an unshuffled
+# deck containing known cards) then this'll have to change.
+deal(deck: ref Object, n: int, stacks: array of ref Object, first: int)
+{
+ ncards := len deck.children;
+ ord := 0;
+ permember := n;
+ leftover := 0;
+ if (n * len stacks > ncards) {
+ # if trying to deal more cards than we've got,
+ # deal all that we've got, distributing the remainder fairly.
+ permember = ncards / len stacks;
+ leftover = ncards % len stacks;
+ }
+ for (i := 0; i < len stacks; i++) {
+ n = permember;
+ if (leftover > 0) {
+ n++;
+ leftover--;
+ }
+ priv := stacks[(first + i) % len stacks];
+ deck.transfer((ncards - n, ncards), priv, len priv.children);
+ priv.setattr("n", string (int priv.getattr("n") + n), All);
+ # make cards visible to member
+ for (j := len priv.children - n; j < len priv.children; j++)
+ setface(priv.children[j], 1);
+
+ ncards -= n;
+ }
+}
+
+setface(card: ref Object, face: int)
+{
+ # XXX check parent stack style and if it's a pile,
+ # only expose a face up card at the top.
+
+ card.setattr("face", string face, All);
+ if (face)
+ card.setattrvisibility("number", All);
+ else
+ card.setattrvisibility("number", None);
+}
+
+nmembers(): int
+{
+ return len cmembers;
+}
+
+getcard(card: ref Object): Card
+{
+ n := int card.getattr("number");
+ (suit, num) := (n % 4, n / 4);
+ return Card(suit, num, int card.getattr("face"));
+}
+
+getcards(stack: ref Object): array of Card
+{
+ a := array[len stack.children] of Card;
+ for (i := 0; i < len a; i++)
+ a[i] = getcard(stack.children[i]);
+ return a;
+}
+
+discard(stk, pile: ref Object, facedown: int)
+{
+ n := len stk.children;
+ if (facedown)
+ for (i := 0; i < n; i++)
+ setface(stk.children[i], 0);
+ stk.transfer((0, n), pile, len pile.children);
+}
+
+# shuffle children into a random order. first we make all the children
+# invisible (which will cause them to be deleted in the clients) then
+# shuffle to our heart's content, and make visible again...
+shuffle(o: ref Object)
+{
+ ovis := o.visibility;
+ o.setvisibility(None);
+ a := o.children;
+ n := len a;
+ for (i := 0; i < n; i++) {
+ j := i + rand(n - i);
+ (a[i], a[j]) = (a[j], a[i]);
+ }
+ o.setvisibility(ovis);
+}
+
+sort(o: ref Object, rank, suitrank: array of int)
+{
+ if (rank == nil)
+ rank = defaultrank;
+ if (suitrank == nil)
+ suitrank = defaultsuitrank;
+ ovis := o.visibility;
+ o.setvisibility(None);
+ cardmergesort(o.children, array[len o.children] of ref Object, rank, suitrank);
+ o.setvisibility(ovis);
+}
+
+cardcmp(a, b: ref Object, rank, suitrank: array of int): int
+{
+ c1 := getcard(a);
+ c2 := getcard(b);
+ if (suitrank[c1.suit] != suitrank[c2.suit])
+ return suitrank[c1.suit] - suitrank[c2.suit];
+ return rank[c1.number] - rank[c2.number];
+}
+
+cardmergesort(a, b: array of ref Object, rank, suitrank: array of int)
+{
+ r := len a;
+ if (r > 1) {
+ m := (r-1)/2 + 1;
+ cardmergesort(a[0:m], b[0:m], rank, suitrank);
+ cardmergesort(a[m:], b[m:], rank, suitrank);
+ b[0:] = a;
+ for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+ if (cardcmp(b[i], b[j], rank, suitrank) > 0)
+ a[k] = b[j++];
+ else
+ a[k] = b[i++];
+ }
+ if (i < m)
+ a[k:] = b[i:m];
+ else if (j < r)
+ a[k:] = b[j:r];
+ }
+}
+
+# reverse and flip all cards in stack.
+flip(stack: ref Object)
+{
+ ovis := stack.visibility;
+ stack.setvisibility(None);
+ a := stack.children;
+ (n, m) := (len a, len a / 2);
+ for (i := 0; i < m; i++) {
+ j := n - i - 1;
+ (a[i], a[j]) = (a[j], a[i]);
+ }
+ for (i = 0; i < n; i++)
+ setface(a[i], !int a[i].getattr("face"));
+ stack.setvisibility(ovis);
+}
+
+selection(stack: ref Object): ref Selection
+{
+ if ((owner := stack.getattr("owner")) != nil &&
+ (cp := Cmember.findid(int owner)) != nil)
+ return cp.sel;
+ return nil;
+}
+
+Selection.set(sel: self ref Selection, stack: ref Object)
+{
+ if (stack == sel.stack)
+ return;
+ if (stack != nil) {
+ oldowner := stack.getattr("owner");
+ if (oldowner != nil) {
+ oldcp := Cmember.findid(int oldowner);
+ if (oldcp != nil)
+ oldcp.sel.set(nil);
+ }
+ }
+ if (sel.stack != nil)
+ sel.stack.setattr("owner", nil, All);
+ sel.stack = stack;
+ sel.isrange = 1;
+ sel.r = (0, 0);
+ sel.idxl = nil;
+ setsel(sel);
+}
+
+Selection.setexcl(sel: self ref Selection, stack: ref Object): int
+{
+ if (stack != nil && (oldowner := stack.getattr("owner")) != nil)
+ if ((cp := Cmember.findid(int oldowner)) != nil && !cp.sel.isempty())
+ return 0;
+ sel.set(stack);
+ return 1;
+}
+
+Selection.owner(sel: self ref Selection): ref Cmember
+{
+ return Cmember.findid(sel.ownerid);
+}
+
+Selection.setrange(sel: self ref Selection, r: Range)
+{
+ if (!sel.isrange) {
+ sel.idxl = nil;
+ sel.isrange = 1;
+ }
+ sel.r = r;
+ setsel(sel);
+}
+
+Selection.addindex(sel: self ref Selection, i: int)
+{
+ if (sel.isrange) {
+ sel.r = (0, 0);
+ sel.isrange = 0;
+ }
+ ll: list of int;
+ for (l := sel.idxl; l != nil; l = tl l) {
+ if (hd l >= i)
+ break;
+ ll = hd l :: ll;
+ }
+ if (l != nil && hd l == i)
+ return;
+ l = i :: l;
+ for (; ll != nil; ll = tl ll)
+ l = hd ll :: l;
+ sel.idxl = l;
+ setsel(sel);
+}
+
+Selection.delindex(sel: self ref Selection, i: int)
+{
+ if (sel.isrange) {
+ sys->print("cardlib: delindex from range-type selection\n");
+ return;
+ }
+ ll: list of int;
+ for (l := sel.idxl; l != nil; l = tl l) {
+ if (hd l == i) {
+ l = tl l;
+ break;
+ }
+ ll = hd l :: ll;
+ }
+ for (; ll != nil; ll = tl ll)
+ l = hd ll :: l;
+ sel.idxl = l;
+ setsel(sel);
+}
+
+Selection.isempty(sel: self ref Selection): int
+{
+ if (sel.stack == nil)
+ return 1;
+ if (sel.isrange)
+ return sel.r.start == sel.r.end;
+ return sel.idxl == nil;
+}
+
+Selection.isset(sel: self ref Selection, index: int): int
+{
+ if (sel.isrange)
+ return index >= sel.r.start && index < sel.r.end;
+ for (l := sel.idxl; l != nil; l = tl l)
+ if (hd l == index)
+ return 1;
+ return 0;
+}
+
+Selection.transfer(sel: self ref Selection, dst: ref Object, index: int)
+{
+ if (sel.isempty())
+ return;
+ src := sel.stack;
+ if (sel.isrange) {
+ r := sel.r;
+ sel.set(nil);
+ src.transfer(r, dst, index);
+ } else {
+ if (sel.stack == dst) {
+ sys->print("cardlib: cannot move multisel to same stack\n");
+ return;
+ }
+ xl := l := sel.idxl;
+ sel.set(nil);
+ rl: list of Range;
+ for (; l != nil; l = tl l) {
+ r := Range(hd l, hd l);
+ last := l;
+ # concatenate adjacent items, for efficiency.
+ for (l = tl l; l != nil; (last, l) = (l, tl l)) {
+ if (hd l != r.end + 1)
+ break;
+ r.end = hd l;
+ }
+ rl = (r.start, r.end + 1) :: rl;
+ l = last;
+ }
+ # do ranges in reverse, so that later ranges
+ # aren't affected by earlier ones.
+ if (index == -1)
+ index = len dst.children;
+ for (; rl != nil; rl = tl rl)
+ src.transfer(hd rl, dst, index);
+ }
+}
+
+setsel(sel: ref Selection)
+{
+ if (sel.stack == nil)
+ return;
+ s := "";
+ if (sel.isrange) {
+ if (sel.r.end > sel.r.start)
+ s = string sel.r.start + " - " + string sel.r.end;
+ } else {
+ if (sel.idxl != nil) {
+ s = string hd sel.idxl;
+ for (l := tl sel.idxl; l != nil; l = tl l)
+ s += " " + string hd l;
+ }
+ }
+ if (s != nil)
+ sel.stack.setattr("owner", string sel.owner().id, All);
+ else
+ sel.stack.setattr("owner", nil, All);
+ vis := None.add(sel.owner().p.id);
+ sel.stack.setattr("sel", s, vis);
+ sel.stack.setattrvisibility("sel", vis);
+}
+
+newlayout(parent: ref Object, vis: Set): ref Layout
+{
+ l := ref Layout(clique.newobject(parent, vis, "layout"));
+ x := strhash(nil, len layouts);
+ layobj := ref Layobject.Frame(nil, "", dTOP|EXPAND|FILLX|FILLY, dTOP);
+ layobj.lay = clique.newobject(l.lay, All, "layframe");
+ layobj.lay.setattr("opts", packopts2s(layobj.packopts), All);
+ layouts[x] = (nil, l, layobj) :: layouts[x];
+# sys->print("[%d] => ('%s', %ux, %ux) (new layout)\n", x, "", l, layobj);
+ return l;
+}
+
+addlayframe(name, parent: string, layout: ref Layout, packopts: int, facing: int)
+{
+# sys->print("addlayframe('%s', %ux, name: %s\n", parent, layout, name);
+ addlay(parent, layout, ref Layobject.Frame(nil, name, packopts, facing));
+}
+
+addlayobj(name, parent: string, layout: ref Layout, packopts: int, obj: ref Object)
+{
+# sys->print("addlayobj('%s', %ux, name: %s, obj %d\n", parent, layout, name, obj.id);
+ addlay(parent, layout, ref Layobject.Obj(nil, name, packopts, obj));
+}
+
+addlay(parent: string, layout: ref Layout, layobj: ref Layobject)
+{
+ a := layouts;
+ name := layobj.name;
+ x := strhash(name, len a);
+ added := 0;
+ for (nl := a[strhash(parent, len a)]; nl != nil; nl = tl nl) {
+ (s, lay, parentlay) := hd nl;
+ if (s == parent && (layout == nil || layout == lay)) {
+ pick p := parentlay {
+ Obj =>
+ sys->fprint(sys->fildes(2),
+ "cardlib: cannot add layout to non-frame: %d\n", p.obj.id);
+ Frame =>
+ nlayobj := copylayobj(layobj);
+ nlayobj.packopts = packoptsfacing(nlayobj.packopts, p.facing);
+ o: ref Object;
+ pick lo := nlayobj {
+ Obj =>
+ o = clique.newobject(p.lay, All, "layobj");
+ id := lo.obj.getattr("layid");
+ if (id == nil) {
+ id = string maxlayid++;
+ lo.obj.setattr("layid", id, All);
+ }
+ o.setattr("layid", id, All);
+ Frame =>
+ o = clique.newobject(p.lay, All, "layframe");
+ lo.facing = (lo.facing + p.facing) % 4;
+ }
+ o.setattr("opts", packopts2s(nlayobj.packopts), All);
+ nlayobj.lay = o;
+ if (name != nil)
+ a[x] = (name, lay, nlayobj) :: a[x];
+ added++;
+ }
+ }
+ }
+ if (added == 0)
+ sys->print("no parent found, adding '%s', parent '%s', layout %ux\n",
+ layobj.name, parent, layout);
+# sys->print("%d new entries\n", added);
+}
+
+maketable(parent: string)
+{
+ # make a table for all current members.
+ plcount := len cmembers;
+ packopts := table[plcount];
+ for (i := 0; i < plcount; i++) {
+ layout := cmembers[i].layout;
+ for (j := 0; j < len packopts; j++) {
+ (ord, outer, inner, facing) := packopts[j];
+ name := "public";
+ if (ord != -1)
+ name = "p" + string ((ord + i) % plcount);
+ addlayframe("@" + name, parent, layout, outer, dTOP);
+ addlayframe(name, "@" + name, layout, inner, facing);
+ }
+ }
+}
+
+dellay(name: string, layout: ref Layout)
+{
+ a := layouts;
+ x := strhash(name, len a);
+ rl: list of (string, ref Layout, ref Layobject);
+ for (nl := a[x]; nl != nil; nl = tl nl) {
+ (s, lay, layobj) := hd nl;
+ if (s != name || (layout != nil && layout != lay))
+ rl = hd nl :: rl;
+ }
+ a[x] = rl;
+}
+
+dellayout(layout: ref Layout)
+{
+ for (i := 0; i < len layouts; i++) {
+ ll: list of (string, ref Layout, ref Layobject);
+ for (nl := layouts[i]; nl != nil; nl = tl nl) {
+ (s, lay, layobj) := hd nl;
+ if (lay != layout)
+ ll = hd nl :: ll;
+ }
+ layouts[i] = ll;
+ }
+}
+
+copylayobj(obj: ref Layobject): ref Layobject
+{
+ pick o := obj {
+ Frame =>
+ return ref *o;
+ Obj =>
+ return ref *o;
+ }
+ return nil;
+}
+
+packoptsfacing(opts, facing: int): int
+{
+ if (facing == dTOP)
+ return opts;
+ nopts := 0;
+
+ # 4 directions
+ nopts |= (facing + (opts & dMASK)) % 4;
+
+ # 2 orientations
+ nopts |= ((facing + ((opts & oMASK) >> oSHIFT)) % 4) << oSHIFT;
+
+ # 8 anchorpoints (+ centre)
+ a := (opts & aMASK);
+ if (a != aCENTRE)
+ a = ((((a >> aSHIFT) - 1 + facing * 2) % 8) + 1) << aSHIFT;
+ nopts |= a;
+
+ # two fill options
+ if (facing % 2) {
+ if (opts & FILLX)
+ nopts |= FILLY;
+ if (opts & FILLY)
+ nopts |= FILLX;
+ } else
+ nopts |= (opts & (FILLX | FILLY));
+
+ nopts |= (opts & EXPAND);
+ return nopts;
+}
+
+# these arrays are dependent on the ordering of
+# the relevant constants defined in cardlib.m
+
+sides := array[] of {"top", "left", "bottom", "right"};
+anchors := array[] of {"centre", "n", "nw", "w", "sw", "s", "se", "e", "ne"};
+orientations := array[] of {"right", "up", "left", "down"};
+fills := array[] of {"none", "x", "y", "both"};
+
+packopts2s(opts: int): string
+{
+ s := orientations[(opts & oMASK) >> oSHIFT] +
+ " -side " + sides[opts & dMASK];
+ if ((opts & aMASK) != aCENTRE)
+ s += " -anchor " + anchors[(opts & aMASK) >> aSHIFT];
+ if (opts & EXPAND)
+ s += " -expand 1";
+ if (opts & (FILLX | FILLY))
+ s += " -fill " + fills[(opts & FILLMASK) >> FILLSHIFT];
+ return s;
+}
+
+searchopt(a: array of string, s: string): int
+{
+ for (i := 0; i < len a; i++)
+ if (a[i] == s)
+ return i;
+ panic("unknown pack option '" + s + "'");
+ return 0;
+}
+
+s2packopts(s: string): int
+{
+ (nil, toks) := sys->tokenize(s, " ");
+ if (toks == nil)
+ panic("invalid packopts: " + s);
+ p := searchopt(orientations, hd toks) << oSHIFT;
+ for (toks = tl toks; toks != nil; toks = tl tl toks) {
+ if (tl toks == nil)
+ panic("invalid packopts: " + s);
+ arg := hd tl toks;
+ case hd toks {
+ "-anchor" =>
+ p |= searchopt(anchors, arg) << aSHIFT;
+ "-fill" =>
+ p |= searchopt(fills, arg) << FILLSHIFT;
+ "-side" =>
+ p |= searchopt(sides, arg) << dSHIFT;
+ "-expand" =>
+ if (int hd tl toks)
+ p |= EXPAND;
+ * =>
+ panic("unknown pack option: " + hd toks);
+ }
+ }
+ return p;
+}
+
+panic(e: string)
+{
+ sys->fprint(sys->fildes(2), "cardlib panic: %s\n", e);
+ raise "panic";
+}
+
+assert(b: int, err: string)
+{
+ if (b == 0)
+ raise "parse:" + err;
+}
+
+# from Aho Hopcroft Ullman
+strhash(s: string, n: int): int
+{
+ h := 0;
+ m := len s;
+ for(i := 0; i<m; i++){
+ h = 65599 * h + s[i];
+ }
+ return (h & 16r7fffffff) % n;
+}
diff --git a/appl/spree/lib/cardlib.m b/appl/spree/lib/cardlib.m
new file mode 100644
index 00000000..94c05002
--- /dev/null
+++ b/appl/spree/lib/cardlib.m
@@ -0,0 +1,114 @@
+Cardlib: module {
+ PATH: con "/dis/spree/lib/cardlib.dis";
+
+ Layout: adt {
+ lay: ref Spree->Object; # the actual layout object
+ };
+
+ Stackspec: adt {
+ style: string;
+ maxcards: int;
+ conceal: int;
+ title: string;
+ };
+
+ Card: adt {
+ suit: int;
+ number: int;
+ face: int;
+ };
+
+ # a member currently playing
+ Cmember: adt {
+ ord: int;
+ id: int;
+ p: ref Spree->Member;
+ obj: ref Spree->Object;
+ layout: ref Layout;
+ sel: ref Selection;
+
+ join: fn(p: ref Spree->Member, ord: int): ref Cmember;
+ index: fn(ord: int): ref Cmember;
+ find: fn(p: ref Spree->Member): ref Cmember;
+ findid: fn(id: int): ref Cmember;
+ leave: fn(cp: self ref Cmember);
+ next: fn(cp: self ref Cmember, fwd: int): ref Cmember;
+ prev: fn(cp: self ref Cmember, fwd: int): ref Cmember;
+ };
+
+ Selection: adt {
+ stack: ref Spree->Object;
+ ownerid: int;
+ isrange: int;
+ r: Range;
+ idxl: list of int;
+
+ set: fn(sel: self ref Selection, stack: ref Spree->Object);
+ setexcl: fn(sel: self ref Selection, stack: ref Spree->Object): int;
+ setrange: fn(sel: self ref Selection, r: Range);
+ addindex: fn(sel: self ref Selection, i: int);
+ delindex: fn(sel: self ref Selection, i: int);
+ isempty: fn(sel: self ref Selection): int;
+ isset: fn(sel: self ref Selection, index: int): int;
+ transfer: fn(sel: self ref Selection, dst: ref Spree->Object, index: int);
+ owner: fn(sel: self ref Selection): ref Cmember;
+ };
+
+ selection: fn(stack: ref Spree->Object): ref Selection;
+
+ # pack and facing directions (clockwise by face direction)
+ dTOP, dLEFT, dBOTTOM, dRIGHT: con iota;
+ dMASK: con 7;
+ dSHIFT: con 0;
+
+ # anchor positions
+ aSHIFT: con 4;
+ aMASK: con 16rf0;
+ aCENTRE, aUPPERCENTRE, aUPPERLEFT, aCENTRELEFT,
+ aLOWERLEFT, aLOWERCENTRE, aLOWERRIGHT,
+ aCENTRERIGHT, aUPPERRIGHT: con iota << aSHIFT;
+
+ # orientations
+ oMASK: con 16rf00;
+ oSHIFT: con 8;
+ oRIGHT, oUP, oLEFT, oDOWN: con iota << oSHIFT;
+
+ EXPAND: con 16r1000;
+
+ FILLSHIFT: con 13;
+ FILLX, FILLY: con 1 << (FILLSHIFT + iota);
+ FILLMASK: con FILLX|FILLY;
+
+ CLUBS, DIAMONDS, HEARTS, SPADES: con iota;
+
+ init: fn(spree: Spree, clique: ref Spree->Clique);
+
+ addlayframe: fn(name: string, parent: string, layout: ref Layout, packopts: int, facing: int);
+ addlayobj: fn(name: string, parent: string, layout: ref Layout, packopts: int, obj: ref Spree->Object);
+ dellay: fn(name: string, layout: ref Layout);
+
+ newstack: fn(parent: ref Spree->Object, p: ref Spree->Member, spec: Stackspec): ref Spree->Object;
+
+ archive: fn(): ref Spree->Object;
+ unarchive: fn(): ref Spree->Object;
+ setarchivename: fn(o: ref Spree->Object, name: string);
+ getarchiveobj: fn(name: string): ref Spree->Object;
+ archivearray: fn(a: array of ref Spree->Object, name: string);
+ getarchivearray: fn(name: string): array of ref Spree->Object;
+
+ newlayout: fn(parent: ref Spree->Object, vis: Sets->Set): ref Layout;
+ makecards: fn(stack: ref Spree->Object, r: Range, rear: string);
+ maketable: fn(parent: string);
+ deal: fn(stack: ref Spree->Object, n: int, stacks: array of ref Spree->Object, first: int);
+ shuffle: fn(stack: ref Spree->Object);
+ sort: fn(stack: ref Spree->Object, rank, suitrank: array of int);
+
+ getcard: fn(card: ref Spree->Object): Card;
+ getcards: fn(stack: ref Spree->Object): array of Card;
+ discard: fn(stk, pile: ref Spree->Object, facedown: int);
+ setface: fn(card: ref Spree->Object, face: int);
+
+ flip: fn(stack: ref Spree->Object);
+
+ nmembers: fn(): int;
+};
diff --git a/appl/spree/lib/commandline.b b/appl/spree/lib/commandline.b
new file mode 100644
index 00000000..8b60ab01
--- /dev/null
+++ b/appl/spree/lib/commandline.b
@@ -0,0 +1,191 @@
+implement Commandline;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "commandline.m";
+
+Debug: con 0;
+
+nomodule(modpath: string)
+{
+ sys->fprint(stderr(), "fibs: couldn't load %s: %r\n", modpath);
+ raise "fail:bad module";
+}
+
+init()
+{ sys = load Sys Sys->PATH;
+
+ tk = load Tk Tk->PATH;
+ if (tk == nil) nomodule(Tk->PATH);
+
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil) nomodule(Tkclient->PATH);
+ tkclient->init();
+}
+
+Cmdline.new(top: ref Tk->Toplevel, w, textopts: string): (ref Cmdline, chan of string)
+{
+ window_cfg := array[] of {
+ "frame " + w,
+ "scrollbar " + w + ".scroll -command {" + w + ".t yview}",
+ "text " + w + ".t -yscrollcommand {" + w + ".scroll set} " + textopts,
+ "pack " + w + ".scroll -side left -fill y",
+ "pack " + w + ".t -fill both -expand 1",
+
+ "bind " + w + ".t <Key> {send evch k {%A}}",
+ "bind " + w + ".t <Control-d> {send evch k {%A}}",
+ "bind " + w + ".t <Control-u> {send evch k {%A}}",
+ "bind " + w + ".t <Control-w> {send evch k {%A}}",
+ "bind " + w + ".t <Control-h> {send evch k {%A}}",
+ # treat button 2 and button 3 the same so we're alright with a 2-button mouse
+ "bind " + w + ".t <ButtonPress-2> {send evch b %x %y}",
+ "bind " + w + ".t <ButtonPress-3> {send evch b %x %y}",
+ w + ".t mark set outpoint end",
+ w + ".t mark gravity outpoint left",
+ w + ".t mark set inpoint end",
+ w + ".t mark gravity inpoint left",
+ };
+ evch := chan of string;
+ tk->namechan(top, evch, "evch");
+
+ for (i := 0; i < len window_cfg; i++) {
+ e := cmd(top, window_cfg[i]);
+ if (e != nil && e[0] == '!')
+ break;
+ }
+
+ err := tk->cmd(top, "variable lasterror");
+ if (err != nil) {
+ sys->fprint(stderr(), "error in commandline config: %s\n", err);
+ raise "fail:commandline config error";
+ }
+ cmd(top, w + ".t mark set insert end;" + w + ".t see insert");
+ return (ref Cmdline(w, top), evch);
+}
+
+Cmdline.focus(cmdl: self ref Cmdline)
+{
+ cmd(cmdl.top, "focus " + cmdl.w + ".t");
+}
+
+Cmdline.event(cmdl: self ref Cmdline, e: string): list of string
+{
+ case e[0] {
+ 'k' =>
+ return handle_key(cmdl, e[2:]);
+ 'b' =>
+ ;
+ }
+ return nil;
+}
+
+BS: con 8; # ^h backspace character
+BSW: con 23; # ^w bacspace word
+BSL: con 21; # ^u backspace line
+
+handle_key(cmdl: ref Cmdline, c: string): list of string
+{
+ (w, top) := (cmdl.w, cmdl.top);
+ # don't allow editing of the text before the inpoint.
+ if (int cmd(top, w + ".t compare insert < inpoint"))
+ return nil;
+ lines: list of string;
+ char := c[1];
+ if (char == '\\')
+ char = c[2];
+ case char {
+ * =>
+ cmd(top, w + ".t insert insert "+c+" {}");
+ '\n' =>
+ cmd(top, w + ".t insert insert "+c+" {}");
+ lines = sendinput(cmdl);
+ BSL or BSW or BS =>
+ delpoint: string;
+ case char {
+ BSL => delpoint = "{insert linestart}";
+ BSW => delpoint = "{insert -1char wordstart}"; # wordstart isn't ideal
+ BS => delpoint = "{insert-1char}";
+ }
+ if (int cmd(top, w + ".t compare inpoint < " + delpoint))
+ cmd(top, w + ".t delete "+delpoint+" insert");
+ else
+ cmd(top, w + ".t delete inpoint insert");
+ }
+ cmd(top, w + ".t see insert;update");
+ return lines;
+}
+
+sendinput(cmdl: ref Cmdline): list of string
+{
+ (w, top) := (cmdl.w, cmdl.top);
+ # loop through all the lines that have been entered,
+ # processing each one in turn.
+ nl, lines: list of string;
+ for (;;) {
+ input: string;
+ input = cmd(top, w + ".t get inpoint end");
+ if (len input == 0)
+ break;
+ for (i := 0; i < len input; i++)
+ if (input[i] == '\n')
+ break;
+ if (i >= len input)
+ break;
+ cmd(top, w + ".t mark set outpoint inpoint+"+string (i+1)+"chars");
+ cmd(top, w + ".t mark set inpoint outpoint");
+ lines = input[0:i+1] :: lines;
+ }
+ for (; lines != nil; lines = tl lines)
+ nl = hd lines :: nl;
+ return nl;
+}
+
+add(cmdl: ref Cmdline, t: string, n: int)
+{
+ (w, top) := (cmdl.w, cmdl.top);
+ cmd(top, w + ".t insert outpoint " + t);
+ cmd(top, w + ".t mark set outpoint outpoint+"+string n+"chars");
+ cmd(top, w + ".t mark set inpoint outpoint");
+ cmd(top, w + ".t see insert");
+}
+
+Cmdline.tagaddtext(cmdl: self ref Cmdline, t: list of (string, string))
+{
+ txt := "";
+ n := 0;
+ for (; t != nil; t = tl t) {
+ (tags, s) := hd t;
+ txt += " " + tk->quote(s) + " {" + tags + "}";
+ n += len s;
+ }
+ add(cmdl, txt, n);
+}
+
+Cmdline.addtext(cmdl: self ref Cmdline, txt: string)
+{
+ if (Debug) sys->print("%s", txt);
+ add(cmdl, tk->quote(txt) + " {}" , len txt);
+}
+
+Cmdline.maketag(cmdl: self ref Cmdline, name, options: string)
+{
+ cmd(cmdl.top, cmdl.w + ".t tag configure " + name + " " + options);
+}
+
+stderr(): ref Sys->FD
+{
+ return sys->fildes(2);
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ e := tk->cmd(top, s);
+ if (e != nil && e[0] == '!')
+ sys->fprint(stderr(), "cmd error on '%s': %s\n", s, e);
+ return e;
+}
diff --git a/appl/spree/lib/commandline.m b/appl/spree/lib/commandline.m
new file mode 100644
index 00000000..7fcfa965
--- /dev/null
+++ b/appl/spree/lib/commandline.m
@@ -0,0 +1,16 @@
+Commandline: module {
+ init: fn();
+
+ PATH: con "/dis/spree/lib/commandline.dis";
+ Cmdline: adt {
+ new: fn(win: ref Tk->Toplevel, w, textopts: string): (ref Cmdline, chan of string);
+ event: fn(cmdl: self ref Cmdline, e: string): list of string;
+ tagaddtext: fn(cmdl: self ref Cmdline, t: list of (string, string));
+ addtext: fn(cmdl: self ref Cmdline, txt: string);
+ focus: fn(cmdl: self ref Cmdline);
+ maketag: fn(cmdl: self ref Cmdline, name, options: string);
+
+ w: string;
+ top: ref Tk->Toplevel;
+ };
+};
diff --git a/appl/spree/lib/objstore.b b/appl/spree/lib/objstore.b
new file mode 100644
index 00000000..47d0b13d
--- /dev/null
+++ b/appl/spree/lib/objstore.b
@@ -0,0 +1,65 @@
+implement Objstore;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ None: import Sets;
+include "../spree.m";
+ spree: Spree;
+ Object, Clique: import spree;
+include "objstore.m";
+
+clique: ref Clique;
+archiveobjs: array of list of (string, ref Object);
+
+init(mod: Spree, g: ref Clique)
+{
+ sys = load Sys Sys->PATH;
+ spree = mod;
+ clique = g;
+}
+
+unarchive()
+{
+ archiveobjs = array[27] of list of (string, ref Object);
+ for (i := 0; i < len clique.objects; i++) {
+ obj := clique.objects[i];
+ if (obj != nil && (nm := obj.getattr("§")) != nil) {
+ (n, toks) := sys->tokenize(nm, " ");
+ for (; toks != nil; toks = tl toks) {
+ x := strhash(hd toks, len archiveobjs);
+ archiveobjs[x] = (hd toks, obj) :: archiveobjs[x];
+ }
+ obj.setattr("§", nil, None);
+ }
+ }
+}
+
+setname(obj: ref Object, name: string)
+{
+ nm := obj.getattr("§");
+ if (nm != nil)
+ nm += " " + name;
+ else
+ nm = name;
+ obj.setattr("§", nm, None);
+}
+
+get(name: string): ref Object
+{
+ for (al := archiveobjs[strhash(name, len archiveobjs)]; al != nil; al = tl al)
+ if ((hd al).t0 == name)
+ return (hd al).t1;
+ return nil;
+}
+
+# from Aho Hopcroft Ullman
+strhash(s: string, n: int): int
+{
+ h := 0;
+ m := len s;
+ for(i := 0; i<m; i++){
+ h = 65599 * h + s[i];
+ }
+ return (h & 16r7fffffff) % n;
+}
diff --git a/appl/spree/lib/objstore.m b/appl/spree/lib/objstore.m
new file mode 100644
index 00000000..86aa33b5
--- /dev/null
+++ b/appl/spree/lib/objstore.m
@@ -0,0 +1,8 @@
+Objstore: module {
+ PATH: con "/dis/spree/lib/objstore.dis";
+
+ init: fn(mod: Spree, g: ref Clique);
+ unarchive: fn();
+ setname: fn(o: ref Object, name: string);
+ get: fn(name: string): ref Object;
+};
diff --git a/appl/spree/lib/testsets.b b/appl/spree/lib/testsets.b
new file mode 100644
index 00000000..2556838f
--- /dev/null
+++ b/appl/spree/lib/testsets.b
@@ -0,0 +1,152 @@
+implement Testsets;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "rand.m";
+include "sets.m"; # "sets.m" or "sets32.m"
+ sets: Sets;
+ Set, set, A, B: import sets;
+
+BPW: con 32;
+SHIFT: con 5;
+MASK: con 31;
+
+Testsets: module {
+ init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+∅: Set;
+
+Testbig: con 1;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+ sys = load Sys Sys->PATH;
+ sets = load Sets Sets->PATH;
+ if (sets == nil) {
+ sys->print("cannot load %s: %r\n", Sets->PATH);
+ exit;
+ }
+ rand := load Rand Rand->PATH;
+ sets->init();
+
+ ∅ = set();
+ s := set().addlist(1::2::3::4::nil);
+ addit(s);
+ sys->print("s %s\n", s.str());
+ r := s.invert();
+ sys->print("r %s\n", r.str());
+ r = r.del(20);
+ addit(r);
+ sys->print("r del20: %s\n", r.str());
+ z := r.X(~A&~B, s);
+ addit(z);
+ sys->print("z: %s\n", z.str());
+
+ x := set();
+ for (i := 0; i < 31; i++)
+ if (rand->rand(2))
+ x = x.add(i);
+ addit(x);
+ for(i = 0; i < 31; i++)
+ addit(set().add(i));
+ if (Testbig) {
+ r = r.del(100);
+ addit(r);
+ sys->print("rz: %s\n", r.str());
+ r = r.add(100);
+ addit(r);
+ sys->print("rz2: %s\n", r.str());
+ x = set();
+ for (i = 0; i < 200; i++)
+ x = x.add(rand->rand(300));
+ addit(x);
+ for(i = 31; i < 70; i++)
+ addit(set().add(i));
+ }
+ sys->print("empty: %s\n", set().str());
+ addit(set());
+ sys->print("full: %s\n", set().invert().str());
+ test();
+ sys->print("done tests\n");
+}
+
+ds(d: array of byte): string
+{
+ s := "";
+ for(i := len d - 1; i >= 0; i--)
+ s += sys->sprint("%.2x", int d[i]);
+ return s;
+}
+
+testsets: list of Set;
+addit(s: Set)
+{
+ testsets = s :: testsets;
+}
+
+test()
+{
+ for (t := testsets; t != nil; t = tl t)
+ testsets = (hd t).invert() :: testsets;
+
+ for (t = testsets; t != nil; t = tl t)
+ testa(hd t);
+ for (t = testsets; t != nil; t = tl t) {
+ a := hd t;
+ for (s := testsets; s != nil; s = tl s) {
+ b := hd s;
+ testab(a, b);
+ }
+ }
+}
+
+testab(a, b: Set)
+{
+ {
+ check(!a.eq(b) == !b.eq(a), "equality");
+ if (superset(a, b) && !a.eq(b))
+ check(!superset(b, a), "superset");
+ } exception {
+ "test failed" =>
+ sys->print("%s, %s [%s, %s]\n", a.str(), b.str(), a.debugstr(), b.debugstr());
+ }
+}
+
+testa(a: Set)
+{
+ {
+ check(sets->str2set(a.str()).eq(a), "string conversion");
+ check(a.eq(a), "self equality");
+ check(a.eq(a.invert().invert()), "double inversion");
+ check(a.X(A&~B, a).eq(∅), "self not intersect");
+ check(a.limit() == a.invert().limit(), "invert limit");
+ check(a.X(A&~B, set().invert()).limit() == 0, "zero limit");
+ check(sets->bytes2set(a.bytes(0)).eq(a), "bytes conversion");
+ check(sets->bytes2set(a.bytes(3)).eq(a), "bytes conversion(2)");
+
+ if (a.limit() > 0) {
+ if (a.msb())
+ check(!a.holds(a.limit() - 1), "hold limit 1");
+ else
+ check(a.holds(a.limit() - 1), "hold limit 2");
+ }
+ } exception {
+ "test failed" =>
+ sys->print("%s [%s]\n", a.str(), a.debugstr());
+ }
+}
+
+check(ok: int, s: string)
+{
+ if (!ok) {
+ sys->print("test failed: %s; ", s);
+ raise "test failed";
+ }
+}
+
+# return true if a is a superset of b
+superset(a, b: Set): int
+{
+ return a.X(~A&B, b).eq(∅);
+}
diff --git a/appl/spree/lib/tricks.b b/appl/spree/lib/tricks.b
new file mode 100644
index 00000000..3763bac5
--- /dev/null
+++ b/appl/spree/lib/tricks.b
@@ -0,0 +1,140 @@
+implement Tricks;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "sets.m";
+ sets: Sets;
+ Set, All, None: import sets;
+include "../spree.m";
+ spree: Spree;
+ Attributes, Range, Object, Clique, Member: import spree;
+include "cardlib.m";
+ cardlib: Cardlib;
+ Card, getcard: import cardlib;
+include "tricks.m";
+
+clique: ref Clique;
+
+init(mod: Spree, g: ref Clique, cardlibmod: Cardlib)
+{
+ sys = load Sys Sys->PATH;
+ sets = load Sets Sets->PATH;
+ if (sets == nil)
+ panic(sys->sprint("cannot load %s: %r", Sets->PATH));
+ clique = g;
+ spree = mod;
+ cardlib = cardlibmod;
+}
+
+defaultrank := array[13] of {12, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+
+# XXX should take a "rank" array so that we can cope with custom
+# card ranking
+Trick.new(pile: ref Object, trumps: int, hands: array of ref Object, rank: array of int): ref Trick
+{
+ t := ref Trick;
+ t.highcard = t.startcard = Card(-1, -1, -1);
+ t.winner = -1;
+ t.trumps = trumps;
+ t.pile = pile;
+ t.hands = hands;
+ if (rank == nil)
+ rank = defaultrank;
+ t.rank = rank;
+ return t;
+}
+
+Trick.archive(t: self ref Trick, archiveobj: ref Object, name: string)
+{
+ a := clique.newobject(archiveobj, None, "trick");
+ cardlib->setarchivename(a, name);
+ a.setattr("trumps", string t.trumps, None);
+ a.setattr("winner", string t.winner, None);
+ a.setattr("startcard.n", string t.startcard.number, None);
+ a.setattr("startcard.suit", string t.startcard.suit, None);
+ a.setattr("highcard.n", string t.highcard.number, None);
+ a.setattr("highcard.suit", string t.highcard.suit, None);
+ cardlib->setarchivename(t.pile, name + ".pile");
+ cardlib->archivearray(t.hands, name);
+ for (i := 0; i < len t.rank; i++)
+ if (t.rank[i] != defaultrank[i])
+ break;
+ if (i < len t.rank) {
+ r := "";
+ for (i = 0; i < len t.rank; i++)
+ r += " " + string t.rank[i];
+ a.setattr("rank", r, None);
+ }
+}
+
+Trick.unarchive(nil: ref Object, name: string): ref Trick
+{
+ t := ref Trick;
+ a := cardlib->getarchiveobj(name);
+ t.trumps = int a.getattr("trumps");
+ t.winner = int a.getattr("winner");
+ t.startcard.number = int a.getattr("startcard.n");
+ t.startcard.suit = int a.getattr("startcard.suit");
+ t.highcard.number = int a.getattr("highcard.n");
+ t.highcard.suit = int a.getattr("highcard.suit");
+ t.pile = cardlib->getarchiveobj(name + ".pile");
+ t.hands = cardlib->getarchivearray(name);
+ r := a.getattr("rank");
+ if (r != nil) {
+ (nil, toks) := sys->tokenize(r, " ");
+ t.rank = array[len toks] of int;
+ i := 0;
+ for (; toks != nil; toks = tl toks)
+ t.rank[i++] = int hd toks;
+ } else
+ t.rank = defaultrank;
+ return t;
+}
+
+Trick.play(t: self ref Trick, ord, idx: int): string
+{
+ stack := t.hands[ord];
+ if (idx < 0 || idx >= len stack.children)
+ return "invalid card to play";
+
+ c := getcard(stack.children[idx]);
+ c.number = t.rank[c.number];
+ if (len t.pile.children == 0) {
+ t.winner = ord;
+ t.startcard = t.highcard = c;
+ } else {
+ if (c.suit != t.startcard.suit) {
+ if (containssuit(stack, t.startcard.suit))
+ return "you must play the suit that was led";
+ if (c.suit == t.trumps &&
+ (t.highcard.suit != t.trumps ||
+ c.number > t.highcard.number)) {
+ t.highcard = c;
+ t.winner = ord;
+ }
+ } else if (c.suit == t.highcard.suit && c.number > t.highcard.number) {
+ t.highcard = c;
+ t.winner = ord;
+ }
+ }
+
+ stack.transfer((idx, idx + 1), t.pile, len t.pile.children);
+ stack.setattr("n", string (int stack.getattr("n") - 1), All);
+ return nil;
+}
+
+containssuit(stack: ref Object, suit: int): int
+{
+ ch := stack.children;
+ n := len ch;
+ for (i := 0; i < n; i++)
+ if (getcard(ch[i]).suit == suit)
+ return 1;
+ return 0;
+}
+
+panic(e: string)
+{
+ sys->fprint(sys->fildes(2), "tricks panic: %s\n", e);
+ raise "panic";
+}
diff --git a/appl/spree/lib/tricks.m b/appl/spree/lib/tricks.m
new file mode 100644
index 00000000..0cdab785
--- /dev/null
+++ b/appl/spree/lib/tricks.m
@@ -0,0 +1,21 @@
+Tricks: module {
+ PATH: con "/dis/spree/lib/tricks.dis";
+ init: fn(mod: Spree, g: ref Clique, cardlibmod: Cardlib);
+
+ Trick: adt {
+ trumps: int;
+ startcard: Cardlib->Card;
+ highcard: Cardlib->Card;
+ winner: int;
+ pile: ref Object;
+ hands: array of ref Object;
+ rank: array of int;
+
+ new: fn(pile: ref Object, trumps: int,
+ hands: array of ref Object, rank: array of int): ref Trick;
+ play: fn(t: self ref Trick, ord, idx: int): string;
+ archive: fn(t: self ref Trick, archiveobj: ref Object, name: string);
+ unarchive: fn(archiveobj: ref Object, name: string): ref Trick;
+ };
+
+};
diff --git a/appl/spree/man/gamesrv.man2 b/appl/spree/man/gamesrv.man2
new file mode 100644
index 00000000..fd910519
--- /dev/null
+++ b/appl/spree/man/gamesrv.man2
@@ -0,0 +1,471 @@
+.TH GAMESRV 2
+.SH NAME
+Gamesrv \- game server module
+.SH SYNOPSIS
+.EX
+.ps -1
+.vs -1
+include "draw.m";
+include "gamesrv.m";
+gamesrv := load Gamesrv Gamesrv->PATH;
+Range, Object, Game, Player: import gamesrv;
+
+Range: adt {
+ start: int;
+ end: int;
+};
+
+Object: adt {
+ transfer: fn(o: self ref Object,
+ r: Range, dst: ref Object, i: int);
+ setvisibility: fn(o: self ref Object,
+ visibility: int);
+ setattrvisibility: fn(o: self ref Object,
+ name: string, visibility: int);
+ setattr: fn(o: self ref Object,
+ name: string, val: string, vis: int);
+ getattr: fn(o: self ref Object, name: string): string;
+ delete: fn(o: self ref Object);
+ deletechildren: fn(o: self ref Object, r: Range);
+
+ id: int;
+ parentid: int;
+ children: array of ref Object;
+ objtype: string;
+ visibility: int;
+ # ...private data
+
+};
+
+Game: adt {
+ newobject: fn(game: self ref Game, parent: ref Object,
+ visibility: int, objtype: string): ref Object;
+ action: fn(game: self ref Game, cmd: string,
+ objs: list of int, rest: string, whoto: int);
+ player: fn(game: self ref Game, id: int): ref Player;
+
+ objects: array of ref Object;
+ # ...private data
+};
+
+Player: adt {
+ name: fn(player: self ref Player): string;
+ hangup: fn(player: self ref Player);
+ obj: fn(player: self ref Player, id: int): ref Object;
+
+ id: int;
+ # ...private data
+};
+
+Gamemodule: module {
+ clienttype: fn(): string;
+ init: fn(game: ref Gamesrv->Game, srvmod: Gamesrv): string;
+ command: fn(player: ref Gamesrv->Player, e: string): string;
+ join: fn(player: ref Gamesrv->Player): string;
+ leave: fn(player: ref Gamesrv->Player);
+};
+
+rand: fn(n: int): int;
+.ps +1
+.vs +1
+.EE
+.SH DESCRIPTION
+.I Gamesrv
+provides a general server interface that allows distributed
+clients to interact in a controlled manner, with the
+interaction mediated
+by Limbo modules, known as
+.IR "game engines" ,
+or just
+.I engines
+for short.
+Each engine decides on the rules
+of its particular game; the engine interface is described
+at the end of this manual page, under
+``Module Interface''.
+.PP
+This manual page describes the
+interface as presented to an engine
+once it has been loaded by
+.IR gamesrv .
+An engine is responsible for a particular
+.IR game ,
+in which one or more
+.I players
+participate. Messages sent by players
+are interpreted by the game engine, which
+responds by making changes to the hierarchical
+.I object
+database held by the game.
+Behind the scenes
+.I gamesrv
+distributes updates to this database to players
+of the game as appropriate.
+.SS "Objects and visibility"
+Objects hold a game's visible state. An object
+has a unique integer
+.IR id ,
+which is an index into the array
+.IB game .objects\fR;\fP
+it also holds a set of attribute-value pairs, a type, and
+zero or more child objects. Together, all the objects
+in the game form a hierarchical tree, rooted at
+the
+.IR "root object"
+(id 0), which always exists.
+Each attribute and each object also has an associated
+.IR "visibility set" ,
+the set of players that sees updates to the attribute or the children
+of the object. A visibility set is an integer, a bitmask where each
+bit represents one player, hence
+.B ~0
+is visible to all players, and
+.B 0
+is visible to no-one.
+In general, each player has a unique
+identifier
+.IR id ;
+in an integer
+.I i
+representing a set of players,
+the
+.IR id th
+bit represents the presence of the player with
+identifier
+.IR id .
+Thus, for a player
+.IR p ,
+.BI "(1<<" p ".id)"
+is the set containing only
+.IR p ,
+.BI "(" i "&~(1<<" p ".id))"
+excludes
+.I p
+from the set, and
+.BI "(" i "|(1<<" p ".id))"
+includes
+.I p
+in the set.
+.PP
+Note that the visibility set of an object does not alter the visibility
+of that object's attributes, but only that of its children (and of
+their children: in general an object is visible to a player if the
+intersection of all its ancestors' visibility sets contains that
+player).
+.PP
+Objects can be transferred inside the hierarchy from one parent to
+another. If an object is moved to a parent whose visibility conceals it
+from a player, then it will appear to that player to have been deleted;
+if it is later made visible, then it will be recreated for that
+player.
+A game engine can almost always ignore this technicality,
+except for one thing: the identifier used by a particular player to
+identify an object is not necessarily the same as that used by the game
+engine. Thus when an engine receives an object id in a player's
+message, it should convert it using the
+.IB player .obj()
+function.
+.SS \fBGame\fP
+The
+.B Game
+type holds all the objects in a game. It allows the
+creation of new objects, and provides way of communicating
+with players outside the object hierarchy.
+All data members of a
+.B Game
+should be treated as read-only.
+.TP 10
+.IB game .objects
+This array holds the objects in the game. An object with
+identifier
+.I id
+is found at
+.IB game .objects[ id ]\fR.\fP
+.TP
+.IB game .newobject(\fIparent\fP,\ \fIvisibility\fP,\ \fIobjtype\fP)
+.B Newobject
+creates a new object at the end
+of
+.IR parent 's
+children;
+If
+.I parent
+is nil, the new object is created under the root object.
+The new object has visibility
+.IR visibility ,
+and type
+.IR objtype .
+An object's type cannot be changed once
+it has been created.
+.TP
+.IB game .action(\fIcmd\fP,\ \fIobjs\fP,\ \fIrest\fP,\ \fIwhoto\fP)
+.B Action
+sends a message to some players without affecting
+the object hierarchy. It can be used to send transient
+events that have no meaning when stored statically
+(for example, network latency probes).
+The message is sent to the set of players given by
+.IR whoto .
+.I Objs
+is assumed to be a list of object ids, which are
+converted appropriately for each player
+receiving the message; the final
+message is a string built by concatenating
+.IR cmd ,
+the list of object ids, and
+.IR rest ,
+separated by spaces.
+.TP
+.IB game .player(\fIid\fP)
+.B Player
+yields the player corresponding to identifier
+.IR id ,
+or
+.B nil
+if there is none.
+.SS Player
+The
+.B Player
+type represents a player of a game.
+.TP 10
+.IB player .id
+The player's identifier, an integer between
+0 and 31. This is unique across all current players,
+but ids of players that have left the game will
+be reused.
+.TP
+.IB player .obj(\fIid\fP)
+.B Obj
+converts from a player's external object
+identifier to the game's local
+.B Object
+that it represents. It returns
+.B nil
+if there is no such object.
+.TP
+.IB player .hangup()
+.B Hangup
+hangs up a player's connection to the game;
+no more requests from
+.I player
+will be received by the game engine.
+.TP
+.IB player .name()
+.B Name
+yields the authenticated name of the player.
+This is not necessarily unique over the players
+of a game.
+.SS \fBObject\fP
+The
+.B Object
+type is the basic unit of game engine state.
+An object's children can be selectively concealed
+from players; it holds a set of
+.RI ( attribute ,\ value )
+pairs, each of which can be concealed likewise.
+Where an argument
+.IR r ,
+of
+.B Range
+type is used, it refers to a range of an object's
+children starting at index
+.IB r .start\fR,\fP
+and finishing at
+.IB r .end-1\fR.\fP
+All the data members of an
+.B Object
+should be treated as read-only.
+.TP 10
+.IB obj .setattr(\fIname\fP,\ \fIval\fP,\ \fIvis\fP)
+.B Setattr
+sets attribute
+.I name
+in
+.I obj
+to
+.IR val.
+If the attribute is being created for the
+first time, then it will be given visibility
+.IR vis .
+.I Name
+should be non-empty, and should not
+contain any space characters.
+Note that it is not possible for an attribute
+to refer directly to an object by its identifier;
+if this facility is needed, another identifying
+scheme should be used. This also applies
+to player identifiers, which will change
+if the game is saved and loaded again (not
+implemented yet).
+.TP
+.IB obj .getattr(\fIname\fP)
+.B Getattr
+yields the current value of the
+attribute
+.I name
+in
+.IR obj .
+If an attribute is not set, it yields
+.BR nil .
+.TP
+.IB obj .delete()
+.B Delete
+removes
+.I obj
+from the object
+hierarchy.
+.TP
+.IB obj .deletechildren(\fIr\fP)
+.B Deletechildren
+deletes children in range
+.I r
+from
+.IR obj .
+.TP
+.IB obj .transfer(\fIr\fP,\ \fIdst\fP,\ \fIi\fP)
+.B Transfer
+transfers the children in range
+.I r
+from
+.I obj
+to just before the object at index
+.I i
+in
+.IR dst .
+It is permissible for
+.I obj
+and
+.I dst
+to be the same object.
+.TP
+.IB obj .setvisibility(\fIvisibility\fP)
+.B Setvisibility
+allows the set of players
+given in
+.I visibility
+to see the children of
+.IR obj ,
+and denies access to all others.
+Players are notified of the change.
+.TP
+.IB obj .setattrvisibility(\fIname\fP,\ \fIvisibility\fP)
+.B Setattrvisibility
+allows the set of players
+given in
+.I visibility
+to see the value of
+.IR obj 's
+attribute
+.IR name ,
+and denies access to all others.
+Players are not notified of the change;
+if there is a need to communicate
+the fact of an attribute becoming invisible to
+players, it should be done by using another
+(visible) attribute to communicate the change.
+.SS "Module Interface"
+A game engine module,
+.IR mod ,
+must implement the
+following functions. Where a function returns a string,
+it is interpreted as an error response to the player
+responsible for the request; an empty string signifies
+no error.
+.TP
+.IB mod .clienttype()
+.B Clienttype
+should return the type of client required
+by the engine (e.g.
+.B cards
+for the card-game client).
+Each client type has its own conventions
+as to the meaning of object types and attribute
+names and values.
+This function may be called before
+.BR init() .
+.TP
+.IB mod .init(\fIgame\fP,\ \fIsrvmod\fP)
+.B Init
+initialises the game engine.
+.I Game
+is the game that the engine is controlling,
+and
+.I srvmod
+is the
+.B Gamesrv
+module holding its associated data.
+An error response from this function
+causes the game to be aborted.
+.TP
+.IB mod .join(\fIplayer\fP)
+.I Player
+has made a request to join the game;
+an error response causes the request to be
+refused, otherwise the player joins the
+game.
+.TP
+.IB mod .leave(\fIplayer\fP)
+.I Player
+has left the game.
+.TP
+.IB mod .command(\fIplayer\fP,\ \fIe\fP)
+.I Player
+has sent the command
+.IR e .
+The command usually follows
+the simple message conventions
+used in
+.IR gamesrv (4),
+i.e. simple space-separated tokens.
+.SH EXAMPLE
+The following is a small, but working example
+of a game engine that acts as a chat server
+(parsing error checking omitted, and white-space
+compressed to save paper):
+.PP
+.EX
+.ps -1
+.vs -1
+implement Gamemodule;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "../gamesrv.m";
+ gamesrv: Gamesrv;
+ Game, Player: import gamesrv;
+game: ref Game;
+clienttype(): string
+{
+ return "chat";
+}
+init(g: ref Game, srvmod: Gamesrv): string
+{
+ (sys, game, gamesrv) = (load Sys Sys->PATH, g, srvmod);
+ return nil;
+}
+join(nil: ref Player): string
+{
+ return nil;
+}
+leave(nil: ref Player)
+{
+}
+command(player: ref Player, cmd: string): string
+{
+ game.action("say " + string player.id + " " + cmd, nil, nil, ~0);
+ return nil;
+}
+.ps +1
+.vs +1
+.EE
+.SH SOURCE
+.B /appl/cmd/games/gamesrv.b
+.SH "SEE ALSO"
+.IR gamesrv (4)
+.SH BUGS
+The reuse of object ids can lead to
+problems when objects are deleted and
+recreated on the server before clients become
+aware of the changes.
+.PP
+This interface is new and will change.
diff --git a/appl/spree/man/gamesrv.man4 b/appl/spree/man/gamesrv.man4
new file mode 100644
index 00000000..5db352b3
--- /dev/null
+++ b/appl/spree/man/gamesrv.man4
@@ -0,0 +1,296 @@
+.TH GAMESRV 4
+.SH NAME
+gamesrv \- game server
+.SH SYNOPSIS
+.B games/gamesrv
+[
+.B -l
+] [
+.B -a
+.I alg
+]...
+[
+.B -A
+] [
+.IR addr | mntpoint
+]
+.PP
+.IB mntpoint /players
+.br
+.IB mntpoint /new
+.br
+.IB mntpoint / n
+.SH DESCRIPTION
+.B Gamesrv
+serves a file system that allows clients to interact
+through various types of game engine.
+Usually, it operates in network mode:
+it listens for incoming connections on
+.I addr
+(default
+.BR tcp!*!3242 ),
+authenticates them, and serves files to them.
+If the
+.B -A
+option is given, no authentication takes place,
+otherwise each
+.I alg
+gives an additional possible
+encryption or digest algorithm to use
+on the connection (see
+.IR ssl (3)).
+If no
+.I alg
+is specified,
+.B none
+is assumed.
+The
+.B -l
+option causes the game server to be mounted
+locally on
+.I mntpoint
+\- this can be useful for single player games,
+or debugging.
+.PP
+Once the name-space served by
+.I gamesrv
+is mounted, it serves the following files.
+All identifiers referred to below are
+small integers, expressed as decimal ASCII strings.
+.TP
+.B players
+Reading this file provides updates on players
+arriving and leaving, games being created
+and destroyed, and chat messages outside
+the scope of any game.
+Reads will block until something of interest happens.
+Each update holds space separated
+tokens and is terminated with a newline.
+A read will return as many updates as will fit
+into the read buffer. Update messages are as follows:
+.RS
+.TP
+.BI clientid " clientid name"
+Identifies the name,
+.IR name ,
+and the client identifier,
+.IR clientid ,
+of the client
+reading the players file.
+.TP
+.BI join " clientid name"
+A client has authenticated as
+.IR name ,
+and has been allocated identifier
+.IR clientid .
+.TP
+.BI leave " clientid"
+The client identified by
+.I clientid
+has terminated connection with the server.
+.TP
+.BI gametype " clienttype name"
+The server announces the availability of a game
+named
+.I name
+on the server. The game requires a client of
+type
+.I clienttype
+to display the game.
+.TP
+.BI creategame " gameid name clienttype"
+An instance of a game named
+.IR name
+has been created; it needs a client
+of type
+.IR clienttype ,
+and has been given identifier
+.IR gameid .
+.TP
+.BI deletegame " gameid"
+The game identified by
+.I gameid
+has been deleted.
+.TP
+.BI joingame " gameid clientid playerid name"
+Client
+.I clientid
+(named
+.IR name )
+has joined game
+.I gameid ,
+and is allocated player id
+.I playerid
+in the game.
+.TP
+.BI leavegame " gameid playerid name"
+Player
+.I playerid
+(named
+.IR name )
+has left
+.IR gameid .
+.TP
+.BI chat " clientid msg"
+Client
+.I clientid
+has sent the chat message
+.IR msg .
+.PP
+Writing to the
+.B players
+file causes a
+.B chat
+message to be sent to all other clients reading
+the players file. All but the first line of the
+write request is ignored.
+.RE
+.TP
+.B new
+Opening
+.B new
+prepares to create a new game.
+The only message that can be written
+to a newly opened game is
+.BI \fR``\fPcreate " name"\fR'',\fP
+to request a new game named
+.IR name .
+The write request draws an error
+if
+.I gamesrv
+fails to find and load the requisite game
+engine.
+If the write succeeds, the game is created,
+and game updates can be read in the same
+manner as from the
+.B players
+file. The update messages are as follows:
+.RS
+.TP
+.BI playerid " clientid playerid name"
+Identifies the player identifier,
+.IR playerid ,
+and name,
+.IR name ,
+of the reader.
+.TP
+.BI create " objid parentid visibility objtype"
+Create an object, identified by
+.IR objid ,
+at the end of
+.IR parentid 's
+children
+.RI ( parentid
+is
+.B -1
+for the root object).
+.I Visibility
+is the visibility set of the object (see
+.IR gamesrv (2)),
+and
+.I objtype
+is its type.
+.TP
+.BI tx " srcid dstid start end index"
+Transfer objects from
+.I srcid
+to
+.IR dstid.
+Take the objects from the range
+.RI [ start ,\ end )
+in the children of
+.IR srcid ,
+and insert them just before
+.I index
+in
+.IR dstid .
+Note that when objects are transferred
+to an object that conceals its children,
+and the object is itself visible,
+the objects will first be transferred to the
+destination and then deleted; objects transferred
+out of such an object will first be created and
+.I then
+transferred to their destination.
+This enables a client to maintain some knowledge
+of where an object has been transferred to, even
+if the object is no longer visible.
+.TP
+.BI del " parentid start end"
+Delete the range
+.RI [ start ,\ end )
+of children from the object identified by
+.IR parentid .
+.I Gamesrv
+guarantees that those objects will themselves
+not have any children.
+.TP
+.BI set " objid attr val"
+Set the attribute named
+.I attr
+on object
+.I objid
+to
+.IR val .
+.TP
+.BI vis " objid visibility"
+The visibility of object
+.I objid
+has changed to
+.IR visibility .
+.TP
+.I action
+Game engines can generate arbitrary messages
+of their own devising; such messages are specific
+to particular client types.
+.PP
+Note that a given client does not have to interpret
+all the above messages \- different client types
+have their own conventions. The
+.B card
+client type uses most of the above functionality,
+for example, whereas a client for the
+.B chat
+engine listed in
+.IR gamesrv (2)
+can get away with interpreting only one message, the custom action
+.BR chat .
+.PP
+Writes to the opened game file
+are interpreted as game actions by
+the game that has been loaded, and acted on accordingly.
+Invalid actions will draw a write error.
+.RE
+.TP
+.I n
+Once a game has been created, it appears as
+a numbered file, corresponding to the
+.I gameid
+of the game in question.
+Opening this file joins the game; reads and writes
+work as for the
+.B new
+file, above.
+A single client cannot join a particular game
+more than once.
+.PP
+A zero-length write to any file causes any reads
+of that file from the same file descriptor to yield
+EOF (no bytes).
+This is necessary to force a hangup under
+systems such as Windows, where it is not possible
+to interrupt a kproc blocked on a network read.
+.SH EXAMPLE
+The simplest client!
+.PP
+.EX
+mount tcp!somehost.com!3242 /n/remote
+{
+ echo create chat >[1=0]
+ cat &
+ cat >[1=0] < /dev/cons
+} <> /n/remote/new
+.SH SOURCE
+.B /appl/cmd/games/gamesrv.b
+.SH SEE ALSO
+.IR gamesrv (2)
diff --git a/appl/spree/man/styxservers-nametree.man2 b/appl/spree/man/styxservers-nametree.man2
new file mode 100644
index 00000000..f64e519a
--- /dev/null
+++ b/appl/spree/man/styxservers-nametree.man2
@@ -0,0 +1,180 @@
+.TH STYXSERVERS-NAMETREE 2
+.SH NAME
+Styxservers: nametree \-
+hierarchical name storage for use with Styxservers.
+.SH SYNOPSIS
+.EX
+include "sys.m";
+include "styx.m";
+include "styxservers.m";
+nametree := load Nametree Nametree->PATH;
+ Tree: import nametree;
+
+Tree: adt {
+ create: fn(t: self ref Tree, parentpath: big, d: Sys->Dir): string;
+ remove: fn(t: self ref Tree, path: big): string;
+ wstat: fn(t: self ref Tree, path: big, d: Sys->Dir);
+ quit: fn(t: self ref Tree);
+};
+init: fn();
+start: fn(): (ref Tree, chan of ref Styxservers->Navop);
+.EE
+.SH DESCRIPTION
+.B Nametree
+provides the storage for a hierarchical namespace
+to be used by
+.IR styxservers (2).
+After the module is loaded, the
+.B init
+function should be called to
+initialise the module's internal variables.
+.B Start
+spawns a new
+.B nametree
+process; it returns a tuple, say
+.RI ( tree ,\ c ),
+where c is a channel that can be used to create
+an instance of
+.BR Styxservers->Navigator ,
+to access files inside
+.BR nametree ,
+and
+.I tree
+is an adt that allows creation and removal of those files.
+On failure, these functions return a string describing
+the error.
+.PP
+Note that the full set of operations on
+.B Nametree
+(i.e. stat, walk, readdir, wstate, create and remove),
+is only available in conjunction with
+.BR Styxserver 's
+.B Navigator
+interface.
+Files in the name space are ultimately identified by a 64-bit
+.I path
+value, which forms the path component of the file's Qid.
+(See
+.IR intro (5)
+for a description of the system's interpretation of Qids.)
+.PP
+The
+.B Tree
+operations
+are:
+.TP 10
+.IB t .create(\fIparentpath\fP,\ \fId\fP)
+Create a new file or directory.
+.I D
+gives the directory information that will be stored
+for the file, including its own path value,
+given by
+.IB d .qid.path .
+If the file referenced by
+.I parentpath
+does not exist, creation will not be allowed,
+other than in the special case when
+.IB d .qid.path
+is equal to
+.IR parentpath ,
+in which case it is assumed to be a root directory
+and may be created. This potentially allows a single
+.B Nametree
+instance to hold many distinct directory hierarchies.
+Note that no attempt is made to ensure that
+.I parentpath
+refers to a directory; the check is assumed to have
+been made previously.
+When a hierarchy is traversed,
+.B Nametree
+interprets the name
+.RB ` .. '
+itself as `parent directory', and that name should not be created explicitly.
+.TP
+.IB t .remove(\fIpath\fP)
+Remove the file referred to by
+.IR path ,
+and all its descendants.
+.TP
+.IB t .wstat(\fIpath\fP,\ \fId\fP)
+Change the directory information held on file
+.IR path .
+The Qid path itself cannot be changed by
+.IR d .
+.TP
+.IB t .quit()
+Shut down the
+.B nametree
+process.
+.SH EXAMPLE
+Here is a complete example that uses
+.B Nametree
+in conjunction with
+.B Styxservers
+in order to serve two files
+.B data
+and
+.BR ctl " ..."
+and do nothing with them:
+.EX
+implement Tst;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "styx.m";
+include "styxservers.m";
+ styxservers: Styxservers;
+ Styxserver, Navigator: import styxservers;
+ nametree: Nametree;
+ Tree: import nametree;
+
+Tst: module
+{
+ init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+Qroot, Qctl, Qdata: con big iota; # paths
+init(nil: ref Draw->Context, args: list of string)
+{
+ sys = load Sys Sys->PATH;
+ styx := load Styx Styx->PATH;
+ styx->init();
+ styxservers = load Styxservers Styxservers->PATH;
+ styxservers->init(styx);
+ nametree = load Nametree Nametree->PATH;
+ nametree->init();
+ sys->pctl(Sys->FORKNS, nil);
+ (tree, treeop) := nametree->start();
+ tree.create(Qroot, dir(".", 8r555|Sys->DMDIR, Qroot));
+ tree.create(Qroot, dir("ctl", 8r666, Qctl));
+ tree.create(Qroot, dir("data", 8r444, Qdata));
+ (tchan, srv) := Styxserver.new(sys->fildes(0),
+ Navigator.new(treeop), Qroot);
+ while((gm := <-tchan) != nil) {
+ # normally a pick on gm would act on
+ # Tmsg.Read and Tmsg.Write at least
+ srv.default(gm);
+ }
+ tree.quit();
+}
+
+dir(name: string, perm: int, qid: big): Sys->Dir
+{
+ d := sys->zerodir;
+ d.name = name;
+ d.uid = "me";
+ d.gid = "me";
+ d.qid.path = qid;
+ if (perm & Sys->DMDIR)
+ d.qid.qtype = Sys->QTDIR;
+ else
+ d.qid.qtype = Sys->QTFILE;
+ d.mode = perm;
+ return d;
+}
+.EE
+.SH SOURCE
+.B /appl/lib/nametree.b
+.SH SEE ALSO
+.IR styxservers (2),
+.IR intro (5)
diff --git a/appl/spree/man/styxservers.man2 b/appl/spree/man/styxservers.man2
new file mode 100644
index 00000000..fca2748a
--- /dev/null
+++ b/appl/spree/man/styxservers.man2
@@ -0,0 +1,902 @@
+.TH STYXSERVERS 2
+.SH NAME
+styxservers \-
+Styx server implementation assistance
+.SH SYNOPSIS
+.EX
+include "sys.m";
+include "styx.m";
+Tmsg, Rmsg: import Styx;
+include "styxservers.m";
+styxservers := load Styxservers Styxservers->PATH;
+Styxserver, Fid, Navigator: import styxservers;
+
+Styxserver: adt {
+ fd: ref Sys->FD; # file server end of connection
+ t: ref Navigator; # name space navigator for this server
+ msize: int; # negotiated Styx message size
+
+ new: fn(fd: ref Sys->FD, t: ref Navigator, rootpath: big)
+ :(chan of ref Tmsg, ref Styxserver);
+ reply: fn(srv: self ref Styxserver, m: ref Rmsg): int;
+
+ # protocol operations
+ attach: fn(srv: self ref Styxserver, m: ref Tmsg.Attach): ref Fid;
+ clunk: fn(srv: self ref Styxserver, m: ref Tmsg.Clunk): ref Fid;
+ walk: fn(srv: self ref Styxserver, m: ref Tmsg.Walk): ref Fid;
+ open: fn(srv: self ref Styxserver, m: ref Tmsg.Open): ref Fid;
+ read: fn(srv: self ref Styxserver, m: ref Tmsg.Read): ref Fid;
+ remove: fn(srv: self ref Styxserver, m: ref Tmsg.Remove): ref Fid;
+ stat: fn(srv: self ref Styxserver, m: ref Tmsg.Stat);
+ default: fn(srv: self ref Styxserver, gm: ref Tmsg);
+
+ # check validity
+ cancreate: fn(srv: self ref Styxserver, m: ref Tmsg.Create)
+ :(ref Fid, int, ref Sys->Dir, string);
+ canopen: fn(srv: self ref Styxserver, m: ref Tmsg.Open)
+ :(ref Fid, int, ref Sys->Dir, string);
+ canread: fn(srv: self ref Styxserver, m: ref Tmsg.Read)
+ :(ref Fid, string);
+ canwrite: fn(srv: self ref Styxserver, m: ref Tmsg.Write)
+ :(ref Fid, string);
+
+ # fid management
+ getfid: fn(srv: self ref Styxserver, fid: int): ref Fid;
+ newfid: fn(srv: self ref Styxserver, fid: int): ref Fid;
+ delfid: fn(srv: self ref Styxserver, c: ref Fid);
+ allfids: fn(srv: self ref Styxserver): list of ref Fid;
+
+ iounit: fn(srv: self ref Styxserver): int;
+};
+
+Fid: adt {
+ fid: int; # client's fid
+ path: big; # file's 64-bit unique path
+ qtype: int; # file's qid type (eg, Sys->QTDIR if directory)
+ isopen: int; # non-zero if file is open
+ mode: int; # if open, the open mode
+ uname: string; # user name from original attach
+ param: string; # attach aname from original attach
+ data: array of byte; # application data
+
+ clone: fn(f: self ref Fid, nf: ref Fid): ref Fid;
+ open: fn(f: self ref Fid, mode: int, qid: Sys->Qid);
+ walk: fn(f: self ref Fid, qid: Sys->Qid);
+};
+
+Navop: adt {
+ reply: chan of (ref Sys->Dir, string); # channel for reply
+ path: big; # file or directory path
+ pick {
+ Stat =>
+ Walk =>
+ name: string;
+ Readdir =>
+ offset: int; # index (origin 0) of first entry to return
+ count: int; # number of directory entries requested
+ }
+};
+
+Navigator: adt {
+ new: fn(c: chan of ref Navop): ref Navigator;
+ stat: fn(t: self ref Navigator, path: big): (ref Sys->Dir, string);
+ walk: fn(t: self ref Navigator, parent: big, name: string)
+ : (ref Sys->Dir, string);
+ readdir:fn(t: self ref Navigator, path: big,
+ offset, count: int): array of ref Sys->Dir;
+};
+
+init: fn(styx: Styx);
+traceset: fn(on: int);
+
+readbytes: fn(m: ref Styx->Tmsg.Read, d: array of byte):
+ ref Styx->Rmsg.Read;
+readstr: fn(m: ref Styx->Tmsg.Read, s: string):
+ ref Styx->Rmsg.Read;
+openok: fn(uname: string, omode,
+ perm: int, funame, fgname: string): int;
+openmode: fn(o: int): int;
+.EE
+.SH DESCRIPTION
+When writing a Styx file server, there are some
+commonly performed tasks that are
+fiddly or tedious to implement each time.
+.B Styxservers
+provides a framework to automate some of these
+routine tasks.
+In particular, it helps manage the fid space,
+implements common default processing for protocol messages,
+and assists walking around the
+directory hierarchy and reading of directories. Other
+tasks, such as defining the structure of the
+name space, and reading and writing files in it, are
+left to the file server program itself.
+Familiarity with Section 5 of the manual which defines the protocol
+(see
+.IR intro (5)),
+and with the representation of Styx messages in Limbo
+(see
+.IR styx (2)),
+is a prerequisite for use of this module.
+.PP
+.B Styxservers
+does not define or store any of the directory hierarchy itself;
+instead it queries an external process for information
+when necessary, through a value of type
+.BR Navigator ,
+which encapsulates communication with that process.
+That process must be started up
+independently of each
+.BR Styxserver ;
+a channel to such a process should be provided
+when starting a new
+.BR Styxserver .
+The channel carries messages of type
+.BR Navop .
+.IR Styxservers-nametree (2)
+provides a ready-made
+implementation of such a process that is sufficient for many applications.
+.PP
+.B Styxserver
+keeps tabs on the fids that are currently in use, and remembers
+some associated information, such as the Qid path
+of the file, whether it has been opened, etc.
+It does this using values of type
+.BR Fid .
+.PP
+Once the
+.B Styxservers
+module has been loaded,
+the
+.B init
+function must be called before anything else,
+to initialise its internal state. The
+.I styx
+argument should be an implementation of
+the
+.IR styx (2)
+module, which will be used to translate messages.
+Individual
+.B Styxserver
+instances do not share state, and are therefore
+independently thread-safe.
+.SS Fid representation
+.B Styxservers
+represents each active fid as a
+.B Fid
+value,
+which has the following public members:
+.TF param
+.TP
+.B fid
+The integer
+.I fid
+value provided by the client to refer to an active instance of a file in the file server,
+as described in
+.IR intro (5).
+.TP
+.B path
+The 64-bit qid path that uniquely identifies the file on the file server,
+as described in
+.IR intro (5).
+It is set by
+.IB f .walk
+and
+.IB f .open
+(see below).
+.TP
+.B qtype
+The file's qid type; it is
+.B Sys->QTDIR
+if and only if the fid refers to a directory.
+The value is set by
+.IB f .walk
+and
+.IB f .open
+(see below).
+.TP
+.B isopen
+Non-zero if and only if the fid has been opened by an
+.IR open (5)
+message.
+It is initially zero, and set by
+.IB f .open
+(see below).
+.TP
+.B mode
+Valid only if the fid has been opened.
+It has one of the values
+.BR Sys->OREAD ,
+.BR Sys->OWRITE ,
+.BR Sys->ORDWR ,
+possibly ORed with
+.BR Sys->ORCLOSE ,
+corresponding to the mode with which the file was opened.
+It is set by
+.IB f .open
+(see below).
+.TP
+.B uname
+The name of the user that created the fid.
+.TP
+.B param
+Set by
+.B Styxservers
+to the
+.B aname
+of the initial
+.IR attach (5)
+message,
+and subsequently inherited by each new fid created by
+.IR walk (5),
+but not otherwise used by
+.B Styxservers
+itself, and may be changed by the application.
+.TP
+.B data
+Unused by
+.BR Styxservers ;
+for application use.
+It might be used, for instance, to implement a file that gives different
+data to different clients.
+.TP
+.IB f .clone( nf )
+Copy the current state of all members of
+.I f
+except
+.IB f .fid\f1,\fP
+into
+.IR nf ,
+and return
+.IR nf .
+Used by
+.BR Styxserver.walk ,
+and is needed by an application only if it replaces that function.
+.TP
+.IB f .walk( qid )
+Make
+.I f
+refer to the file with the given
+.IR qid :
+set
+.IB f .path
+and
+.IB f .qtype
+from
+.IB qid .path
+and
+.IB qid .qtype .
+Used by
+.IB Styxserver.walk
+and is needed by an application only if it replaces that function.
+.TP
+.IB f .open( mode,\ qid )
+Mark
+.I f
+as `open',
+set
+.IR f .mode
+to
+.IR mode ,
+and set
+.B path
+and
+.B qtype
+to the path and type of
+.IR qid .
+Used by the
+implementations of
+.B open
+and
+.B create
+messages.
+The default implementation of
+.IR open (5)
+in
+.B Styxserver
+obtains the value of
+.I mode
+from
+.B Styxserver.canopen
+(below),
+and
+obtains the value of
+.I qid
+by querying the application's navigator.
+.SS Styxserver and file server state
+Each
+.B Styxserver
+value holds the state for a single file server, including its active fids,
+the link to the external name space process, and other internal data.
+Most of the state is manipulated through the member functions described below.
+The exceptions are two read-only values:
+the
+.B Navigator
+reference
+.IB srv .t
+which can be used to access that navigator; and
+the file descriptor
+.IB srv .fd
+that is the file server's end of the connection to the Styx client.
+Both values are initially provided by the file serving application,
+but can be accessed through the
+.B Styxserver
+value for convenience.
+The file descriptor value is normally used only through
+.BR Styxserver.reply ,
+but will be needed directly if the caller needs the file descriptor value
+as a parameter to
+.IR sys-pctl (2)
+when insulating the serving process's file descriptors from the surrounding environment.
+.PP
+The first set of functions in
+.B Styxserver
+provides common and default actions:
+.TP
+.B Styxserver.new(\fIfd\fP,\ \fIt\fP,\ \fIrootpath\fP)
+Create a new
+.BR Styxserver .
+It returns a tuple, say
+.RI ( c ", " srv ),
+and spawns a new process, which uses
+.IR styx (2)
+to read and parse Styx messages read
+from
+.IR fd ,
+and send them down
+.IR c ;
+.I t
+should be a
+.B Navigator
+adt which the
+.B Styxserver
+can use to answer queries
+on the name space (see ``Navigating file trees'', below).
+.I Rootpath
+gives the Qid path of the root of the served name space.
+.TP
+.IB srv .reply(\fIm\fP)
+Send a reply (R-message) to a client. The various utility methods,
+listed below, call this function to make their response.
+.TP
+.IB srv .attach(\fIm\fP)
+Respond to an
+.IR attach (5)
+message
+.IR m ,
+creating a new fid in the process, and returning it.
+Returns
+.B nil
+if
+.IB m .fid
+is a duplicate of an existing fid.
+The value of the attach parameter
+.IB m .aname
+is copied into the new fid's
+.B param
+field, as is the attaching user name,
+.IB m .uname .
+.TP
+.IB srv .clunk(\fIm\fP)
+Respond to a
+.IR clunk (5)
+message
+.IR m ,
+and return the old
+.BR Fid .
+Note that this does nothing about remove-on-close
+files; that should be programmed explicitly if needed.
+.TP
+.IB srv .walk(\fIm\fP)
+Respond to a
+.IR walk (5)
+message
+.IR m ,
+querying
+.IB srv . t
+for information on existing files.
+.TP
+.IB srv .open(\fIm\fP)
+Respond to an
+.IR open (5)
+message
+.IR m .
+This will allow a file to be opened if its permissions allow the
+specified mode of access.
+.TP
+.IB srv .read(\fIm\fP)
+Respond to a
+.IR read (5)
+message
+.IR m .
+If a directory is being read, the appropriate reply
+is made; for files, an error is given.
+.TP
+.IB srv .remove(\fIm\fP)
+Respond to a
+.IR remove (5)
+message
+.IR m
+with an error, clunking the fid as it does so,
+and returning the old
+.BR Fid .
+.TP
+.IB srv .stat(\fIm\fP)
+Respond to a
+.IR stat (5)
+message
+.IR m .
+.TP
+.IB srv .default(\fIgm\fP)
+Respond to an arbitrary T-message,
+.IR gm ,
+as appropriate (eg, by calling
+.IB srv .walk
+for a
+.IR walk (5)
+message).
+It responds appropriately to
+.IR version (5),
+and replies to
+.B Tauth
+(see
+.IR attach (5))
+stating that authentication is not required.
+Other messages without an associated
+.B Styxserver
+function are generally responded to
+with a ``permission denied'' error.
+.PP
+All the functions above check the validity of the fids, modes, counts and offsets
+in the messages, and automatically reply to the client with a suitable
+.IR error (5)
+message on error.
+.PP
+The following further
+.B Styxserver
+operations are useful
+in applications that override all or part of the default handling
+(in particular,
+to process read and write requests):
+.TP
+.IB srv .canopen( m )
+Check whether it is legal to open a file as requested by message
+.IR m :
+the fid is valid but not already open, the corresponding file exists and its
+permissions allow access in the requested mode, and if
+.B Sys->ORCLOSE
+is requested, the parent directory is writable (to allow the file to be removed when closed).
+.B Canopen
+returns a tuple, say
+.RI ( f ,\ mode ,\ d,\ err\ \fP).
+If the open request was invalid,
+.I f
+will be nil, and the string
+.I err
+will diagnose the error (for return to the client in an
+.B Rmsg.Error
+message).
+If the request was valid:
+.I f
+contains the
+.B Fid
+representing the file to be opened;
+.I mode
+is the access mode derived from
+.IB m .mode ,
+.BR Sys->OREAD ,
+.BR Sys->OWRITE ,
+.BR Sys->ORDWR ,
+ORed with
+.BR Sys->ORCLOSE ;
+.I d
+is a
+.B Dir
+value giving the file's attributes, obtained from the navigator;
+and
+.I err
+is nil.
+Once the application has done what it must to open the file,
+it must call
+.IB f .open
+to mark it open.
+.TP
+.IB srv .cancreate( m )
+Checks whether the
+creation of the file requested by
+message
+.I m
+is legal:
+the fid is valid but not open, refers to a directory,
+the permissions returned by
+.IR srv .t.stat
+show that directory is writable by the requesting user,
+the name does not already exist in that directory,
+and the mode with which the new file would be opened is valid.
+.B Cancreate
+returns a tuple, say
+.RI ( f ,\ mode,\ d,\ err\ \fP).
+If the creation request was invalid,
+.I f
+will be nil, and the string
+.I err
+will diagnose the error, for use in an error reply to the client.
+If the request was valid:
+.I f
+contains the
+.B Fid
+representing the parent directory;
+.I mode
+is the open mode as defined for
+.B canopen
+above;
+.I d
+is a
+.B Dir
+value containing some initial attributes for the new file or directory;
+and
+.I err
+is nil.
+The initial attributes set in
+.I d
+are:
+.IB d .name
+(the name of the file to be created);
+.IB d .uid
+and
+.IB d .muid
+(the user that did the initial attach);
+.IB d .gid ,
+.IB d .dtype ,
+.IB d .dev
+(taken from the parent directory's attributes);
+and
+.IB d .mode
+holds the file mode that should be attributed to the new
+file (taking into account the parent mode, as
+described in
+.IR open (5)).
+The caller must supply
+.IB d .qid
+once the file has successfully been created,
+and
+.IB d .atime
+and
+.IB d .mtime ;
+it must also call
+.IB f .open
+to mark
+.I f
+open and set its path to the file's path.
+If the file cannot be created successfully, the application should reply with
+an
+.IR error (5)
+message and leave
+.I f
+untouched.
+The
+.B Fid
+.I f
+will then continue to refer to the original directory, and remain unopened.
+.TP
+.IB srv .canread( m )
+Checks whether
+.IR read (5)
+message
+.I m
+refers to a valid fid that has been opened for reading,
+and that the count and file offset are non-negative.
+.B Canread
+returns a tuple, say
+.RI ( f ,\ err );
+if the attempted access is illegal,
+.I f
+will be nil, and
+.I err
+contains a description of the error,
+otherwise
+.I f
+contains the
+.B Fid
+corresponding to the file in question.
+It is typically called by an application's implementation of
+.B Tmsg.Read
+to obtain the
+.B Fid
+corresponding to the fid in the message, and check the access.
+.TP
+.IB srv .canwrite( m )
+Checks whether
+message
+.I m
+refers to a valid fid that has been opened for writing,
+and that the file offset is non-negative.
+.B Canwrite
+returns a tuple, say
+.RI ( f ,\ err );
+if the attempted access is illegal,
+.I f
+will be nil, and
+.I err
+contains a description of the error,
+otherwise
+.I f
+contains the
+.B Fid
+corresponding to the file in question.
+It is typically called by an application's implementation of
+.B Tmsg.Write
+to obtain the
+.B Fid
+corresponding to the fid in the message, and check the access.
+.TP
+.IB srv .iounit()
+Return an appropriate value for use as the
+.I iounit
+element in
+.B Rmsg.Open
+and
+.B Rmsg.Create
+replies,
+as defined in
+.IR open (5),
+based on the message size negotiated by the initial
+.IR version (5)
+message.
+.PP
+The remaining functions are normally used only by servers that need to
+override default actions.
+They maintain and access the mapping between a client's fid values presented in
+.B Tmsg
+messages and the
+.B Fid
+values that represent the corresponding files internally.
+.TP
+.IB srv .newfid(\fIfid\fP)
+Create a new
+.B Fid
+associated with number
+.I fid
+and return it.
+Return nil if the
+.I fid
+is already in use (implies a client error if the server correctly clunks fids).
+.TP
+.IB srv .getfid(\fIfid\fP)
+Get the
+.B Fid
+data associated with numeric id
+.IR fid ;
+return nil if there is none such (a malicious or erroneous client
+can cause this).
+.TP
+.IB srv .delfid(\fIfid\fP)
+Delete
+.I fid
+from the table of fids in the
+.BR Styxserver .
+(There is no error return.)
+.TP
+.IB srv .allfids()
+Return a list of all current fids (ie, the files currently active on the client).
+.PP
+.B Newfid
+is required when processing
+.IR auth (5),
+.IR attach (5)
+and
+.IR walk (5)
+messages to create new fids.
+.B Delfid
+is used to clunk fids when processing
+.IR clunk (5),
+.IR remove (5),
+and in a failed
+.IR walk (5)
+when it specified a new fid.
+All other messages should refer only to already existing fids, and the associated
+.B Fid
+data is fetched by
+.BR getfid .
+.SS Navigating file trees
+When a
+.B Styxserver
+instance needs to know about the namespace,
+it queries an external process through a channel
+by sending a
+.B Navop
+request;
+each such request carries with it a
+.B reply
+channel through which the
+reply should be made.
+The reply tuple has a reference to a
+.B Sys->Dir
+value that is non-nil on success, and a diagnostic string
+that is non-nil on error.
+.PP
+Files in the tree are referred to
+by their Qid
+.BR path .
+The requests are:
+.TF Walk
+.TP
+.BR Stat
+.br
+Find a file in the hierarchy by its
+.BR path ,
+and reply with the corresponding
+.B Dir
+data if found (or a diagnostic on error).
+.TP
+.BR Walk
+.br
+Look for file
+.B name
+in the directory with the given
+.BR path .
+.TP
+.BR Readdir
+.br
+Get information on selected files in the directory with the given
+.BR path .
+In this case, the reply channel is used to send
+a sequence of values, one for each entry in the directory, finishing with a tuple value
+.BR (nil,nil) .
+The entries to return are those selected by an
+.B offset
+that is the index (origin 0) of the first directory entry to return,
+and a
+.B count
+of a number of entries to return starting with that index.
+Note that both values are expressed in units of directory entries, not as byte counts.
+.PP
+.B Styxserver
+provides a
+.B Navigator
+adt to enable convenient access to this functionality; calls
+into the
+.B Navigator
+adt are bundled up into requests on the channel, and the
+reply returned.
+The functions provided are:
+.TP 10
+.BI Navigator.new( c )
+Create a new
+.BR Navigator ,
+sending requests down
+.IR c .
+.TP
+.IB t .stat(\fIpath\fP)
+Find the file with the given
+.IR path .
+Return a tuple
+.RI ( d ,\ err ),
+where
+.I d
+holds directory information for the file
+if found; otherwise
+.I err
+contains an error message.
+.TP
+.IB t .walk(\fIparent\fP,\ \fIname\fP)
+Find the file with name
+.I name
+inside parent directory
+.IR parent .
+Return a tuple as for
+.BR stat .
+.TP
+.IB t .readdir(\fIpath\fP,\ \fIoffset\fP,\ \fIcount\fP)
+Return directory data read from directory
+.IR path ,
+starting at entry
+.I offset
+for
+.I count
+entries.
+.SS Other functions
+The following functions provide some commonly used functionality:
+.TP 10
+.BI readbytes( m ,\ d )
+Assuming that the file in question contains data
+.IR d ,
+.B readbytes
+returns an appropriate reply to
+.IR read (5)
+message
+.IR m ,
+taking account of
+.IB m .offset
+and
+.IB m.count
+when extracting data from
+.IR d .
+.TP 10
+.BI readstr( m ,\ s )
+Assuming that the file in question contains string
+.IR s ,
+.B readstr
+returns an appropriate reply to
+.IR read (5)
+message
+.IR m ,
+taking account of
+.IB m .offset
+and
+.IB m.count
+when extracting data from the UTF-8 representation of
+.IR s .
+.TP
+.BI openok (\fIuname\fP,\ \fIomode\fP,\ \fIperm\fP,\ \fIfuid\fP,\ \fIfgid\fP)
+Does standard permission checking, assuming user
+.I uname
+is trying to open a file with access mode
+.IR omode ,
+where the file is owned by
+.IR fuid ,
+has group
+.IR fgid ,
+and permissions
+.IR perm .
+Returns true (non-zero) if permission would be granted, and false (zero) otherwise.
+.TP
+.BI openmode( o )
+Checks to see whether the open mode
+.I o
+is well-formed; if it is not,
+.B openmode
+returns -1; if it is, it returns the mode
+with OTRUNC and ORCLOSE flags removed.
+.TP
+.BI traceset( on )
+If
+.I on
+is true (non-zero),
+will trace Styx requests and replies, on standard error.
+This option must be set before creating a
+.BR Styxserver ,
+to ensure that it preserves its standard error descriptor.
+.SS Constants
+.B Styxservers
+defines a number of constants applicable to the writing
+of Styx servers, including:
+.TP
+.BR Einuse\fP,\fP\ Ebadfid\fP,\fP\ Eopen\fP,\fP\ Enotfound\fP,\fP\ Enotdir\fP,\fP\ Eperm\fP,\fP\ Ebadarg\fP,\fP\ Eexists
+These provide standard strings for commonly used error conditions,
+to be used in
+.B Rmsg.Error
+replies.
+.SS Authentication
+If authentication is required beyond that provided at the link level
+(for instance by
+.IR security-auth (2)),
+the server application must handle
+.B Tauth
+itself,
+remember the value of
+.I afid
+in that message, and generate an
+.B Rauth
+reply with a suitable Qid referring to a file with
+.B Qid.qtype
+of
+.BR QTAUTH .
+Following successful authentication by read and write on that file,
+it must associate that status with the
+.IR afid .
+Then, on a subsequent
+.B Tattach
+message, before calling
+.I srv .attach
+it must check that the
+.BR Tattach 's
+.I afid
+value corresponds to one previously authenticated, and
+reply with an appropriate error if not.
+.SH SOURCE
+.B /appl/lib/styxservers.b
+.SH SEE ALSO
+.IR styxservers-nametree (2),
+.IR sys-stat (2),
+.IR intro (5)
diff --git a/appl/spree/mkfile b/appl/spree/mkfile
new file mode 100644
index 00000000..ca94b6c0
--- /dev/null
+++ b/appl/spree/mkfile
@@ -0,0 +1,66 @@
+<../../mkconfig
+
+ENGINES=\
+ engines/afghan.dis \
+ engines/bounce.dis \
+ engines/canfield.dis \
+ engines/freecell.dis \
+ engines/gather.dis \
+ engines/lobby.dis \
+ engines/othello.dis \
+ engines/racingdemon.dis \
+ engines/spit.dis \
+ engines/spider.dis \
+ engines/whist.dis \
+
+CLIENTS=\
+ clients/cards.dis \
+ clients/gather.dis \
+ clients/lobby.dis \
+ clients/othello.dis \
+
+LIB=\
+ lib/allow.dis \
+ lib/cardlib.dis \
+ lib/commandline.dis \
+ lib/objstore.dis \
+ lib/tricks.dis \
+
+MAIN=\
+ archives.dis \
+ join.dis \
+ spree.dis \
+
+MODULES=\
+ sys.m\
+ draw.m\
+ tk.m\
+ tkclient.m\
+ styx.m\
+ styxservers.m\
+
+DEST=$ROOT/dis/spree
+
+ALL= ${ENGINES:%=$DEST/%} \
+ ${CLIENTS:%=$DEST/%} \
+ ${LIB:%=$DEST/%} \
+ ${MAIN:%=$DEST/%}
+
+all:V: $ENGINES $CLIENTS $LIB $MAIN
+
+install:V: $ALL
+
+$ROOT/dis/spree/%.dis: %.dis
+ cp $prereq $target
+
+%.dis: %.b
+ limbo -gw -I lib -o $stem.dis $stem.b
+
+$ENGINES $MAIN $LIB: spree.m gather.m lib/cardlib.m lib/allow.m lib/objstore.m
+$ENGINES $MAIN $CLIENTS $LIB: ${MODULES:%=$ROOT/module/%}
+
+clean:V:
+ rm -f *.dis *.sbl */*.dis */*.sbl
+
+nuke:V: clean
+ rm -f $DEST/*.dis $DEST/*/*.dis
diff --git a/appl/spree/other/tst.b b/appl/spree/other/tst.b
new file mode 100644
index 00000000..3b35fefa
--- /dev/null
+++ b/appl/spree/other/tst.b
@@ -0,0 +1,151 @@
+implement Tst;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect: import draw;
+include "bufio.m";
+ bufio: Bufio;
+ Iobuf: import bufio;
+
+Tst: module
+{
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+File: adt {
+ name: string;
+ fd: ref Sys->FD;
+ pid: int;
+};
+
+files: list of ref File;
+
+stderr: ref Sys->FD;
+outputch: chan of chan of string;
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ bufio = load Bufio Bufio->PATH;
+ sys->print(":cardtst\n");
+ stdin := bufio->fopen(sys->fildes(0), Sys->OREAD);
+ line := "";
+ currfd: ref Sys->FD;
+ outputch = chan of chan of string;
+ spawn outputproc();
+ while ((s := stdin.gets('\n')) != nil) {
+ if (len s > 1 && s[len s - 2] == '\\')
+ line += s[0:len s - 2] + "\n";
+ else {
+ s = line + s;
+ line = nil;
+ if (s[0] == ':') {
+ (nil, toks) := sys->tokenize(s, " \n");
+ case hd toks {
+ ":open" =>
+ if (tl toks == nil) {
+ sys->fprint(stderr, "usage: open file\n");
+ continue;
+ }
+ f := open(hd tl toks);
+ if (f != nil) {
+ currfd = f.fd;
+ sys->print("current file is now %s\n", f.name);
+ }
+ ":close" =>
+ if (tl toks == nil) {
+ sys->fprint(stderr, "usage: close file\n");
+ continue;
+ }
+ fl := files;
+ f: ref File;
+ for (files = nil; fl != nil; fl = tl fl) {
+ if ((hd fl).name == hd tl toks)
+ f = hd fl;
+ else
+ files = hd fl :: files;
+ }
+ if (f == nil) {
+ sys->fprint(stderr, "unknown file '%s'\n", hd tl toks);
+ continue;
+ }
+ sys->fprint(f.fd, "");
+ f = nil;
+ ":files" =>
+ for (fl := files; fl != nil; fl = tl fl) {
+ if ((hd fl).fd == currfd)
+ sys->print(":%s <--- current\n", (hd fl).name);
+ else
+ sys->print(":%s\n", (hd fl).name);
+ }
+ * =>
+ for (fl := files; fl != nil; fl = tl fl)
+ if ((hd fl).name == (hd toks)[1:])
+ break;
+ if (fl == nil) {
+ sys->fprint(stderr, "unknown file '%s'\n", (hd toks)[1:]);
+ continue;
+ }
+ currfd = (hd fl).fd;
+ }
+ } else if (currfd == nil)
+ sys->fprint(stderr, "no current file\n");
+ else if (len s > 1 && sys->fprint(currfd, "%s", s[0:len s - 1]) == -1)
+ sys->fprint(stderr, "command failed: %r\n");
+ }
+ }
+ for (fl := files; fl != nil; fl = tl fl)
+ kill((hd fl).pid);
+ outputch <-= nil;
+}
+
+open(f: string): ref File
+{
+ fd := sys->open("/n/remote/" + f, Sys->ORDWR);
+ if (fd == nil) {
+ sys->fprint(stderr, "cannot open %s: %r\n", f);
+ return nil;
+ }
+ sync := chan of int;
+ spawn updateproc(f, fd, sync);
+ files = ref File(f, fd, <-sync) :: files;
+ sys->print("opened %s\n", f);
+ return hd files;
+}
+
+updateproc(name: string, fd: ref Sys->FD, sync: chan of int)
+{
+ sync <-= sys->pctl(0, nil);
+ c := chan of string;
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(fd, buf, len buf)) > 0) {
+ (nt, toks) := sys->tokenize(string buf[0:n], "\n");
+ outputch <-= c;
+ c <-= "++ " + name + ":\n";
+ for (; toks != nil; toks = tl toks)
+ c <-= sys->sprint("+%s\n", hd toks);
+ c <-= nil;
+ }
+ if (n < 0)
+ sys->fprint(stderr, "cards: error reading %s: %r\n", name);
+ sys->fprint(stderr, "cards: updateproc (%s) exiting\n", name);
+}
+
+outputproc()
+{
+ for (;;) {
+ c := <-outputch;
+ if (c == nil)
+ exit;
+ while ((s := <-c) != nil)
+ sys->print("%s", s);
+ }
+}
+
+kill(pid: int)
+{
+ if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+ sys->write(fd, array of byte "kill", 4);
+}
+
diff --git a/appl/spree/other/tstboing.b b/appl/spree/other/tstboing.b
new file mode 100644
index 00000000..a599a0ab
--- /dev/null
+++ b/appl/spree/other/tstboing.b
@@ -0,0 +1,158 @@
+implement Tst;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect: import draw;
+include "sh.m";
+ sh: Sh;
+ Context: import Sh;
+include "math.m";
+ math: Math;
+ZERO: con 1e-6;
+
+stderr: ref Sys->FD;
+
+Tst: module {
+ init: fn(nil: ref Draw->Context, argv: list of string);
+};
+π: con Math->Pi;
+Maxδ: con π / 4.0;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ math = load Math Math->PATH;
+ if (len argv != 9) {
+ sys->fprint(stderr, "args?\n");
+ exit;
+ }
+ ar := argv2r(tl argv);
+ br := argv2r(tl tl tl tl tl argv);
+
+ a := Line.new(ar.min, ar.max); # ball
+ b := Line.new(br.min, br.max); # bat
+ (hit, hitp, s, t) := b.intersection(a.p, a.v);
+ if (hit) {
+ nv := boing(a.v, b);
+ rl := ref Line(hitp, nv, 50.0);
+ ballθ := a.θ();
+ batθ := b.θ();
+ φ := ballθ - batθ;
+ δ: real;
+ if (math->sin(φ) > 0.0)
+ δ = (t / b.s) * Maxδ * 2.0 - Maxδ;
+ else
+ δ = (t / b.s) * -Maxδ * 2.0 + Maxδ;
+ nl := Line.newpolar(rl.p, rl.θ() + δ, rl.s);
+ sys->print("%s %s %s\n", p2s(rl.point(0.0)), p2s(rl.point(rl.s)), p2s(nl.point(nl.s)));
+ } else
+ sys->fprint(stderr, "no hit\n");
+}
+
+argv2r(v: list of string): Rect
+{
+ r: Rect;
+ (r.min.x, v) = (int hd v, tl v);
+ (r.min.y, v) = (int hd v, tl v);
+ (r.max.x, v) = (int hd v, tl v);
+ (r.max.y, v) = (int hd v, tl v);
+ return r;
+}
+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;
+ θ: fn(b: self ref Line): real;
+ newpolar: fn(p: Realpoint, θ: real, s: real): ref Line;
+};
+
+Realpoint: adt {
+ x, y: real;
+};
+
+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;
+}
+
+Line.newpolar(p: Realpoint, θ: real, s: real): ref Line
+{
+ l := ref Line;
+ l.p = p;
+ l.s = s;
+ l.v = (math->cos(θ), math->sin(θ));
+ return l;
+}
+
+Line.θ(l: self ref Line): real
+{
+ return math->atan2(l.v.y, l.v.x);
+}
+
+# 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);
+}
+
+# bounce ball travelling in direction av off line b.
+# return the new unit vector.
+boing(av: Realpoint, b: ref Line): Realpoint
+{
+ d := math->atan2(real b.v.y, real b.v.x) * 2.0 - math->atan2(av.y, av.x);
+ return (math->cos(d), math->sin(d));
+}
+
+p2s(p: Point): string
+{
+ return string p.x + " " + string p.y;
+}
+
diff --git a/appl/spree/other/tstlines.sh b/appl/spree/other/tstlines.sh
new file mode 100755
index 00000000..7e75b3a4
--- /dev/null
+++ b/appl/spree/other/tstlines.sh
@@ -0,0 +1,53 @@
+#!/dis/sh
+load tk std
+pctl newpgrp
+wid=${tk window 'Test lines'}
+fn x {tk $wid $*}
+x canvas .c
+x pack .c
+x 'bind .c <ButtonRelease-1> {send b1 %x %y}'
+x 'bind .c <ButtonRelease-2> {send b2 %x %y}'
+x update
+chan b1 b2
+tk namechan $wid b1
+tk namechan $wid b2
+while {} {tk winctl $wid ${recv $wid}} &
+chan show
+ifs='
+'
+v1 := 0 0 1 1
+v2 := 1 1 2 2
+while {} {
+ args:=${split ${recv show}}
+ (t args) = $args
+ $t = $args
+
+ tk 0 .c delete lines
+ echo $v1 $v2
+ r := `{tstboing $v1 $v2}
+ (ap1x ap1y ap2x ap2y bp1x bp1y bp2x bp2y) := $v1 $v2
+ tk 0 .c create line $ap1x $ap1y $ap2x $ap2y -tags lines -fill black -width 3 -arrow last
+ tk 0 .c create line $bp1x $bp1y $bp2x $bp2y -tags lines -fill red
+ and {~ $#r 6} {
+ (rp1x rp1y rp2x rp2y sp2x sp2y) := $r
+ tk 0 .c create line $ap2x $ap2y $rp1x $rp1y -tags lines -fill black
+ tk 0 .c create line $rp1x $rp1y $rp2x $rp2y -tags lines -fill green -arrow last
+ tk 0 .c create line $rp1x $rp1y $sp2x $sp2y -tags lines -fill blue -arrow last
+ }
+ tk 0 update
+} &
+
+fn show {
+ a:=$*
+ if {~ $#a 8} {echo usage} {
+ send show ${join ' ' $a}
+ }
+}
+
+for i in 1 2 {
+ while {} {
+ p1:=${recv b^$i}
+ p2:=${recv b^$i}
+ send show ${join ' ' v^$i $p1 $p2}
+ } &
+}
diff --git a/appl/spree/other/tstwin.b b/appl/spree/other/tstwin.b
new file mode 100644
index 00000000..de7c7ab4
--- /dev/null
+++ b/appl/spree/other/tstwin.b
@@ -0,0 +1,351 @@
+implement Tstwin;
+
+include "sys.m";
+ sys: Sys;
+
+include "draw.m";
+ draw: Draw;
+ Context, Display, Point, Rect, Image, Screen: import draw;
+
+include "tk.m";
+ tk: Tk;
+ Toplevel: import tk;
+
+include "tkclient.m";
+ tkclient: Tkclient;
+
+include "math.m";
+ math: Math;
+
+Tstwin: module
+{
+ init: fn(ctxt: ref Context, argv: list of string);
+};
+
+screen: ref Screen;
+display: ref Display;
+win: ref Toplevel;
+
+NC: con 6;
+
+task_cfg := array[] of {
+"label .xy -text {0 0}",
+"canvas .c -height 500 -width 500",
+"pack .xy -side top -fill x",
+"pack .c -side bottom -fill both -expand 1",
+"bind .c <ButtonRelease-1> {send cmd 0 1 %x %y}",
+"bind .c <ButtonRelease-2> {send cmd 0 2 %x %y}",
+"bind .c <Button-1> {send cmd 1 1 %x %y}",
+"bind .c <Button-2> {send cmd 1 2 %x %y}",
+};
+
+Obstacle: adt {
+ line: ref Line;
+ s1, s2: real;
+ id: int;
+ config: fn(b: self ref Obstacle);
+ new: fn(id: int): ref Obstacle;
+};
+
+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;
+};
+bats: list of ref Obstacle;
+init(ctxt: ref 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;
+ math = load Math Math->PATH;
+
+ sys->pctl(Sys->NEWPGRP, nil);
+
+ display = ctxt.display;
+ screen = ctxt.screen;
+
+ tkclient->init();
+
+ menubut: chan of string;
+ (win, menubut) = tkclient->toplevel(screen, nil, "Window testing", 0);
+
+ cmd := chan of string;
+ tk->namechan(win, cmd, "cmd");
+
+ tkclient->tkcmds(win, task_cfg);
+
+ mch := chan of (int, Point);
+ spawn mouseproc(mch);
+
+ bat := Obstacle.new(0);
+ bats = bat :: nil;
+ bat.line = Line.new((100, 0), (150, 500));
+ bat.s1 = 10.0;
+ bat.s2 = 110.0;
+ bat.config();
+
+ tk->cmd(win, "update");
+ buts := 0;
+ for(;;) alt {
+ menu := <-menubut =>
+ tkclient->wmctl(win, menu);
+
+ c := <-cmd =>
+ (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));
+ }
+}
+
+Realpoint: adt {
+ x, y: real;
+};
+
+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;
+}
+
+p2s(p: Point): string
+{
+ return string p.x + " " + string p.y;
+}
+
+mouseproc(mch: chan of (int, Point))
+{
+ 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)
+{
+ line := hitbat.line;
+ batlen := hitbat.s2 - hitbat.s1;
+
+ cvsorigin := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+
+# cmd(win, "grab set .c");
+# cmd(win, "focus .");
+loop: for (;;) alt {
+ (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");
+ if (!buts)
+ break loop;
+ }
+# cmd(win, "grab release .c");
+}
+
+CHARGETIME: con 1000.0;
+MAXCHARGE: con 50.0;
+
+α: con 0.999; # decay in one millisecond
+Max: con 60.0;
+D: con 5;
+ZERO: con 1e-6;
+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();
+ delta := 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");
+ cmd(win, ".c create oval 0 0 1 1 -fill green -tags ball");
+ p2: Point;
+ buts := 2;
+ for (;;) {
+ v := makeunit(delta);
+ bp := pivot.add((int (v.x * charge), int (v.y * charge)));
+ cmd(win, ".c coords wire "+s1+" "+p2s(bp)+" "+s2);
+ cmd(win, ".c coords ball "+string (bp.x - D) + " " + string (bp.y - D) + " " +
+ string (bp.x + D) + " " + string (bp.y + D));
+ 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;
+ delta.x = delta.x * fade + real (p2.x - p.x);
+ delta.y = delta.y * fade + real (p2.y - p.y);
+ mag := delta.x * delta.x + delta.y * delta.y;
+ win.image.display.cursorset(p.add(cvsorigin));
+ }
+ sys->print("pow\n");
+ cmd(win, ".c delete wire ball");
+ cmd(win, "update");
+ 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);
+}
+
+#drag(mch: chan of (int, Point), p: Point)
+#{
+# down := 1;
+# cvsorigin := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+# ms := sys->millisec();
+# delta := Realpoint(0.0, 0.0);
+# id := cmd(win, ".c create line " + p2s(p) + " " + p2s(p));
+# coords := ".c coords " + id + " " + p2s(p) + " ";
+# do {
+# p2: Point;
+# (down, p2) = <-mch;
+# now := sys->millisec();
+# fade := math->pow(α, real (now - ms));
+# ms = now;
+# delta.x = delta.x * fade + real (p2.x - p.x);
+# delta.y = delta.y * fade + real (p2.y - p.y);
+# mag := delta.x * delta.x + delta.y * delta.y;
+# d: Realpoint;
+# if (mag > Max * Max) {
+# fade = Max / math->sqrt(mag);
+# d = (delta.x * fade, delta.y * fade);
+# } else
+# d = delta;
+#
+# cmd(win, coords + p2s(p.add((int d.x, int d.y))));
+# win.image.display.cursorset(p.add(cvsorigin));
+# cmd(win, "update");
+# } while (down);
+#}
+#
+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);
+}
+
+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;
+ return o;
+}
+
+Obstacle.config(o: self ref Obstacle)
+{
+ cmd(win, ".c coords l" + string o.id + " " +
+ p2s(o.line.point(o.s1)) + " " + p2s(o.line.point(o.s2)));
+ cmd(win, ".c itemconfigure l" + string o.id + " -fill red");
+}
+
+abs(x: real): real
+{
+ if (x < 0.0)
+ return -x;
+ return x;
+}
diff --git a/appl/spree/spree.b b/appl/spree/spree.b
new file mode 100644
index 00000000..bd6a0aed
--- /dev/null
+++ b/appl/spree/spree.b
@@ -0,0 +1,1554 @@
+implement Spree;
+
+include "sys.m";
+ sys: Sys;
+include "readdir.m";
+ readdir: Readdir;
+include "styx.m";
+ Rmsg, Tmsg: import Styx;
+include "styxservers.m";
+ styxservers: Styxservers;
+ Styxserver, Fid, Eperm, Navigator: import styxservers;
+ nametree: Nametree;
+include "draw.m";
+include "arg.m";
+include "sets.m";
+ sets: Sets;
+ Set, set, A, B, All, None: import sets;
+include "spree.m";
+ archives: Archives;
+ Archive: import archives;
+
+stderr: ref Sys->FD;
+myself: Spree;
+
+Debug: con 0;
+Update: adt {
+ pick {
+ Set =>
+ o: ref Object;
+ objid: int; # member-specific id
+ attr: ref Attribute;
+ Transfer =>
+ srcid: int; # parent object
+ from: Range; # range within src to transfer
+ dstid: int; # destination object
+ index: int; # insertion point
+ Create =>
+ objid: int;
+ parentid: int;
+ visibility: Sets->Set;
+ objtype: string;
+ Delete =>
+ parentid: int;
+ r: Range;
+ objs: array of int;
+ Setvisibility =>
+ objid: int;
+ visibility: Sets->Set; # set of members that can see it
+ Action =>
+ s: string;
+ objs: list of int;
+ rest: string;
+ Break =>
+ # break in transmission
+ }
+};
+
+T: type ref Update;
+Queue: adt {
+ h, t: list of T;
+ put: fn(q: self ref Queue, s: T);
+ get: fn(q: self ref Queue): T;
+ isempty: fn(q: self ref Queue): int;
+ peek: fn(q: self ref Queue): T;
+};
+
+Openfid: adt {
+ fid: int;
+ uname: string;
+ fileid: int;
+ member: ref Member; # nil for non-clique files.
+ updateq: ref Queue;
+ readreq: ref Tmsg.Read;
+ hungup: int;
+ # alias: string; # could use this to allow a member to play themselves
+
+ new: fn(fid: ref Fid, file: ref Qfile): ref Openfid;
+ find: fn(fid: int): ref Openfid;
+ close: fn(fid: self ref Openfid);
+# cmd: fn(fid: self ref Openfid, cmd: string): string;
+};
+
+Qfile: adt {
+ id: int; # index into files array
+ owner: string;
+ qid: Sys->Qid;
+ ofids: list of ref Openfid; # list of all fids that are holding this open
+ needsupdate: int; # updates have been added since last updateall
+
+ create: fn(parent: big, d: Sys->Dir): ref Qfile;
+ delete: fn(f: self ref Qfile);
+};
+
+# which updates do we send even though the clique isn't yet started?
+alwayssend := array[] of {
+ tagof(Update.Set) => 0,
+ tagof(Update.Transfer) => 0,
+ tagof(Update.Create) => 0,
+ tagof(Update.Delete) => 0,
+ tagof(Update.Setvisibility) => 0,
+ tagof(Update.Action) => 1,
+ tagof(Update.Break) => 1,
+};
+
+srv: ref Styxserver;
+tree: ref Nametree->Tree;
+cliques: array of ref Clique;
+qfiles: array of ref Qfile;
+fids := array[47] of list of ref Openfid; # hash table
+lobby: ref Clique;
+Qroot: big;
+sequence := 0;
+
+fROOT,
+fGAME,
+fNAME,
+fGAMEDIR,
+fGAMEDATA: con iota;
+
+GAMEDIR: con "/n/remote";
+ENGINES: con "/dis/spree/engines";
+ARCHIVEDIR: con "/lib/spreearchive";
+
+badmod(p: string)
+{
+ sys->fprint(stderr, "spree: cannot load %s: %r\n", p);
+ raise "fail:bad module";
+}
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ myself = load Spree "$self";
+
+ styx := load Styx Styx->PATH;
+ if (styx == nil)
+ badmod(Styx->PATH);
+ styx->init();
+
+ styxservers = load Styxservers Styxservers->PATH;
+ if (styxservers == nil)
+ badmod(Styxservers->PATH);
+ styxservers->init(styx);
+
+ nametree = load Nametree Nametree->PATH;
+ if (nametree == nil)
+ badmod(Nametree->PATH);
+ nametree->init();
+
+ sets = load Sets Sets->PATH;
+ if (sets == nil)
+ badmod(Sets->PATH);
+ sets->init();
+
+ readdir = load Readdir Readdir->PATH;
+ if (readdir == nil)
+ badmod(Readdir->PATH);
+
+ archives = load Archives Archives->PATH;
+ if (archives == nil)
+ badmod(Archives->PATH);
+ archives->init(myself);
+
+ initrand();
+
+ navop: chan of ref Styxservers->Navop;
+ (tree, navop) = nametree->start();
+ tchan: chan of ref Tmsg;
+ Qroot = mkqid(fROOT, 0);
+ (tchan, srv) = Styxserver.new(sys->fildes(0), Navigator.new(navop), Qroot);
+ nametree->tree.create(Qroot, dir(Qroot, ".", 8r555|Sys->DMDIR, "spree"));
+ nametree->tree.create(Qroot, dir(mkqid(fNAME, 0), "name", 8r444, "spree"));
+ (lobbyid, nil, err) := lobby.new(ref Archive("lobby" :: nil, nil, nil, nil), "spree");
+ if (lobbyid == -1) {
+ sys->fprint(stderr, "spree: couldn't start lobby: %s\n", err);
+ raise "fail:no lobby";
+ }
+ sys->pctl(Sys->FORKNS, nil);
+ for (;;) {
+ gm := <-tchan;
+ if (gm == nil || tagof(gm) == tagof(Tmsg.Readerror)) {
+ if (gm != nil) {
+ pick m := gm {
+ Readerror =>
+ sys->print("spree: read error: %s\n", m.error);
+ }
+ }
+ sys->print("spree: exiting\n");
+ exit;
+ } else {
+ e := handletmsg(gm);
+ if (e != nil)
+ srv.reply(ref Rmsg.Error(gm.tag, e));
+ }
+ }
+}
+
+
+dir(qidpath: big, name: string, perm: int, owner: string): Sys->Dir
+{
+ DM2QT: con 24;
+ d := Sys->zerodir;
+ d.name = name;
+ d.uid = owner;
+ d.gid = owner;
+ d.qid.path = qidpath;
+ d.qid.qtype = (perm >> DM2QT) & 16rff;
+ d.mode = perm;
+ # d.atime = now;
+ # d.mtime = now;
+ return d;
+}
+
+handletmsg(tmsg: ref Tmsg): string
+{
+ pick m := tmsg {
+ Open =>
+ (fid, omode, d, err) := srv.canopen(m);
+ if (fid == nil)
+ return err;
+ if (d.qid.qtype & Sys->QTDIR) {
+ srv.default(m);
+ return nil;
+ }
+ case qidkind(d.qid.path) {
+ fGAMEDATA =>
+ fid.open(m.mode, Sys->Qid(fid.path, fid.qtype, 0));
+ srv.reply(ref Rmsg.Open(m.tag, Sys->Qid(fid.path, fid.qtype, 0), 0));
+ fGAME =>
+ f := qid2file(d.qid.path);
+ if (f == nil)
+ return "cannot find qid";
+ ofid := Openfid.new(fid, f);
+ err = openfile(ofid);
+ if (err != nil) {
+ ofid.close();
+ return err;
+ }
+ fid.open(m.mode, f.qid);
+ srv.reply(ref Rmsg.Open(m.tag, Sys->Qid(fid.path, fid.qtype, 0), 0));
+ * =>
+ srv.default(m);
+ }
+ updateall();
+ Read =>
+ (fid, err) := srv.canread(m);
+ if (fid == nil)
+ return err;
+ if (fid.qtype & Sys->QTDIR) {
+ srv.default(m);
+ return nil;
+ }
+ case qidkind(fid.path) {
+ fGAMEDATA =>
+ f := qidindex(fid.path);
+ id := f & 16rffff;
+ f = (f >> 16) & 16rffff;
+ data := cliques[id].mod->readfile(f, m.offset, m.count);
+ srv.reply(ref Rmsg.Read(m.tag, data));
+ fGAME =>
+ ff := Openfid.find(m.fid);
+ if (ff.readreq != nil)
+ return "duplicate read";
+ ff.readreq = m;
+ sendupdate(ff);
+ fNAME =>
+ srv.reply(styxservers->readstr(m, fid.uname));
+ * =>
+ return "darn rats!";
+ }
+ Write =>
+ (fid, err) := srv.canwrite(m);
+ if (fid == nil)
+ return err;
+ ff := Openfid.find(m.fid);
+ err = command(ff, string m.data);
+ if (err != nil) {
+ updateall();
+ return err;
+ }
+ srv.reply(ref Rmsg.Write(m.tag, len m.data));
+ updateall(); # XXX might we need to do this on error too?
+ Clunk =>
+ fid := srv.clunk(m);
+ if (fid != nil) {
+ clunked(fid);
+ updateall();
+ }
+ Flush =>
+ for (i := 0; i < len qfiles; i++) {
+ if (qfiles[i] == nil)
+ continue;
+ for (ol := qfiles[i].ofids; ol != nil; ol = tl ol) {
+ ofid := hd ol;
+ if (ofid.readreq != nil && ofid.readreq.tag == m.oldtag)
+ ofid.readreq = nil;
+ }
+ }
+ srv.reply(ref Rmsg.Flush(m.tag));
+# Removed => clunked too.
+ * =>
+ srv.default(tmsg);
+ }
+ return nil;
+}
+
+clunked(fid: ref Fid)
+{
+ if (!fid.isopen || (fid.qtype & Sys->QTDIR))
+ return;
+ ofid := Openfid.find(fid.fid);
+ if (ofid == nil)
+ return;
+ if (ofid.member != nil)
+ memberleaves(ofid.member);
+ ofid.close();
+ f := qfiles[ofid.fileid];
+ # if it's the last close, and clique is hung up, then remove clique from
+ # directory hierarchy.
+ if (f.ofids == nil && qidkind(f.qid.path) == fGAME) {
+ g := cliques[qidindex(f.qid.path)];
+ if (g.hungup) {
+ stopclique(g);
+ nametree->tree.remove(mkqid(fGAMEDIR, g.id));
+ f.delete();
+ cliques[g.id] = nil;
+ }
+ }
+}
+
+mkqid(kind, i: int): big
+{
+ return big kind | (big i << 4);
+}
+
+qidkind(qid: big): int
+{
+ return int (qid & big 16rf);
+}
+
+qidindex(qid: big): int
+{
+ return int (qid >> 4);
+}
+
+qid2file(qid: big): ref Qfile
+{
+ for (i := 0; i < len qfiles; i++) {
+ f := qfiles[i];
+ if (f != nil && f.qid.path == qid)
+ return f;
+ }
+ return nil;
+}
+
+Qfile.create(parent: big, d: Sys->Dir): ref Qfile
+{
+ nametree->tree.create(parent, d);
+ for (i := 0; i < len qfiles; i++)
+ if (qfiles[i] == nil)
+ break;
+ if (i == len qfiles)
+ qfiles = (array[len qfiles + 1] of ref Qfile)[0:] = qfiles;
+ f := qfiles[i] = ref Qfile(i, d.uid, d.qid, nil, 0);
+ return f;
+}
+
+Qfile.delete(f: self ref Qfile)
+{
+ nametree->tree.remove(f.qid.path);
+ qfiles[f.id] = nil;
+}
+
+Openfid.new(fid: ref Fid, file: ref Qfile): ref Openfid
+{
+ i := fid.fid % len fids;
+ ofid := ref Openfid(fid.fid, fid.uname, file.id, nil, ref Queue, nil, 0);
+ fids[i] = ofid :: fids[i];
+ file.ofids = ofid :: file.ofids;
+ return ofid;
+}
+
+Openfid.find(fid: int): ref Openfid
+{
+ for (ol := fids[fid % len fids]; ol != nil; ol = tl ol)
+ if ((hd ol).fid == fid)
+ return hd ol;
+ return nil;
+}
+
+Openfid.close(ofid: self ref Openfid)
+{
+ i := ofid.fid % len fids;
+ newol: list of ref Openfid;
+ for (ol := fids[i]; ol != nil; ol = tl ol)
+ if (hd ol != ofid)
+ newol = hd ol :: newol;
+ fids[i] = newol;
+ newol = nil;
+ for (ol = qfiles[ofid.fileid].ofids; ol != nil; ol = tl ol)
+ if (hd ol != ofid)
+ newol = hd ol :: newol;
+ qfiles[ofid.fileid].ofids = newol;
+}
+
+openfile(ofid: ref Openfid): string
+{
+ name := ofid.uname;
+ f := qfiles[ofid.fileid];
+ if (qidkind(f.qid.path) == fGAME) {
+ if (cliques[qidindex(f.qid.path)].hungup)
+ return "hungup";
+ i := 0;
+ for (o := f.ofids; o != nil; o = tl o) {
+ if ((hd o) != ofid && (hd o).uname == name)
+ return "you cannot join a clique twice";
+ i++;
+ }
+ if (i > MAXPLAYERS)
+ return "too many members";
+ }
+ return nil;
+}
+
+# process a client's command; return a non-nil string on error.
+command(ofid: ref Openfid, cmd: string): string
+{
+ err: string;
+ f := qfiles[ofid.fileid];
+ qid := f.qid.path;
+ if (ofid.hungup)
+ return "hung up";
+ if (cmd == nil) {
+ ofid.hungup = 1;
+ sys->print("hanging up file %s for user %s, fid %d\n", nametree->tree.getpath(f.qid.path), ofid.uname, ofid.fid);
+ return nil;
+ }
+ case qidkind(qid) {
+ fGAME =>
+ clique := cliques[qidindex(qid)];
+ if (ofid.member == nil)
+ err = newmember(clique, ofid, cmd);
+ else
+ err = cliquerequest(clique, ref Rq.Command(ofid.member, cmd));
+ * =>
+ err = "invalid command " + string qid; # XXX dud error message
+ }
+ return err;
+}
+
+Clique.notify(src: self ref Clique, dstid: int, cmd: string)
+{
+ if (cmd == nil)
+ return; # don't allow faking of clique exit.
+ if (dstid < 0 || dstid >= len cliques) {
+ if (dstid != -1)
+ sys->fprint(stderr, "%d cannot notify invalid %d: '%s'\n", src.id, dstid, cmd);
+ return;
+ }
+ dst := cliques[dstid];
+ if (dst.parentid != src.id && dstid != src.parentid) {
+ sys->fprint(stderr, "%d cannot notify %d: '%s'\n", src.id, dstid, cmd);
+ return;
+ }
+ src.notes = (src.id, dstid, cmd) :: src.notes;
+}
+
+# add a new member to a clique.
+# it should already have been checked that the member's name
+# isn't a duplicate of another in the same clique.
+newmember(clique: ref Clique, ofid: ref Openfid, cmd: string): string
+{
+ name := ofid.uname;
+
+ # check if member was suspended, and give them their old id back
+ # if so, otherwise find first free id.
+ for (s := clique.suspended; s != nil; s = tl s)
+ if ((hd s).name == name)
+ break;
+ id: int;
+ suspended := 0;
+ member: ref Member;
+ if (s != nil) {
+ member = hd s;
+ # remove from suspended list
+ q := tl s;
+ for (t := clique.suspended; t != s; t = tl t)
+ q = hd t :: q;
+ clique.suspended = q;
+ suspended = 1;
+ member.suspended = 0;
+ } else {
+ for (id = 0; clique.memberids.holds(id); id++)
+ ;
+ member = ref Member(id, clique.id, nil, nil, nil, name, 0, 0);
+ clique.memberids = clique.memberids.add(member.id);
+ }
+
+ q := ofid.updateq;
+ ofid.member = member;
+
+ started := clique.started;
+ err := cliquerequest(clique, ref Rq.Join(member, cmd, suspended));
+ if (err != nil) {
+ member.del(0);
+ if (suspended) {
+ member.suspended = 1;
+ clique.suspended = member :: clique.suspended;
+ }
+ return err;
+ }
+ if (started) {
+ qrecreateobject(q, member, clique.objects[0], nil);
+ qfiles[ofid.fileid].needsupdate = 1;
+ }
+ member.updating = 1;
+ return nil;
+}
+
+Clique.start(clique: self ref Clique)
+{
+ if (clique.started)
+ return;
+
+ for (ol := qfiles[clique.fileid].ofids; ol != nil; ol = tl ol)
+ if ((hd ol).member != nil)
+ qrecreateobject((hd ol).updateq, (hd ol).member, clique.objects[0], nil);
+ clique.started = 1;
+}
+
+Blankclique: Clique;
+maxcliqueid := 0;
+Clique.new(parent: self ref Clique, archive: ref Archive, owner: string): (int, string, string)
+{
+ for (id := 0; id < len cliques; id++)
+ if (cliques[id] == nil)
+ break;
+ if (id == len cliques)
+ cliques = (array[len cliques + 1] of ref Clique)[0:] = cliques;
+
+ mod := load Engine ENGINES +"/" + hd archive.argv + ".dis";
+ if (mod == nil)
+ return (-1, nil, sys->sprint("cannot load engine: %r"));
+
+ dirq := mkqid(fGAMEDIR, id);
+ fname := string maxcliqueid++;
+ e := nametree->tree.create(Qroot, dir(dirq, fname, 8r555|Sys->DMDIR, owner));
+ if (e != nil)
+ return (-1, nil, e);
+ f := Qfile.create(dirq, dir(mkqid(fGAME, id), "ctl", 8r666, owner));
+ objs: array of ref Object;
+ if (archive.objects != nil) {
+ objs = archive.objects;
+ for (i := 0; i < len objs; i++)
+ objs[i].cliqueid = id;
+ } else
+ objs = array[] of {ref Object(0, Attributes.new(), All, -1, nil, id, nil)};
+
+ memberids := None;
+ suspended: list of ref Member;
+ for (i := 0; i < len archive.members; i++) {
+ suspended = ref Member(i, id, nil, nil, nil, archive.members[i], 0, 1) :: suspended;
+ memberids = memberids.add(i);
+ }
+
+ archive = ref *archive;
+ archive.objects = nil;
+
+ g := cliques[id] = ref Clique(
+ id, # id
+ f.id, # fileid
+ fname, # fname
+ objs, # objects
+ archive, # archive
+ nil, # freelist
+ mod, # mod
+ memberids, # memberids
+ suspended,
+ chan of ref Rq, # request
+ chan of string, # reply
+ 0, # hungup
+ 0, # started
+ -1, # parentid
+ nil # notes
+ );
+ if (parent != nil) {
+ g.parentid = parent.id;
+ g.notes = parent.notes;
+ }
+ spawn cliqueproc(g);
+ e = cliquerequest1(g, ref Rq.Init);
+ if (e != nil) {
+ stopclique(g);
+ nametree->tree.remove(dirq);
+ f.delete();
+ cliques[id] = nil;
+ return (-1, nil, e);
+ }
+ # only send notifications if the clique was successfully created, otherwise
+ # pretend it never existed.
+ if (parent != nil) {
+ parent.notes = g.notes;
+ g.notes = nil;
+ }
+ return (g.id, fname, nil);
+}
+
+# as a special case, if parent is nil, we use the root object.
+Clique.newobject(clique: self ref Clique, parent: ref Object, visibility: Set, objtype: string): ref Object
+{
+ if (clique.freelist == nil)
+ (clique.objects, clique.freelist) =
+ makespace(clique.objects, clique.freelist);
+ id := hd clique.freelist;
+ clique.freelist = tl clique.freelist;
+
+ if (parent == nil)
+ parent = clique.objects[0];
+ obj := ref Object(id, Attributes.new(), visibility, parent.id, nil, clique.id, objtype);
+
+ n := len parent.children;
+ newchildren := array[n + 1] of ref Object;
+ newchildren[0:] = parent.children;
+ newchildren[n] = obj;
+ parent.children = newchildren;
+ clique.objects[id] = obj;
+ applycliqueupdate(clique, ref Update.Create(id, parent.id, visibility, objtype), All);
+ if (Debug)
+ sys->print("new %d, parent %d, visibility %s\n", obj.id, parent.id, visibility.str());
+ return obj;
+}
+
+Clique.hangup(clique: self ref Clique)
+{
+ if (clique.hungup)
+ return;
+sys->print("clique.hangup(%s)\n", clique.fname);
+ f := qfiles[clique.fileid];
+ for (ofids := f.ofids; ofids != nil; ofids = tl ofids)
+ (hd ofids).hungup = 1;
+ f.needsupdate = 1;
+ clique.hungup = 1;
+ if (clique.parentid != -1) {
+ clique.notes = (clique.id, clique.parentid, nil) :: clique.notes;
+ clique.parentid = -1;
+ }
+ # orphan children
+ # XXX could be more efficient for childless cliques by keeping child count
+ for(i := 0; i < len cliques; i++)
+ if (cliques[i] != nil && cliques[i].parentid == clique.id)
+ cliques[i].parentid = -1;
+}
+
+stopclique(clique: ref Clique)
+{
+ clique.hangup();
+ if (clique.request != nil)
+ clique.request <-= nil;
+}
+
+Clique.breakmsg(clique: self ref Clique, whoto: Set)
+{
+ applycliqueupdate(clique, ref Update.Break, whoto);
+}
+
+Clique.action(clique: self ref Clique, cmd: string,
+ objs: list of int, rest: string, whoto: Set)
+{
+ applycliqueupdate(clique, ref Update.Action(cmd, objs, rest), whoto);
+}
+
+Clique.member(clique: self ref Clique, id: int): ref Member
+{
+ for (ol := qfiles[clique.fileid].ofids; ol != nil; ol = tl ol)
+ if ((hd ol).member != nil && (hd ol).member.id == id)
+ return (hd ol).member;
+ for (s := clique.suspended; s != nil; s = tl s)
+ if ((hd s).id == id)
+ return hd s;
+ return nil;
+}
+
+Clique.membernamed(clique: self ref Clique, name: string): ref Member
+{
+ for (ol := qfiles[clique.fileid].ofids; ol != nil; ol = tl ol)
+ if ((hd ol).uname == name)
+ return (hd ol).member;
+ for (s := clique.suspended; s != nil; s = tl s)
+ if ((hd s).name == name)
+ return hd s;
+ return nil;
+}
+
+Clique.owner(clique: self ref Clique): string
+{
+ return qfiles[clique.fileid].owner;
+}
+
+Clique.fcreate(clique: self ref Clique, f: int, parent: int, d: Sys->Dir): string
+{
+ pq: big;
+ if (parent == -1)
+ pq = mkqid(fGAMEDIR, clique.id);
+ else
+ pq = mkqid(fGAMEDATA, clique.id | (parent<<16));
+ d.qid.path = mkqid(fGAMEDATA, clique.id | (f<<16));
+ d.mode &= ~8r222;
+ return nametree->tree.create(pq, d);
+}
+
+Clique.fremove(clique: self ref Clique, f: int): string
+{
+ return nametree->tree.remove(mkqid(fGAMEDATA, clique.id | (f<<16)));
+}
+
+# debugging...
+Clique.show(nil: self ref Clique, nil: ref Member)
+{
+# sys->print("**************** all objects:\n");
+# showobject(clique, clique.objects[0], p, 0, ~0);
+# if (p == nil) {
+# f := qfiles[clique.fileid];
+# for (ol := f.ofids; ol != nil; ol = tl ol) {
+# p = (hd ol).member;
+# if (p == nil) {
+# sys->print("lurker (name '%s')\n",
+# (hd ol).uname);
+# continue;
+# }
+# sys->print("member %d, '%s': ext->obj ", p.id, p.name);
+# for (j := 0; j < len p.ext2obj; j++)
+# if (p.ext2obj[j] != nil)
+# sys->print("%d->%d[%d] ", j, p.ext2obj[j].id, p.ext(p.ext2obj[j].id));
+# sys->print("\n");
+# }
+# }
+}
+
+cliquerequest(clique: ref Clique, rq: ref Rq): string
+{
+ e := cliquerequest1(clique, rq);
+ sendnotifications(clique);
+ return e;
+}
+
+cliquerequest1(clique: ref Clique, rq: ref Rq): string
+{
+ if (clique.request == nil)
+ return "clique has exited";
+ clique.request <-= rq;
+ err := <-clique.reply;
+ if (clique.hungup && clique.request != nil) {
+ clique.request <-= nil;
+ clique.request = nil;
+ }
+ return err;
+}
+
+sendnotifications(clique: ref Clique)
+{
+ notes, pending: list of (int, int, string);
+ (pending, clique.notes) = (clique.notes, nil);
+ n := 0;
+ while (pending != nil) {
+ for (notes = nil; pending != nil; pending = tl pending)
+ notes = hd pending :: notes;
+ for (; notes != nil; notes = tl notes) {
+ (srcid, dstid, cmd) := hd notes;
+ dst := cliques[dstid];
+ if (!dst.hungup) {
+ dst.notes = pending;
+ cliquerequest1(dst, ref Rq.Notify(srcid, cmd));
+ (pending, dst.notes) = (dst.notes, nil);
+ }
+ }
+ if (n++ > 50)
+ panic("probable loop in clique notification"); # XXX probably shouldn't panic, but useful for debugging
+ }
+}
+
+cliqueproc(clique: ref Clique)
+{
+ wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+ spawn cliqueproc1(clique);
+ buf := array[Sys->ATOMICIO] of byte;
+ n := sys->read(wfd, buf, len buf);
+ sys->print("spree: clique '%s' exited: %s\n", clique.fname, string buf[0:n]);
+ clique.hangup();
+ clique.request = nil;
+ clique.reply <-= "clique exited";
+}
+
+cliqueproc1(clique: ref Clique)
+{
+ for (;;) {
+ rq := <-clique.request;
+ if (rq == nil)
+ break;
+ reply := "";
+ pick r := rq {
+ Init =>
+ reply = clique.mod->init(myself, clique, clique.archive.argv);
+ Join =>
+ reply = clique.mod->join(r.member, r.cmd, r.suspended);
+ Command =>
+ reply = clique.mod->command(r.member, r.cmd);
+ Leave =>
+ if (clique.mod->leave(r.member) == 0)
+ reply = "suspended";
+ Notify =>
+ clique.mod->notify(r.srcid, r.cmd);
+ * =>
+ panic("unknown engine request, tag " + string tagof(rq));
+ }
+ clique.reply <-= reply;
+ }
+ sys->print("spree: clique '%s' exiting\n", clique.fname);
+}
+
+Member.ext(member: self ref Member, id: int): int
+{
+ obj2ext := member.obj2ext;
+ if (id >= len obj2ext || id < 0)
+ return -1;
+ return obj2ext[id];
+}
+
+Member.obj(member: self ref Member, ext: int): ref Object
+{
+ if (ext < 0 || ext >= len member.ext2obj)
+ return nil;
+ return member.ext2obj[ext];
+}
+
+# allocate an object in a member's map.
+memberaddobject(p: ref Member, o: ref Object)
+{
+ if (p.freelist == nil)
+ (p.ext2obj, p.freelist) = makespace(p.ext2obj, p.freelist);
+ ext := hd p.freelist;
+ p.freelist = tl p.freelist;
+
+ if (o.id >= len p.obj2ext) {
+ oldmap := p.obj2ext;
+ newmap := array[o.id + 10] of int;
+ newmap[0:] = oldmap;
+ for (i := len oldmap; i < len newmap; i++)
+ newmap[i] = -1;
+ p.obj2ext = newmap;
+ }
+ p.obj2ext[o.id] = ext;
+ p.ext2obj[ext] = o;
+ if (Debug)
+ sys->print("addobject member %d, internal %d, external %d\n", p.id, o.id, ext);
+}
+
+# delete an object from a member's map.
+memberdelobject(member: ref Member, id: int)
+{
+ if (id >= len member.obj2ext) {
+ sys->fprint(stderr, "spree: bad delobject (member %d, id %d, len obj2ext %d)\n",
+ member.id, id, len member.obj2ext);
+ return;
+ }
+ ext := member.obj2ext[id];
+ member.ext2obj[ext] = nil;
+ member.obj2ext[id] = -1;
+ member.freelist = ext :: member.freelist;
+ if (Debug)
+ sys->print("delobject member %d, internal %d, external %d\n", member.id, id, ext);
+}
+
+memberleaves(member: ref Member)
+{
+ clique := cliques[member.cliqueid];
+ sys->print("member %d leaving clique %d\n", member.id, member.cliqueid);
+
+ suspend := 0;
+ if (!clique.hungup)
+ suspend = cliquerequest(clique, ref Rq.Leave(member)) != nil;
+ member.del(suspend);
+}
+
+resetvisibilities(o: ref Object, id: int)
+{
+ o.visibility = setreset(o.visibility, id);
+ a := o.attrs.a;
+ for (i := 0; i < len a; i++) {
+ for (al := a[i]; al != nil; al = tl al) {
+ (hd al).visibility = setreset((hd al).visibility, id);
+ (hd al).needupdate = setreset((hd al).needupdate, id);
+ }
+ }
+ for (i = 0; i < len o.children; i++)
+ resetvisibilities(o.children[i], id);
+}
+
+# remove a member from their clique.
+# the client is still there, but won't get any clique updates.
+Member.del(member: self ref Member, suspend: int)
+{
+ clique := cliques[member.cliqueid];
+ if (!member.suspended) {
+ for (ofids := qfiles[clique.fileid].ofids; ofids != nil; ofids = tl ofids)
+ if ((hd ofids).member == member) {
+ (hd ofids).member = nil;
+ (hd ofids).hungup = 1;
+ # XXX purge update queue?
+ }
+ # go through all clique objects and attributes, resetting
+ # permissions for member id to their default values.
+ if (suspend) {
+ member.obj2ext = nil;
+ member.ext2obj = nil;
+ member.freelist = nil;
+ member.updating = 0;
+ member.suspended = 1;
+ clique.suspended = member :: clique.suspended;
+ }
+ } else if (!suspend) {
+ ns: list of ref Member;
+ for (s := clique.suspended; s != nil; s = tl s)
+ if (hd s != member)
+ ns = hd s :: ns;
+ clique.suspended = ns;
+ }
+ if (!suspend) {
+ resetvisibilities(clique.objects[0], member.id);
+ clique.memberids = clique.memberids.del(member.id);
+ }
+}
+
+Clique.members(clique: self ref Clique): list of ref Member
+{
+ pl := clique.suspended;
+ for (ofids := qfiles[clique.fileid].ofids; ofids != nil; ofids = tl ofids)
+ if ((hd ofids).member != nil)
+ pl = (hd ofids).member :: pl;
+ return pl;
+}
+
+Object.delete(o: self ref Object)
+{
+ clique := cliques[o.cliqueid];
+ if (o.parentid != -1) {
+ parent := clique.objects[o.parentid];
+ siblings := parent.children;
+ for (i := 0; i < len siblings; i++)
+ if (siblings[i] == o)
+ break;
+ if (i == len siblings)
+ panic("object " + string o.id + " not found in parent");
+ parent.deletechildren((i, i+1));
+ } else
+ sys->fprint(stderr, "spree: cannot delete root object\n");
+}
+
+Object.deletechildren(parent: self ref Object, r: Range)
+{
+ if (len parent.children == 0)
+ return;
+ clique := cliques[parent.cliqueid];
+ n := r.end - r.start;
+ objs := array[r.end - r.start] of int;
+ children := parent.children;
+ for (i := r.start; i < r.end; i++) {
+ o := children[i];
+ objs[i - r.start] = o.id;
+ o.deletechildren((0, len o.children));
+ clique.objects[o.id] = nil;
+ clique.freelist = o.id :: clique.freelist;
+ o.id = -1;
+ o.parentid = -1;
+ }
+ children[r.start:] = children[r.end:];
+ for (i = len children - n; i < len children; i++)
+ children[i] = nil;
+ if (n < len children)
+ parent.children = children[0:len children - n];
+ else
+ parent.children = nil;
+
+ if (Debug) {
+ sys->print("+del from %d, range [%d %d], objs: ", parent.id, r.start, r.end);
+ for (i = 0; i < len objs; i++)
+ sys->print("%d ", objs[i]);
+ sys->print("\n");
+ }
+ applycliqueupdate(clique, ref Update.Delete(parent.id, r, objs), All);
+}
+
+# move a range of objects from src and insert them at index in dst.
+Object.transfer(src: self ref Object, r: Range, dst: ref Object, index: int)
+{
+ if (index == -1)
+ index = len dst.children;
+ if (src == dst && index >= r.start && index <= r.end)
+ return;
+ n := r.end - r.start;
+ objs := src.children[r.start:r.end];
+ newchildren := array[len src.children - n] of ref Object;
+ newchildren[0:] = src.children[0:r.start];
+ newchildren[r.start:] = src.children[r.end:];
+ src.children = newchildren;
+
+ if (Debug) {
+ sys->print("+transfer from %d[%d,%d] to %d[%d], objs: ",
+ src.id, r.start, r.end, dst.id, index);
+ for (x := 0; x < len objs; x++)
+ sys->print("%d ", objs[x].id);
+ sys->print("\n");
+ }
+
+ nindex := index;
+
+ # if we've just removed some cards from the destination,
+ # then adjust the destination index accordingly.
+ if (src == dst && nindex > r.start) {
+ if (nindex < r.end)
+ nindex = r.start;
+ else
+ nindex -= n;
+ }
+ newchildren = array[len dst.children + n] of ref Object;
+ newchildren[0:] = dst.children[0:index];
+ newchildren[nindex + n:] = dst.children[nindex:];
+ newchildren[nindex:] = objs;
+ dst.children = newchildren;
+
+ for (i := 0; i < len objs; i++)
+ objs[i].parentid = dst.id;
+
+ clique := cliques[src.cliqueid];
+ applycliqueupdate(clique,
+ ref Update.Transfer(src.id, r, dst.id, index),
+ All);
+}
+
+# visibility is only set when the attribute is newly created.
+Object.setattr(o: self ref Object, name, val: string, visibility: Set)
+{
+ (changed, attr) := o.attrs.set(name, val, visibility);
+ if (changed) {
+ attr.needupdate = All;
+ applycliqueupdate(cliques[o.cliqueid], ref Update.Set(o, o.id, attr), objvisibility(o));
+ }
+}
+
+Object.getattr(o: self ref Object, name: string): string
+{
+ attr := o.attrs.get(name);
+ if (attr == nil)
+ return nil;
+ return attr.val;
+}
+
+# set visibility of an object - reveal any uncovered descendents
+# if necessary.
+Object.setvisibility(o: self ref Object, visibility: Set)
+{
+ if (o.visibility.eq(visibility))
+ return;
+ o.visibility = visibility;
+ applycliqueupdate(cliques[o.cliqueid], ref Update.Setvisibility(o.id, visibility), objvisibility(o));
+}
+
+Object.setattrvisibility(o: self ref Object, name: string, visibility: Set)
+{
+ attr := o.attrs.get(name);
+ if (attr == nil) {
+ sys->fprint(stderr, "spree: setattrvisibility, no attribute '%s', id %d\n", name, o.id);
+ return;
+ }
+ if (attr.visibility.eq(visibility))
+ return;
+ # send updates to anyone that has needs updating,
+ # is in the new visibility list, but not in the old one.
+ ovisibility := objvisibility(o);
+ before := ovisibility.X(A&B, attr.visibility);
+ after := ovisibility.X(A&B, visibility);
+ attr.visibility = visibility;
+ applycliqueupdate(cliques[o.cliqueid], ref Update.Set(o, o.id, attr), before.X(~A&B, after));
+}
+
+# an object's visibility is the intersection
+# of the visibility of all its parents.
+objvisibility(o: ref Object): Set
+{
+ clique := cliques[o.cliqueid];
+ visibility := All;
+ for (id := o.parentid; id != -1; id = o.parentid) {
+ o = clique.objects[id];
+ visibility = visibility.X(A&B, o.visibility);
+ }
+ return visibility;
+}
+
+makespace(objects: array of ref Object,
+ freelist: list of int): (array of ref Object, list of int)
+{
+ if (freelist == nil) {
+ na := array[len objects + 10] of ref Object;
+ na[0:] = objects;
+ for (j := len na - 1; j >= len objects; j--)
+ freelist = j :: freelist;
+ objects = na;
+ }
+ return (objects, freelist);
+}
+
+updateall()
+{
+ for (i := 0; i < len qfiles; i++) {
+ f := qfiles[i];
+ if (f != nil && f.needsupdate) {
+ for (ol := f.ofids; ol != nil; ol = tl ol)
+ sendupdate(hd ol);
+ f.needsupdate = 0;
+ }
+ }
+}
+
+applyupdate(f: ref Qfile, upd: ref Update)
+{
+ for (ol := f.ofids; ol != nil; ol = tl ol)
+ (hd ol).updateq.put(upd);
+ f.needsupdate = 1;
+}
+
+# send update to members in the clique in the needupdate set.
+applycliqueupdate(clique: ref Clique, upd: ref Update, needupdate: Set)
+{
+ always := alwayssend[tagof(upd)];
+ if (needupdate.isempty() || (!clique.started && !always))
+ return;
+ f := qfiles[clique.fileid];
+ for (ol := f.ofids; ol != nil; ol = tl ol) {
+ ofid := hd ol;
+ member := ofid.member;
+ if (member != nil && needupdate.holds(member.id) && (member.updating || always))
+ queueupdate(ofid.updateq, member, upd);
+ }
+ f.needsupdate = 1;
+}
+
+# transform an outgoing update according to the visibility
+# of the object(s) concerned.
+# the update concerned has already occurred.
+queueupdate(q: ref Queue, p: ref Member, upd: ref Update)
+{
+ clique := cliques[p.cliqueid];
+ pick u := upd {
+ Set =>
+ if (p.ext(u.o.id) != -1 && u.attr.needupdate.holds(p.id)) {
+ q.put(ref Update.Set(u.o, p.ext(u.o.id), u.attr));
+ u.attr.needupdate = u.attr.needupdate.del(p.id);
+ } else
+ u.attr.needupdate = u.attr.needupdate.add(p.id);
+
+ Transfer =>
+ # if moving from an invisible object, create the objects
+ # temporarily in the source object, and then transfer from that.
+ # if moving to an invisible object, delete the objects.
+ # if moving from invisible to invisible, do nothing.
+ src := clique.objects[u.srcid];
+ dst := clique.objects[u.dstid];
+ fromvisible := objvisibility(src).X(A&B, src.visibility).holds(p.id);
+ tovisible := objvisibility(dst).X(A&B, dst.visibility).holds(p.id);
+ if (fromvisible || tovisible) {
+ # N.B. objects are already in destination object at this point.
+ (r, index, srcid) := (u.from, u.index, u.srcid);
+
+ # XXX this scheme is all very well when the parent of src
+ # or dst is visible, but not when it's not... in that case
+ # we should revert to the old scheme of deleting objects in src
+ # or recreating them in dst as appropriate.
+ if (!tovisible) {
+ # transfer objects to destination, then delete them,
+ # so client knows where they've gone.
+ q.put(ref Update.Transfer(p.ext(srcid), r, p.ext(u.dstid), 0));
+ qdelobjects(q, p, dst, (u.index, u.index + r.end - r.start), 0);
+ break;
+ }
+ if (!fromvisible) {
+ # create at the end of source object,
+ # then transfer into correct place in destination.
+ n := r.end - r.start;
+ for (i := 0; i < n; i++) {
+ o := dst.children[index + i];
+ qrecreateobject(q, p, o, src);
+ }
+ r = (0, n);
+ }
+ if (p.ext(srcid) == -1 || p.ext(u.dstid) == -1)
+ panic("external objects do not exist");
+ q.put(ref Update.Transfer(p.ext(srcid), r, p.ext(u.dstid), index));
+ }
+ Create =>
+ dst := clique.objects[u.parentid];
+ if (objvisibility(dst).X(A&B, dst.visibility).holds(p.id)) {
+ memberaddobject(p, clique.objects[u.objid]);
+ q.put(ref Update.Create(p.ext(u.objid), p.ext(u.parentid), u.visibility, u.objtype));
+ }
+ Delete =>
+ # we can only get this update when all the children are
+ # leaf nodes.
+ o := clique.objects[u.parentid];
+ if (objvisibility(o).X(A&B, o.visibility).holds(p.id)) {
+ r := u.r;
+ extobjs := array[len u.objs] of int;
+ for (i := 0; i < len u.objs; i++) {
+ extobjs[i] = p.ext(u.objs[i]);
+ memberdelobject(p, u.objs[i]);
+ }
+ q.put(ref Update.Delete(p.ext(o.id), u.r, extobjs));
+ }
+ Setvisibility =>
+ # if the object doesn't exist for this member, don't do anything.
+ # else if there are children, check whether they exist, and
+ # create or delete them as necessary.
+ if (p.ext(u.objid) != -1) {
+ o := clique.objects[u.objid];
+ if (len o.children > 0) {
+ visible := u.visibility.holds(p.id);
+ made := p.ext(o.children[0].id) != -1;
+ if (!visible && made)
+ qdelobjects(q, p, o, (0, len o.children), 0);
+ else if (visible && !made)
+ for (i := 0; i < len o.children; i++)
+ qrecreateobject(q, p, o.children[i], nil);
+ }
+ q.put(ref Update.Setvisibility(p.ext(u.objid), u.visibility));
+ }
+ Action =>
+ s := u.s;
+ for (ol := u.objs; ol != nil; ol = tl ol)
+ s += " " + string p.ext(hd ol);
+ s += " " + u.rest;
+ q.put(ref Update.Action(s, nil, nil));
+ * =>
+ q.put(upd);
+ }
+}
+
+# queue deletions for o; we pretend to the client that
+# the deletions are at index.
+qdelobjects(q: ref Queue, p: ref Member, o: ref Object, r: Range, index: int)
+{
+ if (r.start >= r.end)
+ return;
+ children := o.children;
+ extobjs := array[r.end - r.start] of int;
+ for (i := r.start; i < r.end; i++) {
+ c := children[i];
+ qdelobjects(q, p, c, (0, len c.children), 0);
+ extobjs[i - r.start] = p.ext(c.id);
+ memberdelobject(p, c.id);
+ }
+ q.put(ref Update.Delete(p.ext(o.id), (index, index + (r.end - r.start)), extobjs));
+}
+
+# parent visibility now allows o to be seen, so recreate
+# it for the member. (if parent is non-nil, pretend we're creating it there)
+qrecreateobject(q: ref Queue, p: ref Member, o: ref Object, parent: ref Object)
+{
+ memberaddobject(p, o);
+ parentid := o.parentid;
+ if (parent != nil)
+ parentid = parent.id;
+ q.put(ref Update.Create(p.ext(o.id), p.ext(parentid), o.visibility, o.objtype));
+ recreateattrs(q, p, o);
+ if (o.visibility.holds(p.id)) {
+ a := o.children;
+ for (i := 0; i < len a; i++)
+ qrecreateobject(q, p, a[i], nil);
+ }
+}
+
+recreateattrs(q: ref Queue, p: ref Member, o: ref Object)
+{
+ a := o.attrs.a;
+ for (i := 0; i < len a; i++) {
+ for (al := a[i]; al != nil; al = tl al) {
+ attr := hd al;
+ q.put(ref Update.Set(o, p.ext(o.id), attr));
+ }
+ }
+}
+
+CONTINUATION := array[] of {byte '\n', byte '*'};
+
+# send the client as many updates as we can fit in their read request
+# (if there are some updates to send and there's an outstanding read request)
+sendupdate(ofid: ref Openfid)
+{
+ clique: ref Clique;
+ if (ofid.readreq == nil || (ofid.updateq.isempty() && !ofid.hungup))
+ return;
+ m := ofid.readreq;
+ q := ofid.updateq;
+ if (ofid.hungup) {
+ srv.reply(ref Rmsg.Read(m.tag, nil));
+ q.h = q.t = nil;
+ return;
+ }
+ data := array[m.count] of byte;
+ nb := 0;
+ plid := -1;
+ if (ofid.member != nil) {
+ plid = ofid.member.id;
+ clique = cliques[ofid.member.cliqueid];
+ }
+ avail := len data - len CONTINUATION;
+Putdata:
+ for (; !q.isempty(); q.get()) {
+ upd := q.peek();
+ pick u := upd {
+ Set =>
+ if (plid != -1 && !objvisibility(u.o).X(A&B, u.attr.visibility).holds(plid)) {
+ u.attr.needupdate = u.attr.needupdate.add(plid);
+ continue Putdata;
+ }
+ Break =>
+ if (nb > 0) {
+ q.get();
+ break Putdata;
+ }
+ continue Putdata;
+ }
+ d := array of byte update2s(upd, plid);
+ if (len d + nb > avail)
+ break;
+ data[nb:] = d;
+ nb += len d;
+ }
+ err := "";
+ if (nb == 0) {
+ if (q.isempty())
+ return;
+ err = "short read";
+ } else if (!q.isempty()) {
+ data[nb:] = CONTINUATION;
+ nb += len CONTINUATION;
+ }
+ data = data[0:nb];
+
+ if (err != nil)
+ srv.reply(ref Rmsg.Error(m.tag, err));
+ else
+ srv.reply(ref Rmsg.Read(m.tag, data));
+ ofid.readreq = nil;
+}
+
+# convert an Update adt to a string.
+update2s(upd: ref Update, plid: int): string
+{
+ s: string;
+ pick u := upd {
+ Create =>
+ objtype := u.objtype;
+ if (objtype == nil)
+ objtype = "nil";
+ s = sys->sprint("create %d %d %d %s\n", u.objid, u.parentid, u.visibility.holds(plid) != 0, objtype);
+ Transfer =>
+ # tx src dst dstindex start end
+ if (u.srcid == -1 || u.dstid == -1)
+ panic("src or dst object is -1");
+ s = sys->sprint("tx %d %d %d %d %d\n",
+ u.srcid, u.dstid, u.from.start, u.from.end, u.index);
+ Delete =>
+ s = sys->sprint("del %d %d %d", u.parentid, u.r.start, u.r.end);
+ for (i := 0; i < len u.objs; i++)
+ s += " " + string u.objs[i];
+ s[len s] = '\n';
+ Set =>
+ s = sys->sprint("set %d %s %s\n", u.objid, u.attr.name, u.attr.val);
+ Setvisibility =>
+ s = sys->sprint("vis %d %d\n", u.objid, u.visibility.holds(plid) != 0);
+ Action =>
+ s = u.s + "\n";
+ * =>
+ sys->fprint(stderr, "unknown update tag %d\n", tagof(upd));
+ }
+ return s;
+}
+
+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;
+}
+
+Queue.peek(q: self ref Queue): T
+{
+ s: T;
+ if (q.isempty())
+ return s;
+ s = q.get();
+ q.h = s :: q.h;
+ return s;
+}
+
+Queue.isempty(q: self ref Queue): int
+{
+ return q.h == nil && q.t == nil;
+}
+
+revlist(ls: list of T) : list of T
+{
+ rs: list of T;
+ for (; ls != nil; ls = tl ls)
+ rs = hd ls :: rs;
+ return rs;
+}
+
+Attributes.new(): ref Attributes
+{
+ return ref Attributes(array[7] of list of ref Attribute);
+}
+
+Attributes.get(attrs: self ref Attributes, name: string): ref Attribute
+{
+ for (al := attrs.a[strhash(name, len attrs.a)]; al != nil; al = tl al)
+ if ((hd al).name == name)
+ return hd al;
+ return nil;
+}
+
+# return (haschanged, attr)
+Attributes.set(attrs: self ref Attributes, name, val: string, visibility: Set): (int, ref Attribute)
+{
+ h := strhash(name, len attrs.a);
+ for (al := attrs.a[h]; al != nil; al = tl al) {
+ attr := hd al;
+ if (attr.name == name) {
+ if (attr.val == val)
+ return (0, attr);
+ attr.val = val;
+ return (1, attr);
+ }
+ }
+ attr := ref Attribute(name, val, visibility, All);
+ attrs.a[h] = attr :: attrs.a[h];
+ return (1, attr);
+}
+
+setreset(set: Set, i: int): Set
+{
+ if (set.msb())
+ return set.add(i);
+ return set.del(i);
+}
+
+# from Aho Hopcroft Ullman
+strhash(s: string, n: int): int
+{
+ h := 0;
+ m := len s;
+ for(i := 0; i<m; i++){
+ h = 65599 * h + s[i];
+ }
+ return (h & 16r7fffffff) % n;
+}
+
+panic(s: string)
+{
+ cliques[0].show(nil);
+ sys->fprint(stderr, "panic: %s\n", s);
+ raise "panic";
+}
+
+randbits: chan of int;
+
+initrand()
+{
+ randbits = chan of int;
+ spawn randproc();
+}
+
+randproc()
+{
+ fd := sys->open("/dev/notquiterandom", Sys->OREAD);
+ if (fd == nil) {
+ sys->print("cannot open /dev/random: %r\n");
+ exit;
+ }
+ randbits <-= sys->pctl(0, nil);
+ buf := array[1] of byte;
+ while ((n := sys->read(fd, buf, len buf)) > 0) {
+ b := buf[0];
+ for (i := byte 1; i != byte 0; i <<= 1)
+ randbits <-= (b & i) != byte 0;
+ }
+}
+
+rand(n: int): int
+{
+ x: int;
+ for (nbits := 0; (1 << nbits) < n; nbits++)
+ x ^= <-randbits << nbits;
+ x ^= <-randbits << nbits;
+ x &= (1 << nbits) - 1;
+ i := 0;
+ while (x >= n) {
+ x ^= <-randbits << i;
+ i = (i + 1) % nbits;
+ }
+ return x;
+}
+
+archivenum := -1;
+
+newarchivename(): string
+{
+ if (archivenum == -1) {
+ (d, nil) := readdir->init(ARCHIVEDIR, Readdir->MTIME|Readdir->COMPACT);
+ for (i := 0; i < len d; i++) {
+ name := d[i].name;
+ if (name != nil && name[0] == 'a') {
+ for (j := 1; j < len name; j++)
+ if (name[j] < '0' || name[j] > '9')
+ break;
+ if (j == len name && int name[1:] > archivenum)
+ archivenum = int name[1:];
+ }
+ }
+ archivenum++;
+ }
+ return ARCHIVEDIR + "/a" + string archivenum++;
+}
+
+archivenames(): list of string
+{
+ names: list of string;
+ (d, nil) := readdir->init(ARCHIVEDIR, Readdir->MTIME|Readdir->COMPACT);
+ for (i := 0; i < len d; i++)
+ if (len d[i].name < 4 || d[i].name[len d[i].name - 4:] != ".old")
+ names = ARCHIVEDIR + "/" + d[i].name :: names;
+ return names;
+}
diff --git a/appl/spree/spree.m b/appl/spree/spree.m
new file mode 100644
index 00000000..c4178a6d
--- /dev/null
+++ b/appl/spree/spree.m
@@ -0,0 +1,140 @@
+Spree: module
+{
+ MAXPLAYERS: con 100;
+ Attribute: adt {
+ name: string;
+ val: string;
+ visibility: Sets->Set; # set of members that can see attr
+ needupdate: Sets->Set; # set of members that have not got an update queued
+ };
+
+ Attributes: adt {
+ a: array of list of ref Attribute;
+ set: fn(attr: self ref Attributes, name, val: string, vis: Sets->Set): (int, ref Attribute);
+ get: fn(attr: self ref Attributes, name: string): ref Attribute;
+ new: fn(): ref Attributes;
+ };
+
+ Range: adt {
+ start: int;
+ end: int;
+ };
+
+ Object: adt {
+ id: int;
+ attrs: ref Attributes;
+ visibility: Sets->Set;
+ parentid: int;
+ children: cyclic array of ref Object; # not actually cyclic
+ cliqueid: int;
+ objtype: string;
+
+ transfer: fn(o: self ref Object, r: Range, dst: ref Object, i: int);
+ setvisibility: fn(o: self ref Object, visibility: Sets->Set);
+ setattrvisibility: fn(o: self ref Object, name: string, visibility: Sets->Set);
+ setattr: fn(o: self ref Object, name: string, val: string, vis: Sets->Set);
+ getattr: fn(o: self ref Object, name: string): string;
+ delete: fn(o: self ref Object);
+ deletechildren: fn(o: self ref Object, r: Range);
+ };
+
+ Rq: adt {
+ pick {
+ Init =>
+ opts: string;
+ Command =>
+ member: ref Member;
+ cmd: string;
+ Join =>
+ member: ref Member;
+ cmd: string;
+ suspended: int;
+ Leave =>
+ member: ref Member;
+ Notify =>
+ srcid: int;
+ cmd: string;
+ }
+ };
+
+ # this might also be known as a "group", as there's nothing
+ # inherently clique-like about it; it's just a group of members
+ # mutually creating and manipulating objects.
+ Clique: adt {
+ id: int;
+ fileid: int;
+ fname: string;
+ objects: array of ref Object;
+ archive: ref Archives->Archive;
+ freelist: list of int;
+ mod: Engine;
+ memberids: Sets->Set; # set of allocated member ids
+ suspended: list of ref Member;
+ request: chan of ref Rq;
+ reply: chan of string;
+ hungup: int;
+ started: int;
+ parentid: int;
+ notes: list of (int, int, string); # (src, dest, note)
+
+ new: fn(parent: self ref Clique, archive: ref Archives->Archive, owner: string): (int, string, string); # returns (cliqueid, filename, error)
+ newobject: fn(clique: self ref Clique, parent: ref Object, visibility: Sets->Set, objtype: string): ref Object;
+ start: fn(clique: self ref Clique);
+ action: fn(clique: self ref Clique, cmd: string,
+ objs: list of int, rest: string, whoto: Sets->Set);
+ breakmsg: fn(clique: self ref Clique, whoto: Sets->Set);
+ show: fn(clique: self ref Clique, member: ref Member);
+ member: fn(clique: self ref Clique, id: int): ref Member;
+ membernamed: fn(clique: self ref Clique, name: string): ref Member;
+ members: fn(clique: self ref Clique): list of ref Member;
+ owner: fn(clique: self ref Clique): string;
+ hangup: fn(clique: self ref Clique);
+ fcreate: fn(clique: self ref Clique, i: int, pq: int, d: Sys->Dir): string;
+ fremove: fn(clique: self ref Clique, i: int): string;
+ notify: fn(clique: self ref Clique, cliqueid: int, msg: string);
+ };
+
+ # a Member is involved in one clique only
+ Member: adt {
+ id: int;
+ cliqueid: int;
+ obj2ext: array of int;
+ ext2obj: array of ref Object;
+ freelist: list of int;
+ name: string;
+ updating: int;
+ suspended: int;
+
+ ext: fn(member: self ref Member, id: int): int;
+ obj: fn(member: self ref Member, id: int): ref Object;
+ del: fn(member: self ref Member, suspend: int);
+ };
+
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+ archivenames: fn(): list of string;
+ newarchivename: fn(): string;
+ rand: fn(n: int): int;
+};
+
+Engine: module {
+ init: fn(srvmod: Spree, clique: ref Spree->Clique, argv: list of string): string;
+ command: fn(member: ref Spree->Member, e: string): string;
+ join: fn(member: ref Spree->Member , e: string, suspended: int): string;
+ leave: fn(member: ref Spree->Member): int;
+ notify: fn(fromid: int, s: string);
+ readfile: fn(f: int, offset: big, count: int): array of byte;
+};
+
+Archives: module {
+ PATH: con "/dis/spree/archives.dis";
+ Archive: adt {
+ argv: list of string; # how to restart the session.
+ members: array of string; # members involved.
+ info: list of (string, string); # any other information.
+ objects: array of ref Spree->Object;
+ };
+ init: fn(mod: Spree);
+ write: fn(clique: ref Spree->Clique, info: list of (string, string), file: string, members: Sets->Set): string;
+ read: fn(file: string): (ref Archive, string);
+ readheader: fn(file: string): (ref Archive, string);
+};