code: 9ferno

ref: 916fe767404498e0108f7b1d3fd0994ae19913f3
dir: /appl/spree/lib/cardlib.b/

View raw version
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;
}