import util;
////////////////////////////////////////////////////////////////////////////////
class Pos
{
public immutable int y, x;
mixin DeriveCreate;
mixin DeriveCompare;
mixin DeriveShow;
Pos clone() const { return cast(Pos) this; }
@property:
Pos wait() { return this.clone(); }
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;
Water clone() const { return cast(Water)this; }
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) const
{
return pace ? base+(number_of_update/pace) : base;
}
int until_rise(int number_of_update) const
{
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 Hige
{
public immutable int pace;
mixin DeriveCreate;
mixin DeriveCompare;
mixin DeriveShow;
Hige clone() const { return cast(Hige)this; }
static load(string[string] params)
{
return new Hige(params.get("Growth", "25").to!int());
}
bool is_growing_turn(int turn) const
{
return pace ? turn%pace == pace-1 : false;
}
int until_rise(int turn) const
{
return pace ? pace-turn%pace : int.max;
}
}
////////////////////////////////////////////////////////////////////////////////
class Map
{
mixin DeriveShow;
static Map load(string[] raw_data, string[string] params, char[char] trampo)
{
// TODO: choose optimal representation.
return new Map(raw_data, params, trampo);
}
char[][] data;
Pos robot;
Pos lift;
int waterproof;
Pos[char] tr_target;
Pos[][char] tr_source;
const(Hige) hige;
int razor;
Map clone() const { return new Map(this); }
this(in Map m) {
foreach(s; m.data)
this.data ~= s.dup;
this.robot = m.robot.clone();
this.lift = m.lift.clone();
this.waterproof = m.waterproof;
this.tr_target = cast(Pos[char])m.tr_target;
this.tr_source = cast(Pos[][char])m.tr_source;
this.hige = m.hige.clone();
this.razor = m.razor;
}
this(string[] raw_data, string[string] params, char[char] trampo)
{
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[y,x] == 'O')
this.lift = new Pos(y,x);
}
Pos[char] tr_pos;
for(int y=1; y<=H; ++y)
for(int x=1; x<=W; ++x) {
char c = this[y,x];
if('1'<=c && c<='9' || 'A'<=c&&c<='I')
tr_pos[c] = new Pos(y,x);
}
this.waterproof = params.get("Waterproof", "5").to!int();
foreach(fr,to; trampo) {
tr_target[fr] = tr_pos[to];
if(to !in tr_source) tr_source[to] = [];
tr_source[to] ~= tr_pos[fr];
}
this.hige = Hige.load(params);
this.razor = params.get("Razors", "0").to!int();
}
const @property {
int H() { return data.length; }
int W() { return data[0].length; }
}
const {
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(in 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, in Pos p)
{
this[p.y, p.x] = c;
}
Pos[] objects(char c) const {
Pos[] ans;
for(int y=1; y<=H; ++y)
for(int x=1; x<=W; ++x)
if(this[y,x] == c)
ans ~= new Pos(y,x);
return ans;
}
Pos[] razors() const { return objects('!'); }
Pos[] lambdas() const { return objects('\\'); }
bool cleared() const
{
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, int turn)
{
assert( this[robot] == 'R' );
if(c=='R') return move( 0, +1, turn);
if(c=='L') return move( 0, -1, turn);
if(c=='U') return move(+1, 0, turn);
if(c=='D') return move(-1, 0, turn);
if(c=='W') return move( 0, 0, turn);
if(c=='S') return use_razor(turn);
assert(false);
}
Tuple!(int, bool) use_razor(int turn)
{
if(razor) {
razor--;
for(int dy=-1; dy<=+1; ++dy)
for(int dx=-1; dx<=+1; ++dx)
if(this[robot.y+dy,robot.x+dx] == 'W')
this[robot.y+dy,robot.x+dx] = ' ';
}
bool dead = update(turn);
return tuple(0,dead);
}
Tuple!(int, bool) move(int dy, int dx, int turn)
{
int y = robot.y;
int x = robot.x;
int lambda = 0;
if( '\\' == this[y+dy,x+dx] )
lambda++;
if( '!' == this[y+dy,x+dx] )
razor++;
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);
} else if('A'<=this[y+dy,x+dx] && this[y+dy,x+dx]<='I') {
this[y,x]=' ';
Pos tp = tr_target[this[y+dy,x+dx]];
foreach(p; tr_source[this[tp]])
this[p] = ' ';
this[tp] = 'R';
robot = tp;
}
bool dead = update(turn);
return tuple(lambda,dead);
}
bool update(int turn)
{
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';
}
else if(this[p]=='W') {
if( hige.is_growing_turn(turn) )
for(int dy=-1; dy<=+1; ++dy)
for(int dx=-1; dx<=+1; ++dx)
if(this[p.y+dy,p.x+dx] == ' ')
access(new Pos(p.y+dy,p.x+dx)) = 'W';
}
}
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.
char[char] trampo;
for(string line; !(line=input.readln()).empty; ) {
string[] ss = line.split();
if( ss.length == 2 )
params[ss[0]] = ss[1];
if( ss.length == 4 && ss[0]=="Trampoline" && ss[2]=="targets" )
trampo[ss[1][0]] = ss[3][0];
}
return load(raw_data, params, trampo);
}
static Game load(string[] raw_data, string[string] params, char[char] trampo = null)
{
return new Game(raw_data, params, trampo);
}
this(string[] raw_data, string[string] params, char[char] trampo)
{
this.map = Map.load(raw_data, params, trampo);
this.water = Water.load(params);
}
Game clone() const { return new Game(this); }
this(in Game g) {
map = g.map.clone();
water = g.water.clone();
turn = g.turn;
dead = g.dead;
lambda = g.lambda;
cleared = g.cleared;
under_water = g.under_water;
}
void command(char c)
{
assert(c != 'A');
if(dead || cleared)
return;
// TODO: clarify the event order
Tuple!(int,bool) ld = map.command(c, turn);
if( map.cleared() ) {
cleared = true;
}
else {
lambda += ld[0];
if( ld[1] )
dead = true;
}
if(!cleared) {
if( map.robot.y <= water_level )
++under_water;
else
under_water = 0;
if( under_water > map.waterproof )
dead = true;
}
turn += 1;
}
Map map;
Water water;
int turn = 0;
bool dead = false;
int lambda = 0;
int under_water = 0;
bool cleared = false;
// TODO: when adding members, take care of clone().
// TODO: fix this poor design.
@property const {
long score() { return lambda*(dead ? 25L : cleared ? 75L : 50L) - turn; }
int water_level() { return water.level(turn); }
int water_until_rise() { return water.until_rise(turn); }
int hige_until_rise() { return map.hige.until_rise(turn); }
int hp() { return map.waterproof - under_water; }
}
}
*/
////////////////////////////////////////////////////////////////////////////////
class Game
{
public:
this(File input)
{
// Read map data
string[] map_data_lines;
for(string line; !(line=input.readln().chomp()).empty; )
map_data_lines ~= line;
// H*W
H_ = map_data_lines.length;
W_ = 0;
foreach(mdl; map_data_lines)
W_ = max(W_, mdl.length);
// Copy to modifiable buffer and adjust coordinates.
raw_data_ = new char[][H_+1];
foreach(i,mdl; map_data_lines) {
char[] buf = new char[mdl.length+1];
buf[0] = '#';
buf[1..$] = mdl[];
raw_data_[H_-i] = buf;
}
// Detect objects
for(int y=1; y<=H_; ++y)
for(int x=1; x<raw_data_[y].length; ++x)
{
char c = raw_data_[y][x];
switch(c)
{
case '#':
case '.':
case ' ':
break;
case 'L':
case 'O':
lift_pos_ = new Pos(y,x);
break;
case 'A': .. case 'I':
case '1': .. case '9':
trampoline_pos_[c] = new Pos(y,x);
break;
case '!':
razor_pos_ ~= new Pos(y,x);
break;
case '\\':
lambda_pos_ ~= new Pos(y,x);
break;
// Moving objects are erased from raw_data_
case 'R':
robot_pos_ = new Pos(y,x);
raw_data_[y][x] = ' ';
break;
case '*':
case 'W':
dynamic_objects_[new Pos(y,x)] = c;
raw_data_[y][x] = ' ';
if(c=='*')
may_update_[new Pos(y,x)] = true;
break;
default:
assert(false);
}
}
// Read other parameters
for(string line; !(line=input.readln()).empty; )
{
string[] ss = line.split();
if( ss.length == 2 )
switch(ss[0])
{
case "Water": water_base_ = ss[1].to!int(); break;
case "Flooding": water_pace_ = ss[1].to!int(); break;
case "Waterproof": max_air_ = ss[1].to!int(); break;
case "Growth": hige_pace_ = ss[1].to!int(); break;
case "Razors": num_razor_ = ss[1].to!int(); break;
default: assert(false);
}
if( ss.length == 4 && ss[0]=="Trampoline" && ss[2]=="targets" )
{
char fr=ss[1][0], to=ss[3][0];
trampoline_[fr] = to;
if(to !in trampoline_rev_) trampoline_rev_[to] = [];
trampoline_rev_[to] ~= fr;
}
}
air_left_ = max_air_;
}
@property const {
int H() { return H_; }
int W() { return W_; }
char trampoline(char c) { return (c in trampoline_ ? trampoline_[c] : 0); }
int water_level() {
return water_pace_ ? water_base_ + turn_/water_pace_ : water_base_;
}
int water_until_rise() {
return water_pace_ ? water_pace_ - turn_%water_pace_ : int.max;
}
int hige_until_rise() {
return hige_pace_ ? hige_pace_ - turn_%hige_pace_ : int.max;
}
bool is_hige_turn() {
return hige_pace_ ? turn_%hige_pace_ == hige_pace_-1 : false;
}
int hp() { return air_left_; }
int num_razor() { return num_razor_; }
bool cleared() { return cleared_; }
bool dead() { return dead_; }
long score() { return num_lambda_*(dead_ ? 25L : cleared_ ? 75L : 50L) - turn_; }
}
const {
char opIndex(in Pos p) { return opIndex(p.y, p.x); }
char opIndex(int y, int x) { return map_get(y, x); }
}
public:
void command(char c)
{
if(dead || cleared)
return;
if(c == 'U') command_move(+1, 0);
if(c == 'D') command_move(-1, 0);
if(c == 'L') command_move(0, -1);
if(c == 'R') command_move(0, +1);
if(c == 'S') use_razor();
if(c == 'W') {}
if(cleared)
return;
map_update();
water_update();
turn_ ++;
}
void command_move(int dy, int dx)
{
int y = robot_pos_.y, x = robot_pos_.x;
char c = this[y+dy, x+dx];
Pos p = new Pos(y+dy, x+dx);
switch(c){
case 'O':
cleared_ = true;
move_robot_to(p);
break;
case '\\':
take_lambda_at(p);
move_robot_to(p);
break;
case '!':
take_razor_at(p);
move_robot_to(p);
break;
case 'A': .. case 'I':
enter_trampoline_at(p, c);
break;
case ' ':
case '.':
move_robot_to(p);
break;
case '*':
if(dy!=0 || this[y,x+dx*2]!=' ')
break;
move_robot_to(p);
push_rock(p, new Pos(y,x+dx*2));
break;
default:
break;
}
}
void use_razor()
{
if(num_razor_ == 0)
return;
num_razor_ --;
for(int dy=-1; dy<=+1; ++dy)
for(int dx=-1; dx<=+1; ++dx) if(dy||dx)
{
Pos p = new Pos(robot_pos_.y+dy, robot_pos_.x+dx);
if(auto it = p in dynamic_objects_)
if(*it == 'W') {
something_gone(p);
dynamic_objects_.remove(p);
}
}
}
void take_lambda_at(Pos p)
{
map_set_empty(p);
num_lambda_ ++;
lambda_pos_ = lambda_pos_.erase(p);
}
void take_razor_at(Pos p)
{
map_set_empty(p);
num_razor_ ++;
razor_pos_ = razor_pos_.erase(p);
}
void enter_trampoline_at(Pos p, char c)
{
char d = trampoline(c);
foreach(cc; trampoline_rev_[d]) {
Pos pp = trampoline_pos_[cc];
something_gone(pp);
map_set_empty(pp);
}
move_robot_to(trampoline_pos_[d]);
}
void move_robot_to(Pos p)
{
something_gone(robot_pos_);
map_set_empty(p.y, p.x);
robot_pos_ = p;
}
void push_rock(Pos fr, Pos to)
{
dynamic_objects_.remove(fr);
dynamic_objects_[to] = '*';
may_update_[to] = true;
}
void something_gone(Pos p)
{
for(int dy=0; dy<=+1; ++dy)
for(int dx=-1; dx<=+1; ++dx) if(dy||dx)
may_update_[new Pos(p.y+dy,p.x+dx)] = true;
}
void map_update()
{
Pos[] may_update_list;
foreach(p,_; may_update_)
if(this[p] == '*')
may_update_list ~= p;
may_update_ = null;
if( is_hige_turn() )
foreach(p,c; dynamic_objects_)
if(c == 'W')
may_update_list ~= p;
sort(may_update_list);
char[Pos] to_be_written;
foreach(p; may_update_list)
if(is_hige_turn() && this[p]=='W')
{
for(int dy=-1; dy<=+1; ++dy)
for(int dx=-1; dx<=+1; ++dx) {
Pos q = new Pos(p.y+dy,p.x+dx);
if( this[q] == ' ' )
to_be_written[q] = 'W';
}
}
else
{
int y = p.y;
int x = p.x;
char below = this[y-1,x];
// *
// _
if(below==' ') {
Pos q = new Pos(y-1,x);
to_be_written[p] = ' ';
to_be_written[q] = '*';
may_update_[q] = true;
}
// *_ *_
// *_ or \_
else if((below=='*'||below=='\\')&&this[y-1,x+1]==' '&&this[y,x+1]==' ') {
Pos q = new Pos(y-1,x+1);
to_be_written[p] = ' ';
to_be_written[q] = '*';
may_update_[q] = true;
}
// _*
// _*
else if(below=='*'&&this[y-1,x-1]==' '&&this[y,x-1]==' ') {
Pos q = new Pos(y-1,x-1);
to_be_written[p] = ' ';
to_be_written[q] = '*';
may_update_[q] = true;
}
}
foreach(p,c; to_be_written) {
dynamic_objects_[p] = c;
if(c=='*' && p.y==robot_pos_.y+1 && p.x==robot_pos_.x)
dead_ = true;
}
if(lambda_pos_.empty)
raw_data_[lift_pos_.y][lift_pos_.x] = 'O';
}
void water_update()
{
if( robot_pos_.y <= water_level() )
air_left_ --;
else
air_left_ = max_air_;
if( air_left_ < 0 )
dead_ = true;
}
private:
char map_get(int y, int x) const
{
if( y<1 || H<y || x<1 || W<x ) return '#';
Pos p = new Pos(y,x);
if(p == robot_pos_)
return 'R';
if(auto it = (p in dynamic_objects_))
return *it;
if( x<0 || raw_data_[y].length<=x ) return ' ';
return raw_data_[y][x];
}
void map_set_empty(in Pos p)
{
return map_set_empty(p.y, p.x);
}
void map_set_empty(int y, int x)
{
if( y<1 || H<y || x<1 || W<x ) return;
if( x<0 || raw_data_[y].length<=x ) return;
raw_data_[y][x] = ' ';
}
private:
int H_;
int W_;
char[][] raw_data_;
Pos[char] trampoline_pos_;
Pos[] razor_pos_;
Pos[] lambda_pos_;
Pos lift_pos_;
Pos robot_pos_;
char[Pos] dynamic_objects_;
char[char] trampoline_;
char[][char] trampoline_rev_;
int water_base_ = 0;
int water_pace_ = 0;
int max_air_ = 10;
int hige_pace_ = 25;
int num_razor_ = 0;
int num_lambda_ = 0;
int turn_ = 0;
int air_left_ = 0;
bool cleared_ = false;
bool dead_ = false;
bool[Pos] may_update_;
}