ADDED game.d Index: game.d ================================================================== --- game.d +++ game.d @@ -0,0 +1,345 @@ +import util; +import output; + +//////////////////////////////////////////////////////////////////////////////// + +class Pos +{ + public immutable int y, x; + mixin DeriveCreate; + mixin DeriveCompare; + mixin DeriveShow; + +@property: + Pos wait() { return this; } + Pos up() { return new Pos(y+1, x); } + Pos down() { return new Pos(y-1, x); } + Pos left() { return new Pos(y, x-1); } + Pos right() { return new Pos(y, x+1); } + alias wait W,w; + alias up U,u; + alias down D,d; + alias left L,l; + alias right R,r; +} + +unittest +{ + assert( (new Pos(2,1)).U == new Pos(3,1) ); + assert( (new Pos(0,1)).D == new Pos(-1,1) ); + assert( (new Pos(2,1)).L == new Pos(2,0) ); + assert( (new Pos(2,1)).R == new Pos(2,2) ); + int[Pos] aa; + aa[new Pos(1,2)] = 1; + aa[new Pos(1,2)] = 2; + aa[new Pos(2,1)] = 3; + assert( aa.length==2 ); + assert( aa[new Pos(1,2)]==2 ); +} + +//////////////////////////////////////////////////////////////////////////////// + +class Water +{ + public immutable int base, pace; + mixin DeriveCreate; + mixin DeriveCompare; + mixin DeriveShow; + + static load(string[string] params) + { + return new Water( + params.get("Water", "0").to!int(), + params.get("Flooding", "0").to!int() + ); + } + + int level(int number_of_update) + { + return pace ? base+(number_of_update/pace) : base; + } + + int until_rise(int number_of_update) + { + return pace ? pace-number_of_update%pace : int.max; + } +} + +unittest +{ + Water w = new Water(1, 3); + assert( 1 == w.level(0) ); + assert( 1 == w.level(1) ); + assert( 1 == w.level(2) ); + assert( 2 == w.level(3) ); + assert( 2 == w.level(4) ); + assert( 2 == w.level(5) ); + assert( 3 == w.level(6) ); + + w = new Water(1, 0); + assert( 1 == w.level(0) ); + assert( 1 == w.level(1) ); + assert( 1 == w.level(2) ); + assert( 1 == w.level(3) ); + assert( 1 == w.level(4) ); + assert( 1 == w.level(5) ); +} + +//////////////////////////////////////////////////////////////////////////////// + +class Map +{ + mixin DeriveShow; + + static Map load(string[] raw_data, string[string] params) + { + // TODO: choose optimal representation. + return new Map(raw_data, params); + } + + private { + char[][] data; + Pos robot; + Pos lift; + int waterproof; + } + + this(string[] raw_data, string[string] params) + { + int width = 0; + foreach(r; raw_data) + width = max(width, r.length); + foreach(r; raw_data) { + this.data ~= r.dup; + this.data[$-1].length = width; + this.data[$-1][r.length..$] = ' '; + } + + for(int y=1; y<=H; ++y) + for(int x=1; x<=W; ++x) { + if(this[y,x] == 'R') + this.robot = new Pos(y,x); + if(this[y,x] == 'L') + this.lift = new Pos(y,x); + } + + this.waterproof = params.get("Waterproof", "5").to!int(); + } + + const @property { + int H() { return data.length; } + int W() { return data[0].length; } + } + + char opIndex(int y, int x) + { + // Adjust coordinate to the spec. bottom-left is (1,1). + --y, --x; + if(y<0||H<=y||x<0||W<=x) + return '#'; + return data[H-1-y][x]; + } + + char opIndex(Pos p) + { + return this[p.y, p.x]; + } + + void opIndexAssign(char c, int y, int x) + { + // Adjust coordinate to the spec. bottom-left is (1,1). + --y, --x; + if(y<0||H<=y||x<0||W<=x) + return; + data[H-1-y][x] = c; + } + + void opIndexAssign(char c, Pos p) + { + this[p.y, p.x] = c; + } + + bool cleared() + { + for(int y=1; y<=H; ++y) + for(int x=1; x<=W; ++x) + if(this[y,x] == 'L' || this[y,x] == 'O') + return false; + return true; + } + + Tuple!(int,bool) command(char c) + { + if(c=='R') return move( 0, +1); + if(c=='L') return move( 0, -1); + if(c=='U') return move(+1, 0); + if(c=='D') return move(-1, 0); + if(c=='W') return move( 0, 0); + assert(false); + } + + Tuple!(int, bool) move(int dy, int dx) + { + int y = robot.y; + int x = robot.x; + assert( this[robot] == 'R' ); + int lambda = 0; + bool dead = false; + if( '\\' == this[y+dy,x+dx] ) + lambda++; + if( " \\.O".count(this[y+dy,x+dx])==1 ) { + this[y,x]=' '; + this[y+dy,x+dx]='R'; + robot = new Pos(y+dy,x+dx); + } else if(dy==0 && '*'==this[y+dy,x+dx] && ' '==this[y+dy*2,x+dx*2]) { + this[y,x]=' '; + this[y+dy,x+dx]='R'; + this[y+dy*2,x+dx*2]='*'; + robot = new Pos(y+dy,x+dx); + } + if( update() ) + dead = true; + return tuple(lambda,dead); + } + + bool update() + { + bool dead = false; + + char[][] next; + foreach(y,s; data) + next ~= s.dup; + + ref char access(Pos p) { return next[H-p.y][p.x-1]; } + + bool lambda = false; + for(int y=1; y<=H; ++y) + for(int x=1; x<=W; ++x) + lambda |= (this[y,x] == '\\'); + + for(int y=1; y<=H; ++y) + for(int x=1; x<=W; ++x) { + Pos p = new Pos(y,x); + if(this[p]=='*') { + if(this[p.D]==' ') { + access(p) =' '; + access(p.D)='*'; + if(robot == p.D.D) + dead=true; + } + else if((this[p.D]=='*' || this[p.D]=='\\') && this[p.R]==' ' && this[p.R.D]==' ') { + access(p)=' '; + access(p.R.D)='*'; + if(robot == p.R.D.D) + dead=true; + } + else if(this[p.D]=='*' && this[p.L]==' ' && this[p.L.D]==' ') { + access(p)=' '; + access(p.L.D)='*'; + if(robot == p.L.D.D) + dead=true; + } + } + else if(this[p]=='L') { + if(!lambda) + access(p) = 'O'; + } + } + data = next; + return dead; + } +} + +//////////////////////////////////////////////////////////////////////////////// + +class Game +{ + mixin DeriveShow; + + static Game load(File input) + { + string[] raw_data; + string[string] params; + + // Raw map data; read until empty line. + for(string line; !(line=input.readln().chomp()).empty; ) + raw_data ~= line; + + // Additional commands; read until EOF. + for(string line; !(line=input.readln()).empty; ) { + string[] ss = line.split(); + if( ss.length == 2 ) + params[ss[0]] = ss[1]; + } + + return load(raw_data, params); + } + + static Game load(string[] raw_data, string[string] params) + { + return new Game(raw_data, params); + } + + this(string[] raw_data, string[string] params) + { + this.map = Map.load(raw_data, params); + this.water = Water.load(params); + this.output = new NilOutput; + } + + void set_output(Output o) { this.output = (o is null ? new NilOutput : o); } + + void command(char c) + { + if(dead || cleared) + return; + this.output.command(c); + + if(c == 'A') + { + exit_bonus = 1; + return; + } + + // TODO: clarify the event order + Tuple!(int,bool) ld = map.command(c); + if( map.cleared() ) { + exit_bonus = 2; + } + else { + lambda += ld[0]; + if( ld[1] ) { + dead = true; + } + } + if( map.robot.y <= water_level ) + ++under_warter; + else + under_warter = 0; + if( under_warter > map.waterproof ) + dead = true; + turn += 1; + } + + Map map; + Water water; + Output output; + + int turn = 0; + bool dead = false; + int lambda = 0; + int exit_bonus = 0; + int under_warter = 0; + @property { + int score() { return lambda*25*(1+exit_bonus) - turn; } + int water_level() { return water.level(turn); } + int water_until_rise() { return water.until_rise(turn); } + bool cleared() { return exit_bonus>0; } + int hp() { return map.waterproof - under_warter; } + } +} + +unittest +{ + Game.load(["###","...","#RL"], ["xxx":"yyy"]); +} ADDED gui.d Index: gui.d ================================================================== --- gui.d +++ gui.d @@ -0,0 +1,118 @@ +import dfl.all; +import util; +import game; +import output; + +class GUI : Form +{ + private { + Game g; + int cell; + int turn = 0; + + Font font; + Color[char] colors; + string[char] render; + } + + this(Game g) + { + noMessageFilter(); + this.setStyle(ControlStyles.OPAQUE, true); + this.g = g; + + this.paint ~= &my_paint; + this.keyDown ~= &my_keydown; + + const MAX_SIZE = 640; + this.formBorderStyle = FormBorderStyle.FIXED_DIALOG; + this.maximizeBox = false; + this.minimizeBox = false; + this.cell = MAX_SIZE / max(g.map.W, g.map.H); + this.clientSize = Size(g.map.W*cell, g.map.H*cell); + set_text(); + + // Resources + this.font = new Font("MS Gothic", cell-2, GraphicsUnit.PIXEL); + this.backColor = Color(255,255,255); + this.colors['#'] = + this.colors['.'] = Color(255,191,127); + this.colors['*'] = Color(255,127,127); + this.colors['R'] = Color(128,128,0); + this.colors['D'] = Color(255,0,0); // Dead + this.colors['\\'] = + this.colors['L'] = + this.colors['O'] = Color(127,255,127); + this.colors['W'] = Color(204,229,255); // water + + this.render['#'] = "■"; + this.render['*'] = "✹"; + this.render['.'] = "♒"; + this.render['\\'] = "λ"; + this.render['R'] = "☃"; + this.render['D'] = "☠"; + this.render['L'] = "☒"; + this.render['O'] = "☐"; + } + +private: + void my_paint(Control, PaintEventArgs ev) + { + const scrH = this.clientSize.height; + const scrW = this.clientSize.width; + Graphics gr = new MemoryGraphics(scrW, scrH, ev.graphics); + scope(exit) { + gr.copyTo(ev.graphics, Rect(0,0,scrW,scrH)); + gr.dispose(); + } + + // Fill bg. + gr.fillRectangle(this.backColor, Rect(0,0,scrW,scrH)); + + // Fill water. + int w = g.water_level(); + gr.fillRectangle(this.colors['W'], Rect(0, scrH-cell*w-1, scrW, cell*w+1)); + + // Paint map. + for(int y=1; y<=g.map.H; ++y) + for(int x=1; x<=g.map.W; ++x) { + Rect r = Rect(cell*(x-1), scrH-cell*y, cell, cell); + char c = g.map[y,x]; + if( c != ' ' ) { + if( c == 'R' && g.dead ) + c = 'D'; + gr.drawText(this.render[c], font, this.colors[c], r); + } + } + } + + void my_keydown(Control c, KeyEventArgs ev) + { + switch(ev.keyCode) + { + case Keys.DOWN: g.command('D'); break; + case Keys.UP: g.command('U'); break; + case Keys.LEFT: g.command('L'); break; + case Keys.RIGHT: g.command('R'); break; + case Keys.W: g.command('W'); break; + case Keys.A: g.command('A'); break; + default: break; + } + if(g.cleared) + Application.exit(); + invalidate(); + set_text(); + } + + void set_text() { + this.text = .text("Score: ", g.score, " Air: ", g.hp, " Tide: ", g.water_until_rise); + } +} + +void main(string[] args) +{ + auto g = Game.load(File(args[1])); + g.set_output(new StdOutput); + auto myForm = new GUI(g); + Application.run(myForm); +} ADDED output.d Index: output.d ================================================================== --- output.d +++ output.d @@ -0,0 +1,35 @@ +import util; +import game; +import core.stdc.signal; +import std.c.stdlib; + +abstract class Output +{ + void command(char c); +} + +class NilOutput : Output +{ + override void command(char c) {} +} + +class StdOutput : Output +{ + // Handle SIGINT: force abort and exit. + static this() + { + signal(SIGINT, &sigint); + } + extern(C) static void sigint(int) { + write("A"); + stdout.flush(); + exit(0); + } + + override void command(char c) + { + // TODO: optimize redundancy. + write(c); + stdout.flush(); + } +}