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.beard = { growth: 25, razors: 0 }; map.splice(height, 0, repeat('#', width)); map.splice(0, 0, repeat('#', width), repeat('#', width)); height = height + 3; width = width + 2; for (i = 0; i < height; ++i) { // padding if (map[i].length < width-2) map[i] += repeat(' ', width - map[i].length - 2); // extra # padding: map[i] = '#' + map[i] + '#'; line = map[i] = map[i].split(''); // scan for (x = 0; x < width; ++x) { switch (line[x]) { case ' ': case '*': case '#': case '.': case 'W': 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] + "'" + 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 = parseInt(words[1]); break; case 'Flooding': this.water.flooding = parseInt(words[1]); break; case 'Waterproof': this.water.proof = parseInt(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; case 'Growth': this.beard.growth = parseInt(words[1]); break; case 'Razors': this.beard.razors = parseInt(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.razors = this.beard.razors; 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); c = this.map[this.robot.y][this.robot.x+n]; switch (c) { case '#': return false; case ' ': case '.': case '!': case '\\': return true; case 'L': return false; case 'O': return true; case '*': 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); c = this.map[this.robot.y+n][this.robot.x]; switch (c) { case '#': return false; case ' ': 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 'S': return this.razors > 0; case 'A': return true; } return false; }; Mine.prototype.move = function(command) { if (this.state != ALIVE) return false; var n, c, s, t; var newMap, x, y, i, j, below, map = this.map, growBeard; command = command.toUpperCase(); if (this.validMove(command)) { switch (command.toUpperCase()) { case 'L': case 'R': n = (command == 'L' ? -1 : 1); map[this.robot.y][this.robot.x] = ' '; c = map[this.robot.y][this.robot.x+n]; switch (c) { case '*': map[this.robot.y][this.robot.x+2*n] = '*'; break; case '\\': this.lambdas--; this.found_lambdas++; break; case '!': this.razors++; 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]]; map[s.y][s.x] = ' '; } this.robot.x = t.x; this.robot.y = t.y; n = 0; } } this.robot.x += n; map[this.robot.y][this.robot.x] = 'R'; break; case 'U': case 'D': n = (command == 'D' ? -1 : 1); map[this.robot.y][this.robot.x] = ' '; c = map[this.robot.y+n][this.robot.x]; switch (c) { case '\\': this.lambdas--; this.found_lambdas++; break; case '!': this.razors++; 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]]; map[s.y][s.x] = ' '; } this.robot.x = t.x; this.robot.y = t.y; n = 0; } } this.robot.y += n; map[this.robot.y][this.robot.x] = 'R'; break; case 'S': --this.razors; for (var i = -1; i <= 1; ++i) for (var j = -1; j <= 1; ++j) { if ('W' == map[this.robot.y+i][this.robot.x+j]) map[this.robot.y+i][this.robot.x+j] = ' '; } break; case 'A': this._abort(); return; } } this.moves++; if (0 == this.lambdas) { if (false !== this.lift) { /* skip 'L' == this.map[this.lift.y][this.lift.x] - official validator replaces * 'R' with 'O' after the last move too */ this.map[this.lift.y][this.lift.x] = 'O'; } } newMap = []; growBeard = false; if (this.beard.growth > 0 && 0 == (this.moves % this.beard.growth)) { growBeard = true; } for (y = 0; y < map.length; ++y) { newMap[y] = this.map[y].slice(); } for (y = 2; y < 2+this.height; ++y) { for (x = 1; x <= this.width; ++x) { if ('*' == map[y][x]) { below = map[y-1][x]; if (' ' == below) { // fall down newMap[y-1][x] = '*'; newMap[y][x] = ' '; if ('R' == map[y-2][x]) this._crushed(); } else if ((below == '*' || below == '\\') && ' ' == map[y-1][x+1] && ' ' == map[y][x+1]) { // fall right newMap[y-1][x+1] = '*'; newMap[y][x] = ' '; if ('R' == map[y-2][x+1]) this._crushed(); } else if ((below == '*') && ' ' == map[y-1][x-1] && ' ' == map[y][x-1]) { // fall left newMap[y-1][x-1] = '*'; newMap[y][x] = ' '; if ('R' == map[y-2][x-1]) this._crushed(); } } else if (growBeard && 'W' == map[y][x]) { for (i = -1; i <= 1; ++i) for (j = -1; j <= 1; ++j) { if (' ' == map[y+i][x+j]) newMap[y+i][x+j] = 'W'; } } } } this.map = newMap; if (this.robot.y < this.water_level + 2) { 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.slice(2,-1).map(function(l) { return l.slice(1,-1).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); } if (this.beard.growth != 25) lines.push("Growth " + this.beard.growth); if (this.beard.razors != 0) lines.push("Razors " + this.beard.razors); 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; }