1 import util;
2
3 ////////////////////////////////////////////////////////////////////////////////
4
5 bool is_spacy(char c)
6 {
7 return c==' ' || c=='.' || c=='R' || c=='!' || c=='\\' || c=='O';
8 }
9
10 bool is_rocky(char c)
11 {
12 return c=='*' || c=='@';
13 }
14
15 bool is_true_space(char c)
16 {
17 return c==' ';
18 }
19
20 bool is_trampoline_source(char c)
21 {
22 return 'A'<=c && c<='I';
23 }
24
25 bool is_rocklambda(char c)
26 {
27 return is_rocky(c) || c=='\\';
28 }
29
30 ////////////////////////////////////////////////////////////////////////////////
31
32 class Pos
33 {
34 public immutable int y, x;
35 mixin DeriveCreate;
36 mixin DeriveCompare;
37 mixin DeriveShow;
38 Pos clone() const { return cast(Pos) this; }
39
40 const @property:
41 Pos wait() { return this.clone(); }
42 Pos up() { return new Pos(y+1, x); }
43 Pos down() { return new Pos(y-1, x); }
44 Pos left() { return new Pos(y, x-1); }
45 Pos right() { return new Pos(y, x+1); }
46 alias wait W,w;
47 alias up U,u;
48 alias down D,d;
49 alias left L,l;
50 alias right R,r;
51 }
52
53 unittest
54 {
55 assert( (new Pos(2,1)).U == new Pos(3,1) );
56 assert( (new Pos(0,1)).D == new Pos(-1,1) );
57 assert( (new Pos(2,1)).L == new Pos(2,0) );
58 assert( (new Pos(2,1)).R == new Pos(2,2) );
59 int[Pos] aa;
60 aa[new Pos(1,2)] = 1;
61 aa[new Pos(1,2)] = 2;
62 aa[new Pos(2,1)] = 3;
63 assert( aa.length==2 );
64 assert( aa[new Pos(1,2)]==2 );
65 }
66
67 ////////////////////////////////////////////////////////////////////////////////
68
69 class Water
70 {
71 mixin DeriveShow;
72
73 private:
74 immutable int base, pace;
75 mixin DeriveCreate;
76 Water clone() const { return cast(Water) this; }
77
78 static load(string[string] params)
79 {
80 return new Water(params.get("Water", "0").to!int(),
81 params.get("Flooding", "0").to!int());
82 }
83
84 int level(int turn) const
85 {
86 return pace ? base+(turn/pace) : base;
87 }
88
89 int until_rise(int turn) const
90 {
91 return pace ? pace-turn%pace : int.max;
92 }
93 }
94
95 unittest
96 {
97 Water w = new Water(1, 3);
98 assert( 1 == w.level(0) );
99 assert( 1 == w.level(1) );
100 assert( 1 == w.level(2) );
101 assert( 2 == w.level(3) );
102 assert( 2 == w.level(4) );
103 assert( 2 == w.level(5) );
104 assert( 3 == w.level(6) );
105
106 w = new Water(1, 0);
107 assert( 1 == w.level(0) );
108 assert( 1 == w.level(1) );
109 assert( 1 == w.level(2) );
110 assert( 1 == w.level(3) );
111 assert( 1 == w.level(4) );
112 assert( 1 == w.level(5) );
113 }
114
115 ////////////////////////////////////////////////////////////////////////////////
116
117 class Hige
118 {
119 mixin DeriveShow;
120
121 private:
122 immutable int pace;
123 mixin DeriveCreate;
124 Hige clone() const { return cast(Hige)this; }
125
126 static load(string[string] params)
127 {
128 return new Hige(params.get("Growth", "25").to!int());
129 }
130
131 bool is_growing_turn(int turn) const
132 {
133 return pace ? turn%pace == pace-1 : false;
134 }
135
136 int until_rise(int turn) const
137 {
138 return pace ? pace-turn%pace : int.max;
139 }
140 }
141
142 ////////////////////////////////////////////////////////////////////////////////
143
144 class Trampoline
145 {
146 mixin DeriveShow;
147
148 private:
149 immutable char[] target_of_;
150 immutable char[][] source_of_;
151 immutable Pos[] position_of_;
152 immutable char[] source_list_;
153 immutable char[] target_list_;
154 Trampoline clone() const { return cast(Trampoline) this; }
155
156 this(in Map m, char[char] tramparam)
157 {
158 auto ta = new char['I'+1];
159 auto sr = new char[]['9'+1];
160 auto po = new Pos[max('I','9')+1];
161 char[] sl, tl;
162 foreach(fr,to; tramparam) {
163 ta[fr] = to;
164 sr[to] ~= fr;
165 }
166 for(int y=1; y<=m.H; ++y)
167 for(int x=1; x<=m.W; ++x) {
168 char c = m[y,x];
169 if('A'<=c && c<='I') {
170 sl ~= c;
171 po[c] = new Pos(y,x);
172 }
173 if('1'<=c && c<='9') {
174 tl ~= c;
175 po[c] = new Pos(y,x);
176 }
177 }
178 target_of_ = cast(immutable) ta;
179 source_of_ = cast(immutable) sr;
180 position_of_ = cast(immutable) po;
181 source_list_ = cast(immutable) sl;
182 target_list_ = cast(immutable) tl;
183 }
184
185 public @property const:
186 const(char[]) source_list() { return source_list_; }
187 const(char[]) target_list() { return target_list_; }
188 const(char[]) source_of(char c) { return source_of_[c]; }
189 char target_of(char c) { return target_of_[c]; }
190 Pos[] source_pos(char c) {
191 Pos[] ps;
192 foreach(s; source_of(c))
193 ps ~= position_of_[s].clone();
194 return ps;
195 }
196 Pos target_pos(char c) { return position_of_[target_of_[c]].clone(); }
197 }
198
199 ////////////////////////////////////////////////////////////////////////////////
200
201 class Map
202 {
203 mixin DeriveShow;
204
205 private char[][] data;
206 Pos robot;
207 Pos lift;
208 private int waterproof;
209 private int collected_razor;
210 int collected_lambda;
211 int total_lambda;
212 private bool cleared;
213 private Pos[] may_update;
214
215 private Map clone() const { return new Map(this); }
216 private this(in Map m) {
217 foreach(s; m.data)
218 this.data ~= s.dup;
219 this.robot = m.robot.clone();
220 this.lift = m.lift.clone();
221 this.waterproof = m.waterproof;
222 this.collected_razor = m.collected_razor;
223 this.collected_lambda = m.collected_lambda;
224 this.total_lambda = m.total_lambda;
225 this.cleared = m.cleared;
226 this.may_update = (cast(Map)m).may_update.dup;
227 }
228
229 const {
230 @property {
231 int H() { return data.length; }
232 int W() { return data[0].length; }
233 int num_razor() { return collected_razor; }
234
235 Pos[] razors() { return objects('!'); }
236 Pos[] lambdas() { return objects('\\'); }
237 }
238
239 Pos[] objects(char c) {
240 Pos[] ans;
241 for(int y=1; y<=H; ++y)
242 for(int x=1; x<=W; ++x)
243 if(this[y,x] == c)
244 ans ~= new Pos(y,x);
245 return ans;
246 }
247
248 char opIndex(int y, int x) {
249 --y, --x;
250 if(y<0||H<=y||x<0||W<=x)
251 return '#';
252 return data[H-1-y][x];
253 }
254
255 char opIndex(in Pos p) {
256 return this[p.y, p.x];
257 }
258 }
259
260 private:
261 this(string[] raw_data, string[string] params, char[char] trampo)
262 {
263 int width = 0;
264 foreach(r; raw_data)
265 width = max(width, r.length);
266 foreach(r; raw_data) {
267 this.data ~= r.dup;
268 this.data[$-1].length = width;
269 this.data[$-1][r.length..$] = ' ';
270 }
271
272 for(int y=1; y<=H; ++y)
273 for(int x=1; x<=W; ++x) {
274 if(this[y,x] == 'R')
275 this.robot = new Pos(y,x);
276 if(this[y,x] == 'L' || this[y,x] == 'O')
277 this.lift = new Pos(y,x);
278 if(this[y,x] == '\\' || this[y,x] == '@')
279 total_lambda++;
280 if(is_rocky(this[y,x]))
281 may_update ~= new Pos(y,x);
282 }
283
284 this.waterproof = params.get("Waterproof", "5").to!int();
285 this.collected_razor = params.get("Razors", "0").to!int();
286 }
287
288 void opIndexAssign(char c, int y, int x)
289 {
290 --y, --x;
291 if(y<0||H<=y||x<0||W<=x)
292 return;
293 data[H-1-y][x] = c;
294 }
295
296 void opIndexAssign(char c, in Pos p)
297 {
298 this[p.y, p.x] = c;
299 }
300
301 bool command(char c, int turn, bool hige_day, in Trampoline tr)
302 {
303 switch(c)
304 {
305 case 'R': return move( 0, +1, hige_day, tr);
306 case 'L': return move( 0, -1, hige_day, tr);
307 case 'U': return move(+1, 0, hige_day, tr);
308 case 'D': return move(-1, 0, hige_day, tr);
309 case 'W': return move( 0, 0, hige_day, tr);
310 case 'S': return use_razor(hige_day);
311 default: assert(false);
312 }
313 }
314
315 bool use_razor(bool hige_day)
316 {
317 if(collected_razor > 0)
318 {
319 collected_razor--;
320 for(int dy=-1; dy<=+1; ++dy)
321 for(int dx=-1; dx<=+1; ++dx)
322 if(this[robot.y+dy,robot.x+dx] == 'W')
323 emptify(new Pos(robot.y+dy,robot.x+dx));
324 }
325 return update(hige_day);
326 }
327
328 // Register a position that may become empty in the last turn.
329 void emptify(Pos p)
330 {
331 this[p] = ' ';
332 for(int dy=0; dy<=+1; ++dy)
333 for(int dx=-1; dx<=+1; ++dx)
334 may_update ~= new Pos(p.y+dy, p.x+dx);
335 }
336
337 bool move(int dy, int dx, bool hige_day, in Trampoline tr)
338 {
339 Pos next = new Pos(robot.y+dy, robot.x+dx);
340 int y=robot.y, x=robot.x;
341
342 if( '\\' == this[next] ) collected_lambda++;
343 if( '!' == this[next] ) collected_razor++;
344 if( 'O' == this[next] ) cleared = true;
345
346 if( is_spacy(this[next]) )
347 {
348 emptify(robot);
349 robot = next;
350 this[next] = 'R';
351 }
352 else if(dy==0 && is_rocky(this[next]) && ' '==this[y+dy*2,x+dx*2])
353 {
354 char rock = this[next];
355 emptify(robot);
356 robot = next;
357 this[next] = 'R';
358 this[y+dy*2,x+dx*2] = rock;
359 may_update ~= new Pos(y+dy*2,x+dx*2);
360 }
361 else if(is_trampoline_source(this[next]))
362 {
363 emptify(robot);
364 Pos tp = tr.target_pos(this[next]);
365 foreach(p; tr.source_pos(this[tp]))
366 emptify(p);
367 this[tp] = 'R';
368 robot = tp;
369 }
370 return update(hige_day);
371 }
372
373 bool update(bool hige_day)
374 {
375 // Write after all the updates are processed.
376 Tuple!(int,int,char)[] write_buffer;
377 void write(int y, int x, char c) { write_buffer ~= tuple(y,x,c); }
378 void writep(Pos p, char c) { write_buffer ~= tuple(0+p.y,0+p.x,c); }
379 scope(exit) {
380 may_update.length = 0;
381 foreach(wr; write_buffer) {
382 this[wr[0],wr[1]] = wr[2];
383 if(is_rocky(wr[2]))
384 may_update ~= new Pos(wr[0],wr[1]);
385 if(wr[2]==' ')
386 emptify(new Pos(wr[0], wr[1]));
387 }
388 }
389
390 if(collected_lambda == total_lambda)
391 if(this[lift]=='L')
392 this[lift] = 'O';
393
394 bool dead = false;
395 if( hige_day ) {
396 for(int y=1; y<=H; ++y)
397 for(int x=1; x<=W; ++x)
398 if(this[y,x]=='W')
399 may_update ~= new Pos(y,x);
400 }
401
402 sort(may_update);
403 foreach(p; may_update) {
404 int y = p.y, x = p.x;
405 char rock = this[p];
406 if(is_rocky(this[p])) {
407 if(this[p.D]==' ') {
408 writep(p, ' ');
409 writep(p.D, (rock=='@'&&this[p.D.D]!=' ' ? '\\' : rock));
410 if(robot == p.D.D)
411 dead=true;
412 }
413 else if((is_rocky(this[p.D]) || this[p.D]=='\\') && this[p.R]==' ' && this[p.R.D]==' ') {
414 writep(p, ' ');
415 writep(p.R.D,(rock=='@'&&this[p.R.D.D]!=' ' ? '\\' : rock));
416 if(robot == p.R.D.D)
417 dead=true;
418 }
419 else if(is_rocky(this[p.D]) && this[p.L]==' ' && this[p.L.D]==' ') {
420 writep(p, ' ');
421 writep(p.L.D, (rock=='@'&&this[p.L.D.D]!=' ' ? '\\' : rock));
422 if(robot == p.L.D.D)
423 dead=true;
424 }
425 }
426 else if(this[p]=='W') {
427 if(hige_day) {
428 for(int dy=-1; dy<=+1; ++dy)
429 for(int dx=-1; dx<=+1; ++dx)
430 if(this[p.y+dy,p.x+dx] == ' ') {
431 write(p.y+dy,p.x+dx,'W');
432 if(robot.y==p.y+dy-1 && robot.x==p.x+dx)
433 dead = false; // guarded by hige!
434 }
435 }
436 }
437 }
438
439 return dead;
440 }
441 }
442
443 ////////////////////////////////////////////////////////////////////////////////
444
445 class Game
446 {
447 mixin DeriveShow;
448
449 private {
450 Map map_;
451 Water water_;
452 Hige hige_;
453 Trampoline tr_;
454 int turn = 0;
455 bool dead_ = false;
456 int under_water = 0;
457 }
458
459 Game clone() const { return new Game(this); }
460 this(in Game g) {
461 map_ = g.map_.clone();
462 water_ = g.water_.clone();
463 hige_ = g.hige_.clone();
464 tr_ = g.tr_.clone();
465 turn = g.turn;
466 dead_ = g.dead_;
467 under_water = g.under_water;
468 }
469
470 this(File input)
471 {
472 string[] raw_data;
473 string[string] params;
474
475 // Raw map data; read until empty line.
476 for(string line; !(line=input.readln().chomp()).empty; )
477 raw_data ~= line;
478
479 // Additional commands; read until EOF.
480 char[char] trampo;
481 for(string line; !(line=input.readln()).empty; ) {
482 string[] ss = line.split();
483 if( ss.length == 2 )
484 params[ss[0]] = ss[1];
485 if( ss.length == 4 && ss[0]=="Trampoline" && ss[2]=="targets" )
486 trampo[ss[1][0]] = ss[3][0];
487 }
488
489 this.map_ = new Map(raw_data, params, trampo);
490 this.water_ = Water.load(params);
491 this.hige_ = Hige.load(params);
492 this.tr_ = new Trampoline(this.map, trampo);
493 }
494
495 void command(char c)
496 {
497 assert(c != 'A');
498 if(dead || cleared)
499 return;
500
501 // TODO: clarify the event order
502 bool dead_now = map_.command(c, turn, hige.is_growing_turn(turn), tr);
503 if( dead_now )
504 dead_ = true;
505 if(!map.cleared) {
506 if( map.robot.y <= water_level )
507 ++under_water;
508 else
509 under_water = 0;
510 if( under_water > map.waterproof )
511 dead_ = true;
512 }
513 turn += 1;
514 }
515
516 @property const:
517 long score() { return map.collected_lambda*(dead?25L:cleared?75L:50L)-turn; }
518 int water_level() { return water.level(turn); }
519 int water_until_rise() { return water.until_rise(turn); }
520 int hige_until_rise() { return hige.until_rise(turn); }
521 int hp() { return map.waterproof - under_water; }
522 bool cleared() { return map.cleared; }
523 bool dead() { return dead_; }
524 const(Map) map() { return map_; }
525 const(Water) water() { return water_; }
526 const(Hige) hige() { return hige_; }
527 const(Trampoline) tr() { return tr_; }
528 }