var Mine = function() { var ALIVE = 0, LOST = 1, ABORTED = 2, WON = 3; function repeat(str, n) { return new Array(n+1).join(str); } function Mine(map) { this.parse(map) }; Mine.ALIVE = ALIVE; Mine.LOST = LOST; Mine.ABORTED = ABORTED; Mine.WON = WON; Mine.prototype.parse = function(map) { var lines, i, width, height, pair, x, line; this.orig_map = map; lines = map.split(/\r\n?|\r?\n/); width = 0; height = lines.length; for (i = 0; i < lines.length; ++i) { if (0 == lines[i].length) { height = i; lines.splice(i, 1); break; } width = Math.max(width, lines[i].length); } this.height = height; this.width = width; this.map = map = lines.splice(0, height).reverse(); this.lambdas = 0; this.found_lambdas = 0; this.moves = 0; this.score = 0; this.moves_below_water = 0; this.water = { level: 0, flooding: 0, proof: 10 }; this.lift = this.robot = false; this.trampoline = { sources: { }, // x, y and target targets: { }, // x, y and sources }; this.trampoline.targets = {}; for (i = 0; i < height; ++i) { // padding if (map[i].length < width) map[i] += repeat(' ', width - map[i].length); line = map[i] = map[i].split(''); // scan for (x = 0; x < width; ++x) { switch (line[x]) { case ' ': case '*': case '#': case '.': break; case '\\': this.lambdas++; break; case 'L': if (this.lift !== false) throw "Only one lift is allowed"; this.lift = { x: x, y: i }; break; case 'R': if (this.robot !== false) throw "Only one robot is allowed"; this.robot = { x: x, y: i }; break; default: if (line[x] >= 'A' && line[x] <= 'I') { if (this.trampoline.sources[line[x]]) throw "Can have only one trampoline " + line[x]; this.trampoline.sources[line[x]] = { x: x, y: i, target: false }; this.trampoline.fromSources } else if (line[x] >= '1' && line[x] <= '9') { if (this.trampoline.targets[line[x]]) throw "Can have only one trampoline target " + line[x]; this.trampoline.targets[line[x]] = { x: x, y: i, sources: [] }; } else { throw "Invalid character in map: '" + line[x] + "'"; } break; } } } // if (this.lift === false) throw "Need a lift"; if (this.robot === false) throw "Need a robot"; // meta data this.meta = {}; for (i = 0; i < lines.length; ++i) { if (0 == lines[i].length) continue; var words = lines[i].split(/ +/); switch (words[0]) { case 'Water': this.water.level = words[1]; break; case 'Flooding': this.water.flooding = words[1]; break; case 'Waterproof': this.water.proof = words[1]; break; case 'Trampoline': if (words.length !== 4 || words[2] !== 'targets') throw "Invalid trampoline: '" + words.join(" ") +"'"; if (words[1].length != 1 || words[1] < 'A' || words[1] > 'I') throw "Invalid trampoline source '" + words[1] +"'"; if (words[3].length != 1 || words[3] < '1' || words[3] > '9') throw "Invalid trampoline target '" + words[3] +"'"; if (!this.trampoline.sources[words[1]]) throw "Trampoline " + words[1] + " not defined"; if (!this.trampoline.targets[words[3]]) throw "Trampoline target " + words[3] + " not defined"; if (this.trampoline.sources[words[1]].target) throw "Trampoline " + words[1] + " already has a target"; this.trampoline.sources[words[1]].target = words[3]; this.trampoline.targets[words[3]].sources.push(words[1]); break; default: this.meta[words[0]] = words.splice(1).join(" "); break; } } for (i in this.trampoline.sources) { if (this.trampoline.sources.hasOwnProperty(i)) { if (!this.trampoline.sources[i].target) throw "Trampoline " + i + " has no target"; } } for (i in this.trampoline.targets) { if (this.trampoline.targets.hasOwnProperty(i)) { if (0 == this.trampoline.targets[i].sources.length) throw "Trampoline target " + i + " has no sources"; } } this.water_level = this.water.level; this.state = ALIVE; }; Mine.prototype.get = function (x, y) { if (x < 0 || y < 0 || x >= this.width || y >= this.height) return '#'; return this.map[y][x]; } Mine.prototype.validMove = function (command) { if (this.state != ALIVE) return false; var n, c; command = command.toUpperCase(); switch (command) { case 'L': case 'R': n = (command == 'L' ? -1 : 1); if (this.robot.x + n < 0) return false; if (this.robot.x + n >= this.width) return false; c = this.map[this.robot.y][this.robot.x+n]; switch (c) { case '#': return false; case ' ': case '.': case '\\': return true; case 'L': return false; case 'O': return true; case '*': if (this.robot.x + 2*n < 0) return false; if (this.robot.x + 2*n >= this.width) return false; if (' ' == this.map[this.robot.y][this.robot.x+2*n]) return true; default: if (this.trampoline.sources.hasOwnProperty(c)) return true; break; } break; case 'U': case 'D': n = (command == 'D' ? -1 : 1); if (this.robot.y + n < 0) return false; if (this.robot.y + n >= this.height) return false; c = this.map[this.robot.y+n][this.robot.x]; switch (c) { case '#': return false; case ' ': case '.': case '\\': return true; case 'L': return false; case 'O': return true; case '*': return false; default: if (this.trampoline.sources.hasOwnProperty(c)) return true; break; } break; case 'W': case 'A': return true; } return false; }; Mine.prototype.move = function(command) { if (this.state != ALIVE) return false; var n, c, s, t; command = command.toUpperCase(); if (this.validMove(command)) { switch (command.toUpperCase()) { case 'L': case 'R': n = (command == 'L' ? -1 : 1); this.map[this.robot.y][this.robot.x] = ' '; c = this.map[this.robot.y][this.robot.x+n]; switch (c) { case '*': this.map[this.robot.y][this.robot.x+2*n] = '*'; break; case '\\': this.lambdas--; this.found_lambdas++; break; case 'O': this._foundLift(); break; default: if (this.trampoline.sources.hasOwnProperty(c)) { s = this.trampoline.sources[c]; t = this.trampoline.targets[s.target]; for (n = 0; n < t.sources.length; ++n) { s = this.trampoline.sources[t.sources[n]]; this.map[s.y][s.x] = ' '; } this.robot.x = t.x; this.robot.y = t.y; n = 0; } } this.robot.x += n; this.map[this.robot.y][this.robot.x] = 'R'; break; case 'U': case 'D': n = (command == 'D' ? -1 : 1); this.map[this.robot.y][this.robot.x] = ' '; c = this.map[this.robot.y+n][this.robot.x]; switch (c) { case '\\': this.lambdas--; this.found_lambdas++; break; case 'O': this._foundLift(); break; default: if (this.trampoline.sources.hasOwnProperty(c)) { s = this.trampoline.sources[c]; t = this.trampoline.targets[s.target]; for (n = 0; n < t.sources.length; ++n) { s = this.trampoline.sources[t.sources[n]]; this.map[s.y][s.x] = ' '; } this.robot.x = t.x; this.robot.y = t.y; n = 0; } } this.robot.y += n; this.map[this.robot.y][this.robot.x] = 'R'; break; case 'A': this._abort(); return; } } this.moves++; if (0 == this.lambdas) { if (false !== this.lift && 'L' == this.map[this.lift.y][this.lift.x]) { this.map[this.lift.y][this.lift.x] = 'O'; } } var newMap = [], x, y, below; for (y = 0; y < this.height; ++y) { newMap[y] = this.map[y].slice(); } for (y = 0; y < this.height; ++y) { for (x = 0; x < this.width; ++x) { if ('*' == this.map[y][x]) { below = this.get(x, y-1); if (' ' == below) { // fall down newMap[y-1][x] = '*'; newMap[y][x] = ' '; if ('R' == this.get(x, y-2)) this._crushed(); } else if ((below == '*' || below == '\\') && ' ' == this.get(x+1, y-1) && ' ' == this.map[y][x+1]) { // fall right newMap[y-1][x+1] = '*'; newMap[y][x] = ' '; if ('R' == this.get(x+1, y-2)) this._crushed(); } else if ((below == '*') && ' ' == this.get(x-1, y-1) && ' ' == this.map[y][x-1]) { // fall left newMap[y-1][x-1] = '*'; newMap[y][x] = ' '; if ('R' == this.get(x-1, y-2)) this._crushed(); } } } } this.map = newMap; if (this.robot.y < this.water_level) { this.moves_below_water++; if (this.moves_below_water > this.water.proof) this._drown(); } else { this.moves_below_water = 0; } if (this.water.flooding > 0 && 0 == (this.moves % this.water.flooding)) { ++this.water_level; } switch (this.state) { case LOST: this.score = 25*this.found_lambdas - this.moves; break; case WON: this.score = 75*this.found_lambdas - this.moves; break; case ALIVE: case ABORTED: this.score = 50*this.found_lambdas - this.moves; break; } }; Mine.prototype._drown = function() { if (this.state < LOST) { this.state = LOST; this.reason = "Drowned"; } }; Mine.prototype._crushed = function() { if (this.state < LOST) { this.state = LOST; this.reason = "Crushed by rock"; } }; Mine.prototype._foundLift = function() { if (this.state < WON) { this.state = WON; this.reason = "Found lift"; } }; Mine.prototype._abort = function() { if (this.state < ABORTED) { this.state = ABORTED; this.reason = "Aborted"; } }; Mine.prototype.toString = function() { return this.map.map(function(l) { return l.join(''); }).reverse().join("\n"); }; Mine.prototype.metaText = function() { var k, keys = [], lines = []; if (this.water.level != 0) lines.push("Water " + this.water.level); if (this.water.flooding != 0) lines.push("Flooding " + this.water.flooding); if (this.water.proof != 10) lines.push("Waterproof " + this.water.proof); for (k in this.trampoline.sources) { if (this.trampoline.sources.hasOwnProperty(k)) keys.push(k); } keys.sort(); for (k = 0; k < keys.length; ++k) { lines.push("Trampoline " + keys[k] + " targets " + this.trampoline.sources[keys[k]].target); } keys = []; for (k in this.meta) { if (this.meta.hasOwnProperty(k)) keys.push(k); } keys.sort(); for (k = 0; k < keys.length; ++k) { lines.push( keys[k] + " " + this.meta[keys[k]]); } return lines.join("\n"); }; return Mine; }(); if (typeof exports !== "undefined") { exports.Mine = Mine; }