xref: /inferno-os/appl/wm/tetris.b (revision 66f5808b81b1df84bc57c4f7b9d487201bc162fb)
1# Copyright  © 1999 Roger Peppe.  All rights reserved.
2implement Tetris;
3
4include "sys.m";
5	sys: Sys;
6	stderr: ref Sys->FD;
7include "draw.m";
8	draw: Draw;
9	Point, Rect: import draw;
10include "tk.m";
11	tk: Tk;
12include "tkclient.m";
13	tkclient: Tkclient;
14include "rand.m";
15	rand: Rand;
16include "scoretable.m";
17	scoretab: Scoretable;
18include "arg.m";
19include "keyboard.m";
20	Up, Down, Right, Left: import Keyboard;
21
22include "keyring.m";
23include "security.m";	# for random seed
24
25Tetris: module {
26	init: fn(ctxt: ref Draw->Context, argv: list of string);
27};
28
29SCORETABLE: con "/lib/scores/tetris";
30LOCKPORT: con 18343;
31
32# number of pieces across and down board.
33BOARDWIDTH: con 10;
34BOARDHEIGHT: con 22;
35
36awaitingscore := 1;
37
38Row: adt {
39	tag:		string;
40	delete:	int;
41};
42
43Board: adt {
44	new:			fn(top: ref Tk->Toplevel, w: string,
45					blocksize: int, maxsize: Point): ref Board;
46	makeblock:	fn(bd: self ref Board, colour: string, p: Point): string;
47	moveblock:	fn(bd: self ref Board, b: string, p: Point);
48	movecurr:	fn(bd: self ref Board, delta: Point);
49	delrows:		fn(bd: self ref Board, rows: list of int);
50	landedblock:	fn(bd: self ref Board, b: string, p: Point);
51	setnextshape:	fn(bd: self ref Board, colour: string, spec: array of Point);
52	setscore:		fn(bd: self ref Board, score: int);
53	setlevel:		fn(bd: self ref Board, level: int);
54	setnrows:		fn(bd: self ref Board, level: int);
55	gameover:	fn(bd: self ref Board);
56	update:		fn(bd: self ref Board);
57
58	state:		array of array of byte;
59	w:			string;
60	dx:			int;
61	win:			ref Tk->Toplevel;
62	rows:		array of Row;
63	maxid:		int;
64};
65
66Piece: adt {
67	shape:	int;
68	rot:		int;
69};
70
71Shape: adt {
72	coords:	array of array of Point;
73	colour:	string;
74	score:	array of int;
75};
76
77Game: adt {
78	new:		fn(bd: ref Board): ref Game;
79	move:	fn(g: self ref Game, dx: int);
80	rotate:	fn(g: self ref Game, clockwise: int);
81	tick:		fn(g: self ref Game): int;
82	drop:	fn(g: self ref Game);
83
84	bd:		ref Board;
85	level:	int;
86	delay:	int;
87	score:	int;
88	nrows:	int;
89	pieceids:	array of string;
90	pos:		Point;
91	next,
92	curr:		Piece;
93};
94
95badmod(path: string)
96{
97	sys->fprint(stderr, "tetris: cannot load %s: %r\n", path);
98	raise "fail: bad module";
99}
100
101usage()
102{
103	sys->fprint(stderr, "usage: tetris [-b blocksize]\n");
104	raise "fail:usage";
105}
106
107init(ctxt: ref Draw->Context, argv: list of string)
108{
109	sys = load Sys Sys->PATH;
110	stderr = sys->fildes(2);
111	draw = load Draw Draw->PATH;
112	tk = load Tk Tk->PATH;
113	if (tk == nil)
114		badmod(Tk->PATH);
115	tkclient = load Tkclient Tkclient->PATH;
116	if (tkclient == nil)
117		badmod(Tkclient->PATH);
118	tkclient->init();
119	rand = load Rand Rand->PATH;
120	if (rand == nil)
121		badmod(Rand->PATH);
122	arg := load Arg Arg->PATH;
123	if (arg == nil)
124		badmod(Arg->PATH);
125	if (ctxt == nil)
126		ctxt = tkclient->makedrawcontext();
127	blocksize := 17;			# preferred block size
128	arg->init(argv);
129	while ((opt := arg->opt()) != 0) {
130		case opt {
131		'b' =>
132			if ((b := arg->arg()) == nil || int b <= 0)
133				usage();
134			blocksize = int b;
135		* =>
136			usage();
137		}
138	}
139	if (arg->argv() != nil)
140		usage();
141
142	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
143	scoretab = load Scoretable Scoretable->PATH;
144	scorech := chan of int;
145	spawn scoresrvwait(scorech);
146	(win, winctl) := tkclient->toplevel(ctxt, "", "Tetris",Tkclient->Hide);
147	seedrand();
148	fromuser := chan of string;
149	tk->namechan(win, fromuser, "user");
150	cmd(win, "bind . <Key> {send user k %s}");
151	cmd(win, "bind . <ButtonRelease-1> {focus .}");
152	cmd(win, "bind .Wm_t <ButtonRelease-1> +{focus .}");
153	cmd(win, "focus .");
154
155	maxsize := Point(10000, 10000);
156	if (ctxt.display.image != nil) {
157		img := ctxt.display.image;
158		wsz := wsize(win, ".");
159		maxsize.y = img.r.dy() - wsz.y;
160		maxsize.x = img.r.dx();
161	}
162
163	tkclient->onscreen(win, nil);
164	tkclient->startinput(win, "kbd"::"ptr"::nil);
165	for (;;) {
166		bd := Board.new(win, ".f", blocksize, maxsize);
167		if (bd == nil) {
168			sys->fprint(stderr, "tetris: couldn't make board\n");
169			return;
170		}
171		cmd(win, "bind .f.c <ButtonRelease-1> {send user m %x %y}");
172		cmd(win, "pack .f -side top");
173		cmd(win, "update");
174		g := Game.new(bd);
175		(finished, rank) := rungame(g, win, fromuser, winctl, scorech);
176		if (finished)
177			break;
178		cmd(win, "pack propagate . 0");
179		if (scoretab != nil) {
180			cmd(win, "destroy .f");
181			if (showhighscores(win, fromuser, winctl, rank) == 0)
182				break;
183		} else
184			cmd(win, "destroy .f");
185	}
186}
187
188wsize(win: ref Tk->Toplevel, w: string): Point
189{
190	bd := int cmd(win, w + " cget -bd");
191	return (int cmd(win, w + " cget -width") + bd * 2,
192		int cmd(win, w + " cget -height") + bd * 2);
193}
194
195rungame(g: ref Game, win: ref Tk->Toplevel, fromuser: chan of string, winctl: chan of string, scorech: chan of int): (int, int)
196{
197	tickchan := chan of int;
198	spawn ticker(g, tickchan);
199	paused := 0;
200	tch := chan of int;
201
202	gameover := 0;
203	rank := -1;
204	bdsize := wsize(win, ".f.c");
205	boundy := bdsize.y * 2 / 3;
206	id := cmd(win, ".f.c create line " + p2s((0, boundy)) + " " + p2s((bdsize.x, boundy)) +
207			" -fill white");
208	cmd(win, ".f.c lower " + id);
209	for (;;) alt {
210	s := <-win.ctxt.kbd =>
211		tk->keyboard(win, s);
212	s := <-win.ctxt.ptr =>
213		tk->pointer(win, *s);
214	s := <-fromuser =>
215		key: int;
216		if (s[0] == 'm') {
217			(nil, toks) := sys->tokenize(s, " ");
218			p := Point(int hd tl toks, int hd tl tl toks);
219			if (p.y > boundy)
220				key = ' ';
221			else {
222				x := p.x / (bdsize.x / 3);
223				case x {
224				0 =>
225					key = '7';
226				1 =>
227					key = '8';
228				2 =>
229					key = '9';
230				* =>
231					break;
232				}
233			}
234		} else if (s[0] == 'k')
235			key = int s[1:];
236		else
237			sys->print("oops (%s)\n", s);
238		if (gameover)
239			return (key == 'q', rank);
240		if (paused) {
241			paused = 0;
242			(tickchan, tch) = (tch, tickchan);
243			if (key != 'q')
244				continue;
245		}
246		case key {
247		'9'  or 'c' or Right =>
248			g.move(1);
249		'7' or 'z' or Left =>
250			g.move(-1);
251		'8' or 'x' or Up =>
252			g.rotate(0);
253		' ' or Down =>
254			g.drop();
255		'p' =>
256			paused = 1;
257			(tickchan, tch) = (tch, tickchan);
258		'q' =>
259			g.delay = -1;
260			while (<-tickchan)
261				;
262			return (1, rank);
263		}
264	s := <-win.ctxt.ctl or
265	s = <-win.wreq or
266	s = <-winctl =>
267		tkclient->wmctl(win, s);
268	n := <-tickchan =>
269		if (g.tick() == -1) {
270			while (n)
271				n = <-tickchan;
272			if (awaitingscore && !<-scorech) {
273				awaitingscore = 0;
274				scoretab = nil;
275			}
276			if (scoretab != nil)
277				rank = scoretab->setscore(g.score, sys->sprint("%d %d %bd", g.nrows, g.level,
278						big readfile("/dev/time") / big 1000000));
279			gameover = 1;
280		}
281	ok := <-scorech =>
282		awaitingscore = 0;
283		if (!ok)
284			scoretab = nil;
285	}
286}
287
288tablerow(win: ref Tk->Toplevel, w, bg: string, relief: string, vals: array of string, widths: array of string)
289{
290	cmd(win, "frame " + w + " -bd 2 -relief " + relief);
291	for (i := 0; i < len vals; i++) {
292		cw := cmd(win, "label " + w + "." + string i + " -text " + tk->quote(vals[i]) + " -width " + widths[i] + bg);
293		cmd(win, "pack " + cw + " -side left -anchor w");
294	}
295	cmd(win, "pack " + w + " -side top");
296}
297
298showhighscores(win: ref Tk->Toplevel, fromuser: chan of string, winctl: chan of string, rank: int): int
299{
300	widths := array[] of {"10w", "7w", "7w", "5w"};	# user, score, level, rows
301	cmd(win, "frame .f -bd 4 -relief raised");
302	cmd(win, "label .f.title -text {High Scores}");
303	cmd(win, "pack .f.title -side top -anchor n");
304	tablerow(win, ".f.h", nil, "raised", array[] of {"User", "Score", "Level", "Rows"}, widths);
305	sl := scoretab->scores();
306	n := 0;
307	while (sl != nil) {
308		s := hd sl;
309		bg := "";
310		if (n == rank)
311			bg = " -bg white";
312		f := ".f.f" + string n++;
313		nrows := level := "";
314		(nil, toks) := sys->tokenize(s.other, " ");
315		if (toks != nil)
316			(nrows, toks) = (hd toks, tl toks);
317		if (toks != nil)
318			level = hd toks;
319		tablerow(win, f, bg, "sunken", array[] of {s.user, string s.score, level, nrows}, widths);
320		sl = tl sl;
321	}
322	cmd(win, "button .f.b -text {New game} -command {send user s}");
323	cmd(win, "pack .f.b -side top");
324	cmd(win, "pack .f -side top");
325	cmd(win, "update");
326	for (;;) alt {
327	s := <-win.ctxt.kbd =>
328		tk->keyboard(win, s);
329	s := <-win.ctxt.ptr =>
330		tk->pointer(win, *s);
331	s := <-fromuser =>
332		if (s[0] == 'k') {
333			cmd(win, "destroy .f");
334			return int s[1:] != 'q';
335		} else if (s[0] == 's') {
336			cmd(win, "destroy .f");
337			return 1;
338		}
339	s := <-win.ctxt.ctl or
340	s = <-win.wreq or
341	s = <-winctl =>
342		tkclient->wmctl(win, s);
343	}
344}
345
346scoresrvwait(ch: chan of int)
347{
348	if (scoretab == nil) {
349		ch <-= 0;
350		return;
351	}
352	(ok, err) := scoretab->init(LOCKPORT, readfile("/dev/user"), "tetris", SCORETABLE);
353	if (ok != -1)
354		ch <-= 1;
355	else {
356		if (err != "timeout")
357			sys->fprint(stderr, "tetris: scoretable error: %s\n", err);
358		else
359			sys->fprint(stderr, "tetris: timed out trying to connect to score server\n");
360		ch <-= 0;
361	}
362}
363
364readfile(f: string): string
365{
366	fd := sys->open(f, Sys->OREAD);
367	if (fd == nil)
368		return nil;
369	buf := array[Sys->ATOMICIO] of byte;
370	n := sys->read(fd, buf, len buf);
371	if (n <= 0)
372		return nil;
373	return string buf[0:n];
374}
375
376ticker(g: ref Game, c: chan of int)
377{
378	c <-= 1;
379	while (g.delay >= 0) {
380		sys->sleep(g.delay);
381		c <-= 1;
382	}
383	c <-= 0;
384}
385
386seedrand()
387{
388	random := load Random Random->PATH;
389	if (random == nil) {
390		sys->fprint(stderr, "tetris: cannot load %s: %r\n", Random->PATH);
391		return;
392	}
393	seed := random->randomint(Random->ReallyRandom);
394	rand->init(seed);
395}
396
397Game.new(bd: ref Board): ref Game
398{
399	g := ref Game;
400	g.bd = bd;
401	g.level = 0;
402	g.pieceids = array[4] of string;
403	g.score = 0;
404	g.delay = delays[g.level];
405	g.nrows = 0;
406	g.next = randompiece();
407	newpiece(g);
408	bd.update();
409	return g;
410}
411
412randompiece(): Piece
413{
414	p: Piece;
415	p.shape = rand->rand(len shapes);
416	p.rot = rand->rand(len shapes[p.shape].coords);
417	return p;
418}
419
420Game.move(g: self ref Game, dx: int)
421{
422	np := g.pos.add((dx, 0));
423	if (canmove(g, g.curr, np)) {
424		g.bd.movecurr((dx, 0));
425		g.bd.update();
426		g.pos = np;
427	}
428}
429
430Game.rotate(g: self ref Game, clockwise: int)
431{
432	inc := 1;
433	if (!clockwise)
434		inc = -1;
435	npiece := g.curr;
436	coords := shapes[npiece.shape].coords;
437	nrots := len coords;
438	npiece.rot = (npiece.rot + inc + nrots) % nrots;
439	if (canmove(g, npiece, g.pos)) {
440		c := coords[npiece.rot];
441		for (i := 0; i < len c; i++)
442			g.bd.moveblock(g.pieceids[i], g.pos.add(c[i]));
443		g.curr = npiece;
444		g.bd.update();
445	}
446}
447
448Game.tick(g: self ref Game): int
449{
450	if (canmove(g, g.curr, g.pos.add((0, 1)))) {
451		g.bd.movecurr((0, 1));
452		g.pos.y++;
453	} else {
454		c := shapes[g.curr.shape].coords[g.curr.rot];
455		max := g.pos.y;
456		min := g.pos.y + 4;
457		for (i := 0; i < len c; i++) {
458			p := g.pos.add(c[i]);
459			if (p.y < 0) {
460				g.delay = -1;
461				g.bd.gameover();
462				g.bd.update();
463				return -1;
464			}
465			if (p.y > max)
466				max = p.y;
467			if (p.y < min)
468				min = p.y;
469			g.bd.landedblock(g.pieceids[i], p);
470		}
471		full: list of int;
472		for (i = min; i <= max; i++) {
473			for (x := 0; x < BOARDWIDTH; x++)
474				if (g.bd.state[i][x] == byte 0)
475					break;
476			if (x == BOARDWIDTH)
477				full = i :: full;
478		}
479		if (full != nil) {
480			g.bd.delrows(full);
481			g.nrows += len full;
482			g.bd.setnrows(g.nrows);
483			level := g.nrows / 10;
484			if (level != g.level) {
485				g.bd.setlevel(level);
486				g.level  = level;
487				if (level >= len delays)
488					level = len delays - 1;
489				g.delay = delays[level];
490			}
491		}
492		g.score += shapes[g.curr.shape].score[g.curr.rot];
493		g.bd.setscore(g.score);
494		newpiece(g);
495	}
496	g.bd.update();
497	return 0;
498}
499
500Game.drop(g: self ref Game)
501{
502	p := g.pos.add((0, 1));
503	while (canmove(g, g.curr, p))
504		p.y++;
505	p.y--;
506	g.bd.movecurr((0, p.y - g.pos.y));
507	g.pos = p;
508	g.bd.update();
509}
510
511canmove(g: ref Game, piece: Piece, p: Point): int
512{
513	c := shapes[piece.shape].coords[piece.rot];
514	for (i := 0; i < len c; i++) {
515		q := p.add(c[i]);
516		if (q.x < 0 || q.x >= BOARDWIDTH || q.y >= BOARDHEIGHT)
517			return 0;
518		if (q.y >= 0 && int g.bd.state[q.y][q.x])
519			return 0;
520	}
521	return 1;
522}
523
524newpiece(g: ref Game)
525{
526	g.curr = g.next;
527	g.next = randompiece();
528	g.bd.setnextshape(shapes[g.next.shape].colour, shapes[g.next.shape].coords[g.next.rot]);
529	shape := shapes[g.curr.shape];
530	coords := shape.coords[g.curr.rot];
531	g.pos = (3, -4);
532	for (i := 0; i < len coords; i++)
533		g.pieceids[i] = g.bd.makeblock(shape.colour, g.pos.add(coords[i]));
534}
535
536p2s(p: Point): string
537{
538	return string p.x + " " + string p.y;
539}
540
541Board.new(top: ref Tk->Toplevel, w: string, blocksize: int, maxsize: Point): ref Board
542{
543	cmd(top, "frame " + w);
544	cmd(top, "canvas " + w + ".c -borderwidth 2 -relief sunken -width 1 -height 1");
545	cmd(top, "frame " + w + ".f");
546	cmd(top, "canvas " + w + ".f.ns -width 1 -height 1");
547	makescorewidget(top, w + ".f.scoref", "Score");
548	makescorewidget(top, w + ".f.levelf", "Level");
549	makescorewidget(top, w + ".f.rowsf", "Rows");
550	cmd(top, "pack " + w + ".c -side left");
551	cmd(top, "pack " + w + ".f -side top");
552	cmd(top, "pack " + w + ".f.ns -side top");
553	cmd(top, "pack " + w + ".f.scoref -side top -fill x");
554	cmd(top, "pack " + w + ".f.levelf -side top -fill x");
555	cmd(top, "pack " + w + ".f.rowsf -side top -fill x");
556
557	sz := wsize(top, w);
558	avail := Point(maxsize.x - sz.x, maxsize.y);
559	avail.x /= BOARDWIDTH;
560	avail.y /= BOARDHEIGHT;
561	dx := avail.x;
562	if (avail.y < avail.x)
563		dx = avail.y;
564	if (dx <= 0)
565		return nil;
566	if (dx > blocksize)
567		dx = blocksize;
568	cmd(top, w + ".f.ns configure -width " + string(4 * dx + 1 - 2*2) +
569			" -height " + string(4 * dx + 1 - 2*2));
570	cmd(top, w + ".c configure -width " + string(dx * BOARDWIDTH + 1) +
571		" -height " + string(dx * BOARDHEIGHT + 1));
572	bd := ref Board(array[BOARDHEIGHT]
573					of {* => array[BOARDWIDTH] of {* => byte 0}},
574			w, dx, top, array[BOARDHEIGHT]  of {* => Row(nil, 0)}, 1);
575	return bd;
576}
577
578makescorewidget(top: ref Tk->Toplevel, w, title: string)
579{
580	cmd(top, "frame " + w);
581	cmd(top, "label " + w + ".title -text " + tk->quote(title));
582	cmd(top, "label " + w +
583		".val -bd 2 -relief sunken -width 5w -text 0 -anchor e");
584	cmd(top, "pack " + w + ".title -side left -anchor w");
585	cmd(top, "pack " + w + ".val -side right -anchor e");
586}
587
588blockrect(bd: ref Board, p: Point): string
589{
590	p = p.mul(bd.dx);
591	q := p.add((bd.dx, bd.dx));
592	return string p.x + " " + string p.y + " " + string q.x + " " + string q.y;
593}
594
595Board.makeblock(bd: self ref Board, colour: string, p: Point): string
596{
597	tag := cmd(bd.win, bd.w + ".c create rectangle " + blockrect(bd, p) + " -fill " + colour + " -tags curr");
598	if (tag != nil && tag[0] == '!')
599		return nil;
600	return tag;
601}
602
603Board.moveblock(bd: self ref Board, b: string, p: Point)
604{
605	cmd(bd.win, bd.w + ".c coords " + b + " " + blockrect(bd, p));
606}
607
608Board.movecurr(bd: self ref Board, delta: Point)
609{
610	delta = delta.mul(bd.dx);
611	cmd(bd.win, bd.w + ".c move curr " + string delta.x + " " + string delta.y);
612}
613
614Board.landedblock(bd: self ref Board, b: string, p: Point)
615{
616	cmd(bd.win, bd.w + ".c dtag " + b + " curr");
617	rs := cmd(bd.win, bd.w + ".c coords " + b);
618	if (rs != nil && rs[0] == '!')
619		return;
620	(nil, toks) := sys->tokenize(rs, " ");
621	if (len toks != 4) {
622		sys->fprint(stderr, "bad coords for block %s\n", b);
623		return;
624	}
625	y := int hd tl toks / bd.dx;
626	if (y < 0)
627		return;
628	if (y >= BOARDHEIGHT) {
629		sys->fprint(stderr, "block '%s' too far down (coords %s)\n", b, rs);
630		return;
631	}
632	rtag := bd.rows[y].tag;
633	if (rtag == nil)
634		rtag = bd.rows[y].tag = "r" + string bd.maxid++;
635	cmd(bd.win, bd.w + ".c addtag " + rtag + " withtag " + b);
636	if (p.y >= 0)
637		bd.state[p.y][p.x] = byte 1;
638}
639
640Board.delrows(bd: self ref Board, rows: list of int)
641{
642	while (rows != nil) {
643		r := hd rows;
644		bd.rows[r].delete = 1;
645		rows = tl rows;
646	}
647	j := BOARDHEIGHT - 1;
648	for (i := BOARDHEIGHT - 1; i >= 0; i--) {
649		if (bd.rows[i].delete) {
650			cmd(bd.win, bd.w + ".c delete " + bd.rows[i].tag);
651			bd.rows[i] = (nil, 0);
652			bd.state[i] = nil;
653		} else {
654			if (i != j && bd.rows[i].tag != nil) {
655				dy := (j - i) * bd.dx;
656				cmd(bd.win, bd.w + ".c move " + bd.rows[i].tag + " 0 " + string dy);
657				bd.rows[j] = bd.rows[i];
658				bd.rows[i] = (nil, 0);
659				bd.state[j] = bd.state[i];
660				bd.state[i] = nil;
661			}
662			j--;
663		}
664	}
665	for (i = 0; i < BOARDHEIGHT; i++)
666		if (bd.state[i] == nil)
667			bd.state[i] = array[BOARDWIDTH] of {* => byte 0};
668}
669
670Board.update(bd: self ref Board)
671{
672	cmd(bd.win, "update");
673}
674
675Board.setnextshape(bd: self ref Board, colour: string, spec: array of Point)
676{
677	cmd(bd.win, bd.w + ".f.ns delete all");
678	min := Point(4,4);
679	max := Point(0,0);
680	for (i := 0; i < len spec; i++) {
681		if (spec[i].x > max.x) max.x = spec[i].x;
682		if (spec[i].x < min.x) min.x = spec[i].x;
683		if (spec[i].y > max.y) max.y = spec[i].y;
684		if (spec[i].y < min.y) min.y = spec[i].y;
685	}
686	o: Point;
687	o.x = (4 - (max.x - min.x + 1)) * bd.dx / 2 - min.x * bd.dx;
688	o.y = (4 - (max.y - min.y + 1)) * bd.dx / 2 - min.y * bd.dx;
689	for (i = 0; i < len spec; i++) {
690		br := Rect(o.add(spec[i].mul(bd.dx)), o.add(spec[i].add((1,1)).mul(bd.dx)));
691		cmd(bd.win, bd.w + ".f.ns create rectangle " +
692			string br.min.x + " " + string br.min.y + " " + string br.max.x + " " + string br.max.y +
693			" -fill " + colour);
694	}
695}
696
697Board.setscore(bd: self ref Board, score: int)
698{
699	cmd(bd.win, bd.w + ".f.scoref.val configure -text " + string score);
700}
701
702Board.setlevel(bd: self ref Board, level: int)
703{
704	cmd(bd.win, bd.w + ".f.levelf.val configure -text " + string level);
705}
706
707Board.setnrows(bd: self ref Board, nrows: int)
708{
709	cmd(bd.win, bd.w + ".f.rowsf.val configure -text " + string nrows);
710}
711
712Board.gameover(bd: self ref Board)
713{
714	cmd(bd.win, "label " + bd.w + ".gameover -text {Game over} -bd 4 -relief ridge");
715	p := Point(BOARDWIDTH * bd.dx / 2, BOARDHEIGHT * bd.dx / 3);
716	cmd(bd.win, bd.w + ".c create window " + string p.x + " " + string p.y + " -window " + bd.w + ".gameover");
717}
718
719cmd(top: ref Tk->Toplevel, s: string): string
720{
721	e := tk->cmd(top, s);
722#	sys->print("%s\n", s);
723	if (e != nil && e[0] == '!')
724		sys->fprint(stderr, "tetris: tk error on '%s': %s\n", s, e);
725	return e;
726}
727
728VIOLET: con "#ffaaff";
729CYAN: con "#93ddf1";
730
731delays := array[] of {300, 250, 200, 150, 100, 80};
732
733shapes := array[] of {
734Shape(
735	# ####
736	array[] of {
737		array[] of {Point(0,1), Point(1,1), Point(2,1), Point(3,1)},
738		array[] of {Point(1,0), Point(1,1), Point(1,2), Point(1,3)},
739	},
740	"red",
741	array[] of {5, 8}),
742Shape(
743	# ##
744	# ##
745	array[] of {
746		array[] of {Point(0,0), Point(0,1), Point(1,0), Point(1,1)},
747	},
748	"orange",
749	array[] of {6}),
750Shape(
751	# #
752	# ##
753	# #
754	array[] of {
755		array[] of {Point(1,0), Point(0,1), Point(1,1), Point(2,1)},
756		array[] of {Point(1,0), Point(1,1), Point(2,1), Point(1,2)},
757		array[] of {Point(0,1), Point(1,1), Point(2,1), Point(1,2)},
758		array[] of {Point(1,0), Point(0,1), Point(1,1), Point(1,2)},
759	},
760	"yellow",
761	array[] of {5,5,6,5}),
762Shape(
763	# ##
764	#  ##
765	array[] of {
766		array[] of {Point(0,0), Point(1,0), Point(1,1), Point(2,1)},
767		array[] of {Point(1,0), Point(0,1), Point(1,1), Point(0,2)},
768	},
769	"green",
770	array[] of {6,7}),
771Shape(
772	#  ##
773	# ##
774	array[] of {
775		array[] of {Point(1,0), Point(2,0), Point(0,1), Point(1,1)},
776		array[] of {Point(0,0), Point(0,1), Point(1,1), Point(1,2)},
777	},
778	"blue",
779	array[] of {6,7}),
780Shape(
781	# ###
782	# #
783	array[] of {
784		array[] of {Point(2,0), Point(0,1), Point(1,1), Point(2,1)},
785		array[] of {Point(0,0), Point(0,1), Point(0,2), Point(1,2)},
786		array[] of {Point(0,0), Point(1,0), Point(2,0), Point(0,1)},
787		array[] of {Point(0,0), Point(1,0), Point(1,1), Point(1,2)},
788	},
789	CYAN,
790	array[] of {6,7,6,7}),
791Shape(
792	# #
793	# ###
794	array[] of {
795		array[] of {Point(0,0), Point(1,0), Point(2,0), Point(2,1)},
796		array[] of {Point(1,0), Point(1,1), Point(0,2), Point(1,2)},
797		array[] of {Point(0,0), Point(0,1), Point(1,1), Point(2,1)},
798		array[] of {Point(0,0), Point(1,0), Point(0,1), Point(0,2)},
799	},
800	VIOLET,
801	array[] of {6,7,6,7}
802),
803};
804
805