Artifact Content
Not logged in

Artifact 18f8bb38825744d043f2caf110c8675dbae76afd


import util;

////////////////////////////////////////////////////////////////////////////////

bool is_spacy(char c)
{
	return c==' ' || c=='.' || c=='R' || c=='!' || c=='\\' || c=='O';
}

bool is_rocky(char c)
{
	return c=='*' || c=='@';
}

bool is_true_space(char c)
{
	return c==' ';
}

bool is_trampoline_source(char c)
{
	return 'A'<=c && c<='I';
}

bool is_rocklambda(char c)
{
	return is_rocky(c) || c=='\\';
}

////////////////////////////////////////////////////////////////////////////////

class Pos
{
	public immutable int y, x;
	mixin DeriveCreate;
	mixin DeriveCompare;
	mixin DeriveShow;
	Pos clone() const { return cast(Pos) this; }

const @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
{
	mixin DeriveShow;

private:
	immutable int base, pace;
	mixin DeriveCreate;
	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 turn) const
	{
		return pace ? base+(turn/pace) : base;
	}

	int until_rise(int turn) const
	{
		return pace ? pace-turn%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
{
	mixin DeriveShow;

private:
	immutable int pace;
	mixin DeriveCreate;
	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 Trampoline
{
	mixin DeriveShow;

private:
	immutable char[]   target_of_;
	immutable char[][] source_of_;
	immutable Pos[]    position_of_;
	immutable char[]   source_list_;
	immutable char[]   target_list_;
	Trampoline clone() const { return cast(Trampoline) this; }

	this(Map m, char[char] tramparam)
	{
		auto ta = new char['I'+1];
		auto sr = new char[]['9'+1];
		auto po = new Pos[max('I','9')+1];
		char[] sl, tl;
		foreach(fr,to; tramparam) {
			ta[fr]  = to;
			sr[to] ~= fr;
		}
		for(int y=1; y<=m.H; ++y)
		for(int x=1; x<=m.W; ++x) {
			char c = m[y,x];
			if('A'<=c && c<='I') {
				sl ~= c;
				po[c] = new Pos(y,x);
			}
			if('1'<=c && c<='9') {
				tl ~= c;
				po[c] = new Pos(y,x);
			}
		}
		target_of_ = cast(immutable) ta;
		source_of_ = cast(immutable) sr;
		position_of_ = cast(immutable) po;
		source_list_ = cast(immutable) sl;
		target_list_ = cast(immutable) tl;
	}

public @property const:
	const(char[]) source_list() { return source_list_; }
	const(char[]) target_list() { return target_list_; }
	const(char[]) source_of(char c) { return source_of_[c]; }
	char target_of(char c) { return target_of_[c]; }
	Pos[] source_pos(char c) {
		Pos[] ps;
		foreach(s; source_of(c))
			ps ~= position_of_[s].clone();
		return ps;
	}
	Pos target_pos(char c) { return position_of_[target_of_[c]].clone(); }
}

////////////////////////////////////////////////////////////////////////////////

class Map
{
	mixin DeriveShow;

	private char[][] data;
	Pos robot;
	Pos lift;
	private int waterproof;
	private int collected_razor;
	int collected_lambda;
	int total_lambda;
	private bool cleared;
	private Pos[] may_update;

	private Map clone() const { return new Map(this); }
	private 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.collected_razor  = m.collected_razor;
		this.collected_lambda = m.collected_lambda;
		this.total_lambda     = m.total_lambda;
		this.cleared    = m.cleared;
		this.may_update = (cast(Map)m).may_update.dup;
	}

	const {
		@property {
			int H() { return data.length; }
			int W() { return data[0].length; }
			int num_razor() { return collected_razor; }

			Pos[] razors()  { return objects('!'); }
			Pos[] lambdas() { return objects('\\'); }
		}

		Pos[] objects(char c) {
			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;
		}

		char opIndex(int y, int x) {
			--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];
		}
	}

private:
	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);
			if(this[y,x] == '\\' || this[y,x] == '@')
				total_lambda++;
			if(is_rocky(this[y,x]))
				may_update ~= new Pos(y,x);
		}

		this.waterproof      = params.get("Waterproof", "5").to!int();
		this.collected_razor = params.get("Razors", "0").to!int();
	}

	void opIndexAssign(char c, int y, int x)
	{
		--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;
	}

	bool command(char c, int turn, bool hige_day, in Trampoline tr)
	{
		switch(c)
		{
		case 'R': return move( 0, +1, hige_day, tr);
		case 'L': return move( 0, -1, hige_day, tr);
		case 'U': return move(+1,  0, hige_day, tr);
		case 'D': return move(-1,  0, hige_day, tr);
		case 'W': return move( 0,  0, hige_day, tr);
		case 'S': return use_razor(hige_day);
		default:  assert(false);
		}
	}

	bool use_razor(bool hige_day)
	{
		if(collected_razor > 0)
		{
			collected_razor--;
			for(int dy=-1; dy<=+1; ++dy)
			for(int dx=-1; dx<=+1; ++dx)
				if(this[robot.y+dy,robot.x+dx] == 'W')
					emptify(new Pos(robot.y+dy,robot.x+dx));
		}
		return update(hige_day);
	}

	// Register a position that may become empty in the last turn.
	void emptify(Pos p)
	{
		this[p] = ' ';
		for(int dy=0;  dy<=+1; ++dy)
		for(int dx=-1; dx<=+1; ++dx)
			may_update ~= new Pos(p.y+dy, p.x+dx);
	}

	bool move(int dy, int dx, bool hige_day, in Trampoline tr)
	{
		Pos next = new Pos(robot.y+dy, robot.x+dx);
		int y=robot.y, x=robot.x;

		if( '\\' == this[next] ) collected_lambda++;
		if(  '!' == this[next] ) collected_razor++;
		if( 'O'  == this[next] ) cleared = true;

		if( is_spacy(this[next]) )
		{
			emptify(robot);
			robot = next;
			this[next] = 'R';
		}
		else if(dy==0 && is_rocky(this[next]) && ' '==this[y+dy*2,x+dx*2])
		{
			char rock  = this[next];
			emptify(robot);
			robot = next;
			this[next]          = 'R';
			this[y+dy*2,x+dx*2] = rock;
			may_update ~= new Pos(y+dy*2,x+dx*2);
		}
		else if(is_trampoline_source(this[next]))
		{
			emptify(robot);
			Pos tp = tr.target_pos(this[next]);
			foreach(p; tr.source_pos(this[tp]))
				emptify(p);
			this[tp] = 'R';
			robot    = tp;
		}
		return update(hige_day);
	}

	bool update(bool hige_day)
	{
		// Write after all the updates are processed.
		Tuple!(int,int,char)[] write_buffer;
		void write(int y, int x, char c) { write_buffer ~= tuple(y,x,c); }
		void writep(Pos p, char c) { write_buffer ~= tuple(0+p.y,0+p.x,c); }
		scope(exit) {
			may_update.length = 0;
			foreach(wr; write_buffer) {
				this[wr[0],wr[1]] = wr[2];
				if(is_rocky(wr[2]))
					may_update ~= new Pos(wr[0],wr[1]);
				if(wr[2]==' ')
					emptify(new Pos(wr[0], wr[1]));
			}
		}

		if(collected_lambda == total_lambda)
			if(this[lift]=='L')
				this[lift] = 'O';

		bool dead = false;
		if( hige_day ) {
			for(int y=1; y<=H; ++y)
			for(int x=1; x<=W; ++x)
				if(this[y,x]=='W')
					may_update ~= new Pos(y,x);
		}

		sort(may_update);
		foreach(p; may_update) {
			int y = p.y, x = p.x;
			char rock = this[p];
			if(is_rocky(this[p])) {
				if(this[p.D]==' ') {
					writep(p, ' ');
					writep(p.D, (rock=='@'&&this[p.D.D]!=' ' ? '\\' : rock));
					if(robot == p.D.D)
						dead=true;
				}
				else if((is_rocky(this[p.D]) || this[p.D]=='\\') && this[p.R]==' ' && this[p.R.D]==' ') {
					writep(p, ' ');
					writep(p.R.D,(rock=='@'&&this[p.R.D.D]!=' ' ? '\\' : rock));
					if(robot == p.R.D.D)
						dead=true;
				}
				else if(is_rocky(this[p.D]) && this[p.L]==' ' && this[p.L.D]==' ') {
					writep(p, ' ');
					writep(p.L.D, (rock=='@'&&this[p.L.D.D]!=' ' ? '\\' : rock));
					if(robot == p.L.D.D)
						dead=true;
				}
			}
			else if(this[p]=='W') {
				if(hige_day) {
					for(int dy=-1; dy<=+1; ++dy)
					for(int dx=-1; dx<=+1; ++dx)
						if(this[p.y+dy,p.x+dx] == ' ') {
							write(p.y+dy,p.x+dx,'W');
							if(robot.y==p.y+dy-1 && robot.x==p.x+dx)
								dead = false; // guarded by hige!
						}
				}
			}
		}

		return dead;
	}
}

////////////////////////////////////////////////////////////////////////////////

class Game
{
	mixin DeriveShow;

	this(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];
		}

		this.map   = new Map(raw_data, params, trampo);
		this.water = Water.load(params);
		this.hige  = Hige.load(params);
		this.tr    = new Trampoline(this.map, trampo);
	}

	Game clone() const { return new Game(this); }
	this(in Game g) {
		map   = g.map.clone();
		water = g.water.clone();
		hige  = g.hige.clone();
		tr    = g.tr.clone();
		turn  = g.turn;
		dead  = g.dead;
		under_water = g.under_water;
	}

	void command(char c)
	{
		assert(c != 'A');
		if(dead || cleared)
			return;

		// TODO: clarify the event order
		bool dead_now = map.command(c, turn, hige.is_growing_turn(turn), tr);
		if( dead_now )
			dead = true;
		if(!map.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;
	Hige hige;
	Trampoline tr;
	int  turn = 0;
	bool dead = false;
	int  under_water = 0;
	// TODO: when adding members, take care of clone().
	// TODO: fix this poor design.

@property const:
	long score()           { return map.collected_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 hige.until_rise(turn); }
	int hp()               { return map.waterproof - under_water; }
	bool cleared()         { return map.cleared; }
}