Api's Adventure on tulevaa Infernon peliohjelmointia käsittelevää opastani varten tekemäni esimerkki.
Peli on Infernon Limbo toteutus MBasic:in mukana tulevasta esimerkki pelistä: Pitman.
Pelissä ohjataan Kiinanpalatsikoira Apia ja yritetään kerätä kaikki pelikentällä olevat luut.
Peli on vielä hiukan keskeneräinen, mutta jo nyt mukava ja haastava pelattava.
Pelipaketti datatiedostoineen: http://www.tip9ug.jp/who/jalih/game.zip
#
# Just another crappy game from jalski's game factory
#
# It's still unfinished...
# You can do what you like with it.
#
implement ApiAdventure;
include "sys.m";
sys : Sys;
include "draw.m";
draw : Draw;
Context, Display, Point, Rect, Image, Screen, Font: import draw;
include "tk.m";
tk: Tk;
Toplevel : import tk;
include "tkclient.m";
tkclient : Tkclient;
include "keyboard.m";
Up, Down, Right, Left: import Keyboard;
include "bufio.m";
bufio : Bufio;
Iobuf : import bufio;
ApiAdventure: module
{
init: fn(ctxt: ref Context, argv: list of string);
};
EMPTY, WALL, LADDER, WALL2 : con iota;
ROCK, BONE : con iota;
TILEX : con 49;
TILEY : con 49;
ROWS : con 8;
COLUMNS : con 11;
LEFT : con Point(-1, 0);
RIGHT : con Point(1, 0);
UP : con Point(0, -1);
DOWN : con Point(0, 1);
AUDIO_MAGIC : con "rate";
AUDIO_DATA : con "/dev/audio";
AUDIO_CTL : con "/dev/audioctl";
AUDIO_BUF_SIZE : con Sys->ATOMICIO;
NULL_DEVICE : con "/dev/null";
#SOUND1 : con "./sounds/chord.iaf";
audio_data : string;
FONTPATH : con "/fonts/lucida/unicode.18.font";
MSG : con "LEVEL COMPLETE";
font : ref Font;
display : ref Display;
buffer : ref Image;
t: ref Toplevel;
tiles := array [4] of ref Image;
player_img, player_mask : ref Image;
rock_img, rock_mask : ref Image;
bone_img, bone_mask : ref Image;
# Pelaaja "luokka"
Player : adt
{
pos : Point;
img : ref Image;
new : fn(x : int, y : int, img : ref Image) : ref Player;
draw : fn(player : self ref Player);
if_can_fall : fn(player : self ref Player);
};
# Objekti "luokka" pelin objekteja varten (luut ja kivet).
Object : adt
{
id : int;
pos : Point;
img : ref Image;
mask : ref Image;
new : fn(id : int, x : int, y : int, img : ref Image, mask : ref Image) : ref Object;
};
# Objektilista "luokka"
Objects : adt
{
objects : array of ref Object;
new : fn() : ref Objects;
add : fn(objects : self ref Objects, id : int, x : int, y : int, img : ref Image, mask : ref Image);
del : fn(objects : self ref Objects, index : int);
sort : fn(objects : self ref Objects);
draw : fn(objects : self ref Objects);
search_pos : fn(objects : self ref Objects, pos : Point) : (int , ref Object);
};
# Pelikenttä "luokka"
Level : adt
{
bones : int;
map : array of array of int;
objects : ref Objects;
player : ref Player;
new : fn() : ref Level;
draw : fn(l : self ref Level);
copy : fn(l : self ref Level) : ref Level;
can_objects_fall : fn(l : self ref Level);
};
level : ref Level; # Käsiteltävä kenttä.
levels : list of ref Level; # kenttälista.
back_list : list of ref Level; # Lista kenttien taaksepäin selaamista varten.
currentl : int; # Nykyisen kentän numero.
# Käyttöliittymän määrittelyt
gameui_cfg := array[] of {
"frame .f",
"button .f.left -bitmap small_color_left.bit -bd 0 -command {send cmdch prevl}",
"button .f.right -bitmap small_color_right.bit -bd 0 -command {send cmdch nextl}",
"button .f.reset -bitmap sadsmiley.bit -bd 0 -command {send cmdch reset}",
"label .f.l -text {level: }",
"label .f.level",
"pack .f.left .f.right -side left",
"pack .f.l -side left",
"pack .f.level -side left",
"pack .f.reset -side right",
"pack .f -fill x",
};
# Pääohjelma. Ohjelman suoritus alkaa tästä.
init(ctxt: ref 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;
bufio = load Bufio Bufio->PATH;
sys->pctl(Sys->NEWPGRP, nil);
tkclient->init();
if(ctxt == nil)
ctxt = tkclient->makedrawcontext();
display = ctxt.display;
font = Font.open(display, FONTPATH);
menubut : chan of string;
(t, menubut) = tkclient->toplevel(ctxt, "", "Api's Adventure (with doggy style)", 0);
cmdch := chan of string;
tk->namechan(t, cmdch, "cmdch");
for (i := 0; i < len gameui_cfg; i++)
cmd(t, gameui_cfg[i]);
center(t);
tkclient->onscreen(t, "exact");
tkclient->startinput(t, "ptr"::"kbd"::nil);
#setup_audio();
load_images();
load_levels("./levels/levels.txt");
currentl = 1; # Ekasta kentästä aloitellaan.
cmd(t, ".f.level configure -text " + string currentl);
back_list = hd levels :: back_list;
level = (hd levels).copy();
cmd(t, "panel .p -bd 3 -relief flat");
cmd(t, "pack .p -fill both -expand 1");
tk->putimage(t, ".p", buffer, nil);
level.draw();
level.player.draw();
level.objects.draw();
cmd(t, ".p dirty; update");
# Tk-ohjelman viestien käsittely silmukka
for(;;) alt {
s := <-t.ctxt.kbd =>
processkbd(s); # Käsitellään näppäimen painallus omassa aliohjelmassa.
s := <-t.ctxt.ptr =>
tk->pointer(t, *s);
s := <-t.ctxt.ctl or
s = <-t.wreq or
s = <-menubut =>
tkclient->wmctl(t, s);
c := <- cmdch =>
case c {
"nextl" => # Nuoli oikealle nappulaa painettu
if (tl levels != nil)
{
back_list = hd levels :: back_list;
levels = tl levels;
level = (hd levels).copy();
currentl ++;
cmd(t, ".f.level configure -text " + string currentl);
level.draw();
level.player.draw();
level.objects.draw();
#cmd(t, "focus .p");
cmd(t, ".p dirty; update");
}
"prevl" => # Nuoli vasemmalle nappulaa painettu.
if (tl back_list != nil)
{
levels = hd back_list :: levels;
back_list = tl back_list;
level = (hd levels).copy();
currentl --;
cmd(t, ".f.level configure -text " + string currentl);
level.draw();
level.player.draw();
level.objects.draw();
#cmd(t, "focus .p");
cmd(t, ".p dirty; update");
}
"reset" => # Surunaama nappulaa painettu
level = (hd levels).copy();
level.draw();
level.player.draw();
level.objects.draw();
#cmd(t, "focus .p");
cmd(t, ".p dirty; update");
}
}
}
# Täällä käsitellään painallukset näppäimistöltä.
processkbd(kbd: int)
{
case kbd
{
Up =>
npos := level.player.pos.add(UP);
if (npos.y >= ROWS || npos.y < 0)
break;
(nil, found) := level.objects.search_pos(npos);
if ((level.map[level.player.pos.x][level.player.pos.y] == LADDER && level.map[npos.x][npos.y] == LADDER) || (level.map[level.player.pos.x][level.player.pos.y] == LADDER && (level.map[npos.x][npos.y] == EMPTY && found == nil)))
{
draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos);
level.player.pos = npos;
level.player.draw();
}
Down =>
npos := level.player.pos.add(DOWN);
if (npos.y >= ROWS || npos.y < 0)
break;
(nil, obj) := level.objects.search_pos(npos);
if (obj != nil)
break;
if (level.map[npos.x][npos.y] == LADDER || (level.map[level.player.pos.x][level.player.pos.y] == LADDER && level.map[npos.x][npos.y] == EMPTY))
{
draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos);
level.player.pos = npos;
level.player.if_can_fall();
level.player.draw();
level.can_objects_fall();
level.objects.draw();
}
Left =>
npos := level.player.pos.add(LEFT);
if (npos.x >= COLUMNS || npos.x < 0)
break;
if (level.map[npos.x][npos.y] != WALL)
{
(index, obj) := level.objects.search_pos(npos);
if (obj != nil)
{
case obj.id
{
ROCK =>
nrpos := obj.pos.add(LEFT);
if (nrpos.x < COLUMNS && nrpos.x >= 0)
{
(nil, tobj) := level.objects.search_pos(nrpos);
if (tobj == nil)
if (level.map[nrpos.x][nrpos.y] == EMPTY)
{
draw_tile(level.map[obj.pos.x][obj.pos.y], obj.pos);
obj.pos = nrpos;
draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos);
level.player.pos = npos;
if (level.map[level.player.pos.x][level.player.pos.y] != LADDER)
level.player.if_can_fall();
level.objects.sort();
level.can_objects_fall();
level.objects.draw();
level.player.draw();
}
}
BONE =>
level.objects.del(index);
level.bones --;
draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos);
draw_tile(level.map[npos.x][npos.y], npos);
level.player.pos = npos;
if (level.map[level.player.pos.x][level.player.pos.y] != LADDER)
level.player.if_can_fall();
level.objects.sort();
level.can_objects_fall();
level.objects.draw();
level.player.draw();
}
}
else
{
if (level.map[npos.x][npos.y] == WALL2)
{
level.map[npos.x][npos.y] = EMPTY;
draw_tile(level.map[npos.x][npos.y], npos);
}
draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos);
level.player.pos = npos;
if (level.map[level.player.pos.x][level.player.pos.y] != LADDER)
level.player.if_can_fall();
level.objects.sort();
level.can_objects_fall();
level.objects.draw();
level.player.draw();
}
}
Right =>
npos := level.player.pos.add(RIGHT);
if (npos.x >= COLUMNS || npos.x < 0)
break;
if (level.map[npos.x][npos.y] != WALL)
{
(index, obj) := level.objects.search_pos(npos);
if (obj != nil)
{
case obj.id
{
ROCK =>
nrpos := obj.pos.add(RIGHT);
if (nrpos.x < COLUMNS && nrpos.x >= 0)
{
(nil, tobj) := level.objects.search_pos(nrpos);
if (tobj == nil)
if (level.map[nrpos.x][nrpos.y] == EMPTY)
{
draw_tile(level.map[obj.pos.x][obj.pos.y], obj.pos);
obj.pos = nrpos;
draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos);
level.player.pos = npos;
if (level.map[level.player.pos.x][level.player.pos.y] != LADDER)
level.player.if_can_fall();
level.objects.sort();
level.can_objects_fall();
level.objects.draw();
level.player.draw();
}
}
BONE =>
level.objects.del(index);
level.bones --;
draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos);
draw_tile(level.map[npos.x][npos.y], npos);
level.player.pos = npos;
if (level.map[level.player.pos.x][level.player.pos.y] != LADDER)
level.player.if_can_fall();
level.objects.sort();
level.can_objects_fall();
level.objects.draw();
level.player.draw();
}
}
else
{
if (level.map[npos.x][npos.y] == WALL2)
{
level.map[npos.x][npos.y] = EMPTY;
draw_tile(level.map[npos.x][npos.y], npos);
}
draw_tile(level.map[level.player.pos.x][level.player.pos.y], level.player.pos);
level.player.pos = npos;
if (level.map[level.player.pos.x][level.player.pos.y] != LADDER)
level.player.if_can_fall();
level.objects.sort();
level.can_objects_fall();
level.objects.draw();
level.player.draw();
}
}
* =>
return;
}
if (level.bones <= 0)
{
red := display.color(Draw->Red);
textw := font.width(MSG);
texth := font.height;
buffer.text(Point(COLUMNS * TILEX / 2 - (textw / 2), ROWS * TILEY / 2 - texth), red, Point(0,0), font, MSG);
cmd(t, ".p dirty; update");
#play_audio_file(SOUND1);
sys->sleep(500);
if (tl levels != nil)
{
back_list = hd levels :: back_list;
levels = tl levels;
level = (hd levels).copy();
currentl ++;
cmd(t, ".f.level configure -text " + string currentl);
level.draw();
level.player.draw();
level.objects.draw();
cmd(t, ".p dirty; update");
}
else
exit;
}
cmd(t, ".p dirty; update");
}
setup_audio()
{
df := sys->open(AUDIO_DATA, Sys->OWRITE);
if (df == nil)
audio_data = NULL_DEVICE;
else
audio_data = AUDIO_DATA;
}
play_audio_file(f: string)
{
buff := array[AUDIO_BUF_SIZE] of byte;
inf := sys->open(f, Sys->OREAD);
if (inf == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: could not open %s: %r\n", f);
return;
}
n := sys->read(inf, buff, AUDIO_BUF_SIZE);
if (n < 0) {
sys->fprint(sys->fildes(2), "Api's Adventure: could not read %s: %r\n", f);
return;
}
if (n < 10 || string buff[0:4] != AUDIO_MAGIC)
{
sys->fprint(sys->fildes(2), "Api's Adventure: %s: not an audio file\n", f);
return;
}
i := 0;
for (;;) {
if (i == n)
{
sys->fprint(sys->fildes(2), "Api's Adventure: %s: bad header\n", f);
return;
}
if (buff[i] == byte '\n')
{
i++;
if (i == n)
{
sys->fprint(sys->fildes(2), "Api's Adventure: %s: bad header\n", f);
return;
}
if (buff[i] == byte '\n')
{
i++;
if ((i % 4) != 0)
{
sys->fprint(sys->fildes(2), "Api's Adventure: %s: unpadded header\n", f);
return;
}
break;
}
}
else
i++;
}
cf := sys->open(AUDIO_CTL, Sys->OWRITE);
if (cf != nil)
{
if (sys->write(cf, buff, i - 1) < 0)
{
sys->fprint(sys->fildes(2), "Api's Adventure: could not write %s: %r\n", AUDIO_CTL);
exit;
}
}
df := sys->open(audio_data, Sys->OWRITE);
if (df == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: could not open %s: %r\n", audio_data);
return;
}
if (n > i && sys->write(df, buff[i:n], n - i) < 0)
{
sys->fprint(sys->fildes(2), "Api's Adventure: could not write %s: %r\n", audio_data);
return;
}
if (sys->stream(inf, df, Sys->ATOMICIO) < 0)
{
sys->fprint(sys->fildes(2), "Api's Adventure: could not stream %s: %r\n", audio_data);
return;
}
}
Player.new(x : int, y : int, img : ref Image) : ref Player
{
player := ref Player(Point(x,y), img);
return player;
}
Player.if_can_fall(player : self ref Player)
{
pos := player.pos;
if (level.map[pos.x][pos.y] != EMPTY)
return;
for(i := pos.y + 1; i < ROWS; i ++)
{
(nil, found) := level.objects.search_pos(Point(pos.x, i));
if (level.map[pos.x][i] != EMPTY || found != nil)
break;
pos.y = i;
}
if (!player.pos.eq(pos))
{
draw_tile(level.map[player.pos.x][player.pos.y], player.pos);
player.pos = pos;
}
}
Object.new(id : int, x : int, y : int, img : ref Image, mask : ref Image) : ref Object
{
object := ref Object(id, Point(x, y), img, mask);
return object;
}
Objects.new() : ref Objects
{
objects := ref Objects;
return objects;
}
Objects.add(objects : self ref Objects, id : int, x : int, y : int, img : ref Image, mask : ref Image )
{
i := len objects.objects;
nobjects := array [i + 1] of ref Object;
(nobjects[0:], nobjects[i], nobjects[i + 1:]) = (objects.objects[0:i], Object.new(id, x, y, img, mask), objects.objects[i:]);
objects.objects = nobjects;
}
Objects.del(objects : self ref Objects, index : int)
{
if (len objects.objects >= 1)
{
objects.objects[index:] = objects.objects[index + 1:];
objects.objects = objects.objects[:len objects.objects - 1];
}
}
Objects.search_pos(objects : self ref Objects, pos : Point) : (int, ref Object)
{
count := len objects.objects;
if (count < 1)
{
return (0, nil);
}
for(i := 0; i < count; i ++)
{
if (objects.objects[i].pos.eq(pos))
{
return (i, objects.objects[i]);
}
}
return (0, nil);
}
Objects.draw(objects : self ref Objects)
{
count := len objects.objects;
for(i := 0; i < count; i ++)
{
draw_sprite(objects.objects[i].img, objects.objects[i].mask, objects.objects[i].pos);
}
}
Objects.sort(objects : self ref Objects)
{
count := len objects.objects;
for(i := 1; i < count ; i ++)
{
v := objects.objects[i];
j := i;
while( ( j > 0 ) && ( objects.objects[j - 1].pos.y < v.pos.y ))
{
objects.objects[j] = objects.objects[j - 1];
j --;
}
objects.objects[j] = v;
}
}
load_images()
{
tiles[0] = display.open("./data/empty.bit");
if (tiles[0] == nil)
{
sys->fprint(sys->fildes(2), "Apis's Adventure: failed to allocate image\n");
exit;
}
tiles[1] = display.open("./data/wall.bit");
if (tiles[1] == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n");
exit;
}
tiles[2] = display.open("./data/ladder.bit");
if (tiles[2] == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n");
exit;
}
tiles[3] = display.open("./data/wall2.bit");
if (tiles[3] == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n");
exit;
}
rock_img = display.open("./data/rock.bit");
if (rock_img == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n");
exit;
}
rock_mask = display.open("./data/rock_mask.bit");
if (rock_mask == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n");
exit;
}
player_img = display.open("./data/api.bit");
if (player_img == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n");
exit;
}
player_mask = display.open("./data/api_mask.bit");
if (player_mask == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n");
exit;
}
bone_img = display.open("./data/bone.bit");
if (bone_img == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n");
exit;
}
bone_mask = display.open("./data/bone_mask.bit");
if (bone_mask == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n");
exit;
}
r := Rect((0, 0), (COLUMNS * TILEX, ROWS * TILEY));
buffer = display.newimage(r, t.image.chans, 0, Draw->Black);
if (buffer == nil)
{
sys->fprint(sys->fildes(2), "Api's Adventure: failed to allocate image\n");
exit;
}
}
draw_tile(tile : int, p : Point)
{
p.x = p.x * tiles[tile].r.dx();
p.y = p.y * tiles[tile].r.dy();
buffer.draw(tiles[tile].r.addpt(p), tiles[tile], nil, Point(0, 0) );
}
Level.draw(l : self ref Level)
{
for(row := 0; row < ROWS; row ++)
for(column := 0; column < COLUMNS; column ++)
{
p := Point( column * TILEX, row * TILEY );
buffer.draw(tiles[l.map[column][row] ].r.addpt(p), tiles[l.map[column][row] ], nil, Point(0, 0) );
}
}
Level.new() : ref Level
{
l := ref Level;
return l;
}
Level.copy(l: self ref Level): ref Level
{
c := ref *l;
c.map = array[COLUMNS] of { * => array[ROWS] of int };
for(i := 0; i < COLUMNS; i++)
c.map[i][0:] = l.map[i];
c.objects = Objects.new();
for(i = 0; i < len l.objects.objects ; i++)
c.objects.add(l.objects.objects[i].id, l.objects.objects[i].pos.x, l.objects.objects[i].pos.y, l.objects.objects[i].img, l.objects.objects[i].mask);
c.player = ref *l.player;
return c;
}
Level.can_objects_fall(l : self ref Level)
{
count := len l.objects.objects;
for(j := 0; j < count; j ++)
{
pos := l.objects.objects[j].pos;
for(i := pos.y + 1; i < ROWS; i ++)
{
if (l.map[pos.x][i] != EMPTY || Point(pos.x, i).eq(l.player.pos))
break;
(nil, found) := l.objects.search_pos(Point(pos.x, i));
if (found != nil)
break;
pos.y = i;
}
if (!l.objects.objects[j].pos.eq(pos))
{
draw_tile(l.map[l.objects.objects[j].pos.x][l.objects.objects[j].pos.y], l.objects.objects[j].pos);
l.objects.objects[j].pos = pos;
}
}
}
Player.draw(player : self ref Player)
{
draw_sprite(player.img, player_mask, player.pos);
}
draw_sprite(image : ref Image, mask : ref Image, p : Point)
{
p.x = p.x * image.r.dx();
p.y = p.y * image.r.dy();
buffer.draw(image.r.addpt(p), image, mask, Point(0, 0) );
}
load_levels(file: string)
{
x := 0;
y := 0;
max := Point(0, 0);
buf := bufio->open(file, Bufio->OREAD);
if (buf == nil) {
sys->fprint(sys->fildes(2), "Api's Adventure level parser: cannot load %s: %r", file);
exit;
}
bones := 0;
map := array [COLUMNS] of { * => array [ROWS] of { * => EMPTY } };
player := Player.new(0, 0, player_img);
objects := Objects.new();
line := 1;
while((c := buf.getc()) >= 0)
{
case c {
';' =>
while((c = buf.getc()) != '\n' && c != Bufio->EOF)
;
line ++;
'\n' =>
max.y = ++ y;
if(x != COLUMNS)
{
sys->fprint(sys->fildes(2), "Api's Adventure level parser: error on line: %d. Level map must have %d columns\n", line, COLUMNS);
exit;
}
line ++;
x = 0;
if((c = buf.getc()) == '\n' || c == Bufio->EOF)
{
if (max.y != ROWS)
{
sys->fprint(sys->fildes(2), "Api's Adventure level parser: error on line: %d. Level map must have %d rows\n", line, ROWS);
exit;
}
lev := Level.new();
lev.bones = bones;
lev.map = map;
lev.player = player;
lev.objects = objects;
levels = lev :: levels;
bones = 0;
map = array[COLUMNS] of { * => array[ROWS] of { * => EMPTY } };
player = Player.new(0, 0, player_img);
objects = Objects.new();
max = Point(0, 0);
y = 0;
line ++;
}
else
buf.ungetc();
'0' =>
map[x][y] = EMPTY;
x ++;
'1' =>
map[x][y] = WALL;
x ++;
'2' =>
map[x][y] = LADDER;
x ++;
'3' =>
map[x][y] = WALL2;
x ++;
'@' =>
player.pos = Point(x, y);
x ++;
'o' =>
objects.add(ROCK, x, y, rock_img, rock_mask);
objects.sort();
x ++;
'b' =>
objects.add(BONE, x, y, bone_img, bone_mask);
objects.sort();
bones ++;
x ++;
* =>
sys->fprint(sys->fildes(2), "Api's Adventure level parser: impossible character: '%c' for level in line: %d\n", c, line);
exit;
}
if (x > max.x)
max.x = x;
if (x >= COLUMNS || y >= ROWS)
{
if((c = buf.getc()) != '\n')
{
sys->fprint(sys->fildes(2), "Api's Adventure level parser: error on line: %d. Level map must be: %dx%d\n", line, COLUMNS, ROWS);
exit;
}
buf.ungetc();
}
}
}
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;
}
# Keskittää peli-ikkunan.
center(t: ref Tk->Toplevel)
{
org: Point;
ir := tk->rect(t, ".", Tk->Border|Tk->Required);
org.x = t.screenr.dx() / 2 - COLUMNS * TILEX / 2;
org.y = (t.screenr.dy() / 2) - (ir.dy() + (ROWS * TILEY) / 2);
if (org.y < 0)
org.y = 0;
tk->cmd(t, ". configure -x " + string org.x + " -y " + string org.y);
}Aihe on jo aika vanha, joten et voi enää vastata siihen.