# 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 . {send user k %s}"); cmd(win, "bind . {focus .}"); cmd(win, "bind .Wm_t +{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 {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; # Needs kicked after new game # Some may be erroneous cmd(win, "bind . {send user k %s}"); cmd(win, "bind . {focus .}"); cmd(win, "bind .Wm_t +{focus .}"); cmd(win, "focus ."); } 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 { key := <-win.ctxt.kbd => tk->keyboard(win, key); ptr := <-win.ctxt.ptr => tk->pointer(win, *ptr); s := <-fromuser => key: int; if (s[0] == 'm') { # Mouse (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; } # New game button - breaks keyboard cmd(win, "button .f.b -text {New game} -command {send user s}"); cmd(win, "pack .f.b -side top"); # Renders final scores cmd(win, "pack .f -side top"); cmd(win, "update"); # Events for scoreboard for (;;) alt { key := <-win.ctxt.kbd => tk->keyboard(win, key); ptr := <-win.ctxt.ptr => tk->pointer(win, *ptr); s := <-fromuser => if (s[0] == 'k') { cmd(win, "destroy .f"); # Don't quit unless 'q' is pressed return int s[1:] != 'q'; } else if (s[0] == 's') { cmd(win, "destroy .f"); # Don't quit 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++) 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) { 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 2 -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; (nil, 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); 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} ), };