summaryrefslogtreecommitdiff
path: root/appl/wm/tetris.b
diff options
context:
space:
mode:
Diffstat (limited to 'appl/wm/tetris.b')
-rw-r--r--appl/wm/tetris.b806
1 files changed, 806 insertions, 0 deletions
diff --git a/appl/wm/tetris.b b/appl/wm/tetris.b
new file mode 100644
index 00000000..ddde1a17
--- /dev/null
+++ b/appl/wm/tetris.b
@@ -0,0 +1,806 @@
+# Copyright © 1999 Roger Peppe. All rights reserved.
+implement Tetris;
+
+include "sys.m";
+ sys: Sys;
+ stderr: ref Sys->FD;
+include "draw.m";
+ draw: Draw;
+ Point, Rect: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "rand.m";
+ rand: Rand;
+include "scoretable.m";
+ scoretab: Scoretable;
+include "arg.m";
+include "keyboard.m";
+ Up, Down, Right, Left: import Keyboard;
+
+include "keyring.m";
+include "security.m"; # for random seed
+
+Tetris: module {
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+SCORETABLE: con "/lib/scores/tetris";
+LOCKPORT: con 18343;
+
+# number of pieces across and down board.
+BOARDWIDTH: con 10;
+BOARDHEIGHT: con 22;
+
+awaitingscore := 1;
+
+Row: adt {
+ tag: string;
+ delete: int;
+};
+
+Board: adt {
+ new: fn(top: ref Tk->Toplevel, w: string,
+ blocksize: int, maxsize: Point): ref Board;
+ makeblock: fn(bd: self ref Board, colour: string, p: Point): string;
+ moveblock: fn(bd: self ref Board, b: string, p: Point);
+ movecurr: fn(bd: self ref Board, delta: Point);
+ delrows: fn(bd: self ref Board, rows: list of int);
+ landedblock: fn(bd: self ref Board, b: string, p: Point);
+ setnextshape: fn(bd: self ref Board, colour: string, spec: array of Point);
+ setscore: fn(bd: self ref Board, score: int);
+ setlevel: fn(bd: self ref Board, level: int);
+ setnrows: fn(bd: self ref Board, level: int);
+ gameover: fn(bd: self ref Board);
+ update: fn(bd: self ref Board);
+
+ state: array of array of byte;
+ w: string;
+ dx: int;
+ win: ref Tk->Toplevel;
+ rows: array of Row;
+ maxid: int;
+};
+
+Piece: adt {
+ shape: int;
+ rot: int;
+};
+
+Shape: adt {
+ coords: array of array of Point;
+ colour: string;
+ score: array of int;
+};
+
+Game: adt {
+ new: fn(bd: ref Board): ref Game;
+ move: fn(g: self ref Game, dx: int);
+ rotate: fn(g: self ref Game, clockwise: int);
+ tick: fn(g: self ref Game): int;
+ drop: fn(g: self ref Game);
+
+ bd: ref Board;
+ level: int;
+ delay: int;
+ score: int;
+ nrows: int;
+ pieceids: array of string;
+ pos: Point;
+ next,
+ curr: Piece;
+};
+
+badmod(path: string)
+{
+ sys->fprint(stderr, "tetris: cannot load %s: %r\n", path);
+ raise "fail: bad module";
+}
+
+usage()
+{
+ sys->fprint(stderr, "usage: tetris [-b blocksize]\n");
+ raise "fail:usage";
+}
+
+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;
+ if (tk == nil)
+ badmod(Tk->PATH);
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil)
+ badmod(Tkclient->PATH);
+ tkclient->init();
+ rand = load Rand Rand->PATH;
+ if (rand == nil)
+ badmod(Rand->PATH);
+ arg := load Arg Arg->PATH;
+ if (arg == nil)
+ badmod(Arg->PATH);
+ if (ctxt == nil)
+ ctxt = tkclient->makedrawcontext();
+ blocksize := 17; # preferred block size
+ arg->init(argv);
+ while ((opt := arg->opt()) != 0) {
+ case opt {
+ 'b' =>
+ if ((b := arg->arg()) == nil || int b <= 0)
+ usage();
+ blocksize = int b;
+ * =>
+ usage();
+ }
+ }
+ if (arg->argv() != nil)
+ usage();
+
+ sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+ scoretab = load Scoretable Scoretable->PATH;
+ scorech := chan of int;
+ spawn scoresrvwait(scorech);
+ (win, winctl) := tkclient->toplevel(ctxt, "", "Tetris",Tkclient->Hide);
+ seedrand();
+ fromuser := chan of string;
+ tk->namechan(win, fromuser, "user");
+ cmd(win, "bind . <Key> {send user k %s}");
+ cmd(win, "bind . <ButtonRelease-1> {focus .}");
+ cmd(win, "bind .Wm_t <ButtonRelease-1> +{focus .}");
+ cmd(win, "focus .");
+
+ maxsize := Point(10000, 10000);
+ if (ctxt.display.image != nil) {
+ img := ctxt.display.image;
+ wsz := wsize(win, ".");
+ maxsize.y = img.r.dy() - wsz.y;
+ maxsize.x = img.r.dx();
+ }
+
+ tkclient->onscreen(win, nil);
+ tkclient->startinput(win, "kbd"::"ptr"::nil);
+ for (;;) {
+ bd := Board.new(win, ".f", blocksize, maxsize);
+ if (bd == nil) {
+ sys->fprint(stderr, "tetris: couldn't make board\n");
+ return;
+ }
+ cmd(win, "bind .f.c <ButtonRelease-1> {send user m %x %y}");
+ cmd(win, "pack .f -side top");
+ cmd(win, "update");
+ g := Game.new(bd);
+ (finished, rank) := rungame(g, win, fromuser, winctl, scorech);
+ if (finished)
+ break;
+ cmd(win, "pack propagate . 0");
+ if (scoretab != nil) {
+ cmd(win, "destroy .f");
+ if (showhighscores(win, fromuser, winctl, rank) == 0)
+ break;
+ } else
+ cmd(win, "destroy .f");
+ }
+}
+
+wsize(win: ref Tk->Toplevel, w: string): Point
+{
+ bd := int cmd(win, w + " cget -bd");
+ return (int cmd(win, w + " cget -width") + bd * 2,
+ int cmd(win, w + " cget -height") + bd * 2);
+}
+
+rungame(g: ref Game, win: ref Tk->Toplevel, fromuser: chan of string, winctl: chan of string, scorech: chan of int): (int, int)
+{
+ tickchan := chan of int;
+ spawn ticker(g, tickchan);
+ paused := 0;
+ tch := chan of int;
+
+ gameover := 0;
+ rank := -1;
+ bdsize := wsize(win, ".f.c");
+ boundy := bdsize.y * 2 / 3;
+ id := cmd(win, ".f.c create line " + p2s((0, boundy)) + " " + p2s((bdsize.x, boundy)) +
+ " -fill white");
+ cmd(win, ".f.c lower " + id);
+ for (;;) alt {
+ s := <-win.ctxt.kbd =>
+ tk->keyboard(win, s);
+ s := <-win.ctxt.ptr =>
+ tk->pointer(win, *s);
+ s := <-fromuser =>
+ key: int;
+ if (s[0] == 'm') {
+ (nil, toks) := sys->tokenize(s, " ");
+ p := Point(int hd tl toks, int hd tl tl toks);
+ if (p.y > boundy)
+ key = ' ';
+ else {
+ x := p.x / (bdsize.x / 3);
+ case x {
+ 0 =>
+ key = '7';
+ 1 =>
+ key = '8';
+ 2 =>
+ key = '9';
+ * =>
+ break;
+ }
+ }
+ } else if (s[0] == 'k')
+ key = int s[1:];
+ else
+ sys->print("oops (%s)\n", s);
+ if (gameover)
+ return (key == 'q', rank);
+ if (paused) {
+ paused = 0;
+ (tickchan, tch) = (tch, tickchan);
+ if (key != 'q')
+ continue;
+ }
+ case key {
+ '9' or 'c' or Right =>
+ g.move(1);
+ '7' or 'z' or Left =>
+ g.move(-1);
+ '8' or 'x' or Up =>
+ g.rotate(0);
+ ' ' or Down =>
+ g.drop();
+ 'p' =>
+ paused = 1;
+ (tickchan, tch) = (tch, tickchan);
+ 'q' =>
+ g.delay = -1;
+ while (<-tickchan)
+ ;
+ return (1, rank);
+ }
+ s := <-win.ctxt.ctl or
+ s = <-win.wreq or
+ s = <-winctl =>
+ tkclient->wmctl(win, s);
+ n := <-tickchan =>
+ if (g.tick() == -1) {
+ while (n)
+ n = <-tickchan;
+ if (awaitingscore && !<-scorech) {
+ awaitingscore = 0;
+ scoretab = nil;
+ }
+ if (scoretab != nil)
+ rank = scoretab->setscore(g.score, sys->sprint("%d %d %bd", g.nrows, g.level,
+ big readfile("/dev/time") / big 1000000));
+ gameover = 1;
+ }
+ ok := <-scorech =>
+ awaitingscore = 0;
+ if (!ok)
+ scoretab = nil;
+ }
+}
+
+tablerow(win: ref Tk->Toplevel, w, bg: string, relief: string, vals: array of string, widths: array of string)
+{
+ cmd(win, "frame " + w + " -bd 2 -relief " + relief);
+ for (i := 0; i < len vals; i++) {
+ cw := cmd(win, "label " + w + "." + string i + " -text " + tk->quote(vals[i]) + " -width " + widths[i] + bg);
+ cmd(win, "pack " + cw + " -side left -anchor w");
+ }
+ cmd(win, "pack " + w + " -side top");
+}
+
+showhighscores(win: ref Tk->Toplevel, fromuser: chan of string, winctl: chan of string, rank: int): int
+{
+ widths := array[] of {"10w", "7w", "7w", "5w"}; # user, score, level, rows
+ cmd(win, "frame .f -bd 4 -relief raised");
+ cmd(win, "label .f.title -text {High Scores}");
+ cmd(win, "pack .f.title -side top -anchor n");
+ tablerow(win, ".f.h", nil, "raised", array[] of {"User", "Score", "Level", "Rows"}, widths);
+ sl := scoretab->scores();
+ n := 0;
+ while (sl != nil) {
+ s := hd sl;
+ bg := "";
+ if (n == rank)
+ bg = " -bg white";
+ f := ".f.f" + string n++;
+ nrows := level := "";
+ (nil, toks) := sys->tokenize(s.other, " ");
+ if (toks != nil)
+ (nrows, toks) = (hd toks, tl toks);
+ if (toks != nil)
+ level = hd toks;
+ tablerow(win, f, bg, "sunken", array[] of {s.user, string s.score, level, nrows}, widths);
+ sl = tl sl;
+ }
+ cmd(win, "button .f.b -text {New game} -command {send user s}");
+ cmd(win, "pack .f.b -side top");
+ cmd(win, "pack .f -side top");
+ cmd(win, "update");
+ for (;;) alt {
+ s := <-win.ctxt.kbd =>
+ tk->keyboard(win, s);
+ s := <-win.ctxt.ptr =>
+ tk->pointer(win, *s);
+ s := <-fromuser =>
+ if (s[0] == 'k') {
+ cmd(win, "destroy .f");
+ return int s[1:] != 'q';
+ } else if (s[0] == 's') {
+ cmd(win, "destroy .f");
+ return 1;
+ }
+ s := <-win.ctxt.ctl or
+ s = <-win.wreq or
+ s = <-winctl =>
+ tkclient->wmctl(win, s);
+ }
+}
+
+scoresrvwait(ch: chan of int)
+{
+ if (scoretab == nil) {
+ ch <-= 0;
+ return;
+ }
+ (ok, err) := scoretab->init(LOCKPORT, readfile("/dev/user"), "tetris", SCORETABLE);
+ if (ok != -1)
+ ch <-= 1;
+ else {
+ if (err != "timeout")
+ sys->fprint(stderr, "tetris: scoretable error: %s\n", err);
+ else
+ sys->fprint(stderr, "tetris: timed out trying to connect to score server\n");
+ ch <-= 0;
+ }
+}
+
+readfile(f: string): string
+{
+ fd := sys->open(f, Sys->OREAD);
+ if (fd == nil)
+ return nil;
+ buf := array[Sys->ATOMICIO] of byte;
+ n := sys->read(fd, buf, len buf);
+ if (n <= 0)
+ return nil;
+ return string buf[0:n];
+}
+
+ticker(g: ref Game, c: chan of int)
+{
+ c <-= 1;
+ while (g.delay >= 0) {
+ sys->sleep(g.delay);
+ c <-= 1;
+ }
+ c <-= 0;
+}
+
+seedrand()
+{
+ random := load Random Random->PATH;
+ if (random == nil) {
+ sys->fprint(stderr, "tetris: cannot load %s: %r\n", Random->PATH);
+ return;
+ }
+ seed := random->randomint(Random->ReallyRandom);
+ rand->init(seed);
+}
+
+Game.new(bd: ref Board): ref Game
+{
+ g := ref Game;
+ g.bd = bd;
+ g.level = 0;
+ g.pieceids = array[4] of string;
+ g.score = 0;
+ g.delay = delays[g.level];
+ g.nrows = 0;
+ g.next = randompiece();
+ newpiece(g);
+ bd.update();
+ return g;
+}
+
+randompiece(): Piece
+{
+ p: Piece;
+ p.shape = rand->rand(len shapes);
+ p.rot = rand->rand(len shapes[p.shape].coords);
+ return p;
+}
+
+Game.move(g: self ref Game, dx: int)
+{
+ np := g.pos.add((dx, 0));
+ if (canmove(g, g.curr, np)) {
+ g.bd.movecurr((dx, 0));
+ g.bd.update();
+ g.pos = np;
+ }
+}
+
+Game.rotate(g: self ref Game, clockwise: int)
+{
+ inc := 1;
+ if (!clockwise)
+ inc = -1;
+ npiece := g.curr;
+ coords := shapes[npiece.shape].coords;
+ nrots := len coords;
+ npiece.rot = (npiece.rot + inc + nrots) % nrots;
+ if (canmove(g, npiece, g.pos)) {
+ c := coords[npiece.rot];
+ for (i := 0; i < len c; i++) {
+ np := g.pos.add(c[i]);
+ g.bd.moveblock(g.pieceids[i], g.pos.add(c[i]));
+ }
+ g.curr = npiece;
+ g.bd.update();
+ }
+}
+
+Game.tick(g: self ref Game): int
+{
+ if (canmove(g, g.curr, g.pos.add((0, 1)))) {
+ g.bd.movecurr((0, 1));
+ g.pos.y++;
+ } else {
+ c := shapes[g.curr.shape].coords[g.curr.rot];
+ max := g.pos.y;
+ min := g.pos.y + 4;
+ for (i := 0; i < len c; i++) {
+ p := g.pos.add(c[i]);
+ if (p.y < 0) {
+ g.delay = -1;
+ g.bd.gameover();
+ g.bd.update();
+ return -1;
+ }
+ if (p.y > max)
+ max = p.y;
+ if (p.y < min)
+ min = p.y;
+ g.bd.landedblock(g.pieceids[i], p);
+ }
+ full: list of int;
+ for (i = min; i <= max; i++) {
+ for (x := 0; x < BOARDWIDTH; x++)
+ if (g.bd.state[i][x] == byte 0)
+ break;
+ if (x == BOARDWIDTH)
+ full = i :: full;
+ }
+ if (full != nil) {
+ g.bd.delrows(full);
+ g.nrows += len full;
+ g.bd.setnrows(g.nrows);
+ level := g.nrows / 10;
+ if (level != g.level) {
+ g.bd.setlevel(level);
+ g.level = level;
+ if (level >= len delays)
+ level = len delays - 1;
+ g.delay = delays[level];
+ }
+ }
+ g.score += shapes[g.curr.shape].score[g.curr.rot];
+ g.bd.setscore(g.score);
+ newpiece(g);
+ }
+ g.bd.update();
+ return 0;
+}
+
+Game.drop(g: self ref Game)
+{
+ p := g.pos.add((0, 1));
+ while (canmove(g, g.curr, p))
+ p.y++;
+ p.y--;
+ g.bd.movecurr((0, p.y - g.pos.y));
+ g.pos = p;
+ g.bd.update();
+}
+
+canmove(g: ref Game, piece: Piece, p: Point): int
+{
+ c := shapes[piece.shape].coords[piece.rot];
+ for (i := 0; i < len c; i++) {
+ q := p.add(c[i]);
+ if (q.x < 0 || q.x >= BOARDWIDTH || q.y >= BOARDHEIGHT)
+ return 0;
+ if (q.y >= 0 && int g.bd.state[q.y][q.x])
+ return 0;
+ }
+ return 1;
+}
+
+newpiece(g: ref Game)
+{
+ piece := g.curr = g.next;
+ g.next = randompiece();
+ g.bd.setnextshape(shapes[g.next.shape].colour, shapes[g.next.shape].coords[g.next.rot]);
+ shape := shapes[g.curr.shape];
+ coords := shape.coords[g.curr.rot];
+ g.pos = (3, -4);
+ for (i := 0; i < len coords; i++)
+ g.pieceids[i] = g.bd.makeblock(shape.colour, g.pos.add(coords[i]));
+}
+
+p2s(p: Point): string
+{
+ return string p.x + " " + string p.y;
+}
+
+Board.new(top: ref Tk->Toplevel, w: string, blocksize: int, maxsize: Point): ref Board
+{
+ cmd(top, "frame " + w);
+ cmd(top, "canvas " + w + ".c -borderwidth 2 -relief sunken -width 1 -height 1");
+ cmd(top, "frame " + w + ".f");
+ cmd(top, "canvas " + w + ".f.ns -width 1 -height 1");
+ makescorewidget(top, w + ".f.scoref", "Score");
+ makescorewidget(top, w + ".f.levelf", "Level");
+ makescorewidget(top, w + ".f.rowsf", "Rows");
+ cmd(top, "pack " + w + ".c -side left");
+ cmd(top, "pack " + w + ".f -side top");
+ cmd(top, "pack " + w + ".f.ns -side top");
+ cmd(top, "pack " + w + ".f.scoref -side top -fill x");
+ cmd(top, "pack " + w + ".f.levelf -side top -fill x");
+ cmd(top, "pack " + w + ".f.rowsf -side top -fill x");
+
+ sz := wsize(top, w);
+ avail := Point(maxsize.x - sz.x, maxsize.y);
+ avail.x /= BOARDWIDTH;
+ avail.y /= BOARDHEIGHT;
+ dx := avail.x;
+ if (avail.y < avail.x)
+ dx = avail.y;
+ if (dx <= 0)
+ return nil;
+ if (dx > blocksize)
+ dx = blocksize;
+ cmd(top, w + ".f.ns configure -width " + string(4 * dx + 1 - 2*2) +
+ " -height " + string(4 * dx + 1 - 2*2));
+ cmd(top, w + ".c configure -width " + string(dx * BOARDWIDTH + 1) +
+ " -height " + string(dx * BOARDHEIGHT + 1));
+ bd := ref Board(array[BOARDHEIGHT]
+ of {* => array[BOARDWIDTH] of {* => byte 0}},
+ w, dx, top, array[BOARDHEIGHT] of {* => Row(nil, 0)}, 1);
+ return bd;
+}
+
+makescorewidget(top: ref Tk->Toplevel, w, title: string)
+{
+ cmd(top, "frame " + w);
+ cmd(top, "label " + w + ".title -text " + tk->quote(title));
+ cmd(top, "label " + w +
+ ".val -bd 3 -relief sunken -width 5w -text 0 -anchor e");
+ cmd(top, "pack " + w + ".title -side left -anchor w");
+ cmd(top, "pack " + w + ".val -side right -anchor e");
+}
+
+blockrect(bd: ref Board, p: Point): string
+{
+ p = p.mul(bd.dx);
+ q := p.add((bd.dx, bd.dx));
+ return string p.x + " " + string p.y + " " + string q.x + " " + string q.y;
+}
+
+Board.makeblock(bd: self ref Board, colour: string, p: Point): string
+{
+ tag := cmd(bd.win, bd.w + ".c create rectangle " + blockrect(bd, p) + " -fill " + colour + " -tags curr");
+ if (tag != nil && tag[0] == '!')
+ return nil;
+ return tag;
+}
+
+Board.moveblock(bd: self ref Board, b: string, p: Point)
+{
+ cmd(bd.win, bd.w + ".c coords " + b + " " + blockrect(bd, p));
+}
+
+Board.movecurr(bd: self ref Board, delta: Point)
+{
+ delta = delta.mul(bd.dx);
+ cmd(bd.win, bd.w + ".c move curr " + string delta.x + " " + string delta.y);
+}
+
+Board.landedblock(bd: self ref Board, b: string, p: Point)
+{
+ cmd(bd.win, bd.w + ".c dtag " + b + " curr");
+ rs := cmd(bd.win, bd.w + ".c coords " + b);
+ if (rs != nil && rs[0] == '!')
+ return;
+ (n, toks) := sys->tokenize(rs, " ");
+ if (len toks != 4) {
+ sys->fprint(stderr, "bad coords for block %s\n", b);
+ return;
+ }
+ y := int hd tl toks / bd.dx;
+ if (y < 0)
+ return;
+ if (y >= BOARDHEIGHT) {
+ sys->fprint(stderr, "block '%s' too far down (coords %s)\n", b, rs);
+ return;
+ }
+ rtag := bd.rows[y].tag;
+ if (rtag == nil)
+ rtag = bd.rows[y].tag = "r" + string bd.maxid++;
+ cmd(bd.win, bd.w + ".c addtag " + rtag + " withtag " + b);
+ if (p.y >= 0)
+ bd.state[p.y][p.x] = byte 1;
+}
+
+Board.delrows(bd: self ref Board, rows: list of int)
+{
+ while (rows != nil) {
+ r := hd rows;
+ bd.rows[r].delete = 1;
+ rows = tl rows;
+ }
+ j := BOARDHEIGHT - 1;
+ for (i := BOARDHEIGHT - 1; i >= 0; i--) {
+ if (bd.rows[i].delete) {
+ cmd(bd.win, bd.w + ".c delete " + bd.rows[i].tag);
+ bd.rows[i] = (nil, 0);
+ bd.state[i] = nil;
+ } else {
+ if (i != j && bd.rows[i].tag != nil) {
+ dy := (j - i) * bd.dx;
+ cmd(bd.win, bd.w + ".c move " + bd.rows[i].tag + " 0 " + string dy);
+ bd.rows[j] = bd.rows[i];
+ bd.rows[i] = (nil, 0);
+ bd.state[j] = bd.state[i];
+ bd.state[i] = nil;
+ }
+ j--;
+ }
+ }
+ for (i = 0; i < BOARDHEIGHT; i++)
+ if (bd.state[i] == nil)
+ bd.state[i] = array[BOARDWIDTH] of {* => byte 0};
+}
+
+Board.update(bd: self ref Board)
+{
+ cmd(bd.win, "update");
+}
+
+Board.setnextshape(bd: self ref Board, colour: string, spec: array of Point)
+{
+ cmd(bd.win, bd.w + ".f.ns delete all");
+ min := Point(4,4);
+ max := Point(0,0);
+ for (i := 0; i < len spec; i++) {
+ if (spec[i].x > max.x) max.x = spec[i].x;
+ if (spec[i].x < min.x) min.x = spec[i].x;
+ if (spec[i].y > max.y) max.y = spec[i].y;
+ if (spec[i].y < min.y) min.y = spec[i].y;
+ }
+ o: Point;
+ o.x = (4 - (max.x - min.x + 1)) * bd.dx / 2 - min.x * bd.dx;
+ o.y = (4 - (max.y - min.y + 1)) * bd.dx / 2 - min.y * bd.dx;
+ for (i = 0; i < len spec; i++) {
+ br := Rect(o.add(spec[i].mul(bd.dx)), o.add(spec[i].add((1,1)).mul(bd.dx)));
+ cmd(bd.win, bd.w + ".f.ns create rectangle " +
+ string br.min.x + " " + string br.min.y + " " + string br.max.x + " " + string br.max.y +
+ " -fill " + colour);
+ }
+}
+
+Board.setscore(bd: self ref Board, score: int)
+{
+ cmd(bd.win, bd.w + ".f.scoref.val configure -text " + string score);
+}
+
+Board.setlevel(bd: self ref Board, level: int)
+{
+ cmd(bd.win, bd.w + ".f.levelf.val configure -text " + string level);
+}
+
+Board.setnrows(bd: self ref Board, nrows: int)
+{
+ cmd(bd.win, bd.w + ".f.rowsf.val configure -text " + string nrows);
+}
+
+Board.gameover(bd: self ref Board)
+{
+ cmd(bd.win, "label " + bd.w + ".gameover -text {Game over} -bd 4 -relief ridge");
+ p := Point(BOARDWIDTH * bd.dx / 2, BOARDHEIGHT * bd.dx / 3);
+ cmd(bd.win, bd.w + ".c create window " + string p.x + " " + string p.y + " -window " + bd.w + ".gameover");
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ e := tk->cmd(top, s);
+# sys->print("%s\n", s);
+ if (e != nil && e[0] == '!')
+ sys->fprint(stderr, "tetris: tk error on '%s': %s\n", s, e);
+ return e;
+}
+
+VIOLET: con "#ffaaff";
+CYAN: con "#93ddf1";
+
+delays := array[] of {300, 250, 200, 150, 100, 80};
+
+shapes := array[] of {
+Shape(
+ # ####
+ array[] of {
+ array[] of {Point(0,1), Point(1,1), Point(2,1), Point(3,1)},
+ array[] of {Point(1,0), Point(1,1), Point(1,2), Point(1,3)},
+ },
+ "red",
+ array[] of {5, 8}),
+Shape(
+ # ##
+ # ##
+ array[] of {
+ array[] of {Point(0,0), Point(0,1), Point(1,0), Point(1,1)},
+ },
+ "orange",
+ array[] of {6}),
+Shape(
+ # #
+ # ##
+ # #
+ array[] of {
+ array[] of {Point(1,0), Point(0,1), Point(1,1), Point(2,1)},
+ array[] of {Point(1,0), Point(1,1), Point(2,1), Point(1,2)},
+ array[] of {Point(0,1), Point(1,1), Point(2,1), Point(1,2)},
+ array[] of {Point(1,0), Point(0,1), Point(1,1), Point(1,2)},
+ },
+ "yellow",
+ array[] of {5,5,6,5}),
+Shape(
+ # ##
+ # ##
+ array[] of {
+ array[] of {Point(0,0), Point(1,0), Point(1,1), Point(2,1)},
+ array[] of {Point(1,0), Point(0,1), Point(1,1), Point(0,2)},
+ },
+ "green",
+ array[] of {6,7}),
+Shape(
+ # ##
+ # ##
+ array[] of {
+ array[] of {Point(1,0), Point(2,0), Point(0,1), Point(1,1)},
+ array[] of {Point(0,0), Point(0,1), Point(1,1), Point(1,2)},
+ },
+ "blue",
+ array[] of {6,7}),
+Shape(
+ # ###
+ # #
+ array[] of {
+ array[] of {Point(2,0), Point(0,1), Point(1,1), Point(2,1)},
+ array[] of {Point(0,0), Point(0,1), Point(0,2), Point(1,2)},
+ array[] of {Point(0,0), Point(1,0), Point(2,0), Point(0,1)},
+ array[] of {Point(0,0), Point(1,0), Point(1,1), Point(1,2)},
+ },
+ CYAN,
+ array[] of {6,7,6,7}),
+Shape(
+ # #
+ # ###
+ array[] of {
+ array[] of {Point(0,0), Point(1,0), Point(2,0), Point(2,1)},
+ array[] of {Point(1,0), Point(1,1), Point(0,2), Point(1,2)},
+ array[] of {Point(0,0), Point(0,1), Point(1,1), Point(2,1)},
+ array[] of {Point(0,0), Point(1,0), Point(0,1), Point(0,2)},
+ },
+ VIOLET,
+ array[] of {6,7,6,7}
+),
+};
+