diff --git a/js/Makefile b/js/Makefile new file mode 100644 index 0000000..186c4f2 --- /dev/null +++ b/js/Makefile @@ -0,0 +1,8 @@ + +MAPS=$(wildcard ../maps/contest*.map ../maps/flood*.map) + +all: maps.js + + +maps.js: mapsToJson.js $(MAPS) + ./mapsToJson.js $(MAPS) > maps.js diff --git a/js/gui.js b/js/gui.js new file mode 100644 index 0000000..abee99a --- /dev/null +++ b/js/gui.js @@ -0,0 +1,184 @@ +var mineGui_mine, mineGui_curmap, mineGui_curndx, mineGui_moves = ""; +var mineGui_movesBackup = []; +var mineGui_customNdx = 1; + +function mineGui_addCustomMap() { + var map = document.getElementById("mineGui_data").value; + var mine; + var selMap = document.getElementById("mineGui_selectMap"); + document.getElementById("mineGui_addDataResult").textContent = ""; + try { + mine = new Mine(map); + } catch (e) { + document.getElementById("mineGui_addDataResult").textContent = "Adding map failed: " + e; + return; + } + mineMaps["custom" + mineGui_customNdx] = map; + var entry = document.createElement("option"); + entry.textContent = "custom" + mineGui_customNdx; + selMap.appendChild(entry); + mineGui_customNdx++; + + window.setTimeout(function() { + selMap.selectedIndex = selMap.options.length - 1; + selMap.onchange(); + }, 0); +} + +function mineGui_start() { + var div = document.getElementById("mineGui"); + var selMap = document.getElementById("mineGui_selectMap"); + var inpMoves = document.getElementById("mineGui_moves"); + var customMapInput = document.getElementById("mineGui_data") + var validMoves = {L:1,U:1,R:1,D:1,A:1,W:1}; + document.getElementById("mineGui_addData").onclick = mineGui_addCustomMap; + selMap.onchange = function() { + var k = selMap.options[selMap.selectedIndex].text; + mineGui_setMap(mineMaps[k], selMap.selectedIndex); + selMap.blur(); + }; + var inpValidate = function() { + if (inpMoves.value == mineGui_moves) return; + mineGui_moves = ""; + var txt = inpMoves.value.toUpperCase(), i; + for (i = 0; i < txt.length; ++i) { + if (validMoves[txt[i]]) mineGui_moves += txt[i]; + } + inpMoves.blur(); + mineGui_updateMine(); + }; + var delayInpValidate = function() { window.setTimeout(inpValidate, 0); }; + inpMoves.onchange = delayInpValidate; + inpMoves.onpaste = delayInpValidate; + inpMoves.onkeypress = delayInpValidate; + document.onkeypress = function (event) { + if (document.activeElement === customMapInput) return; + if (event.ctrlKey || event.altKey || event.metaKey) return; + var handled = true; + if (event.keyCode == 8 || event.charCode == 8) { + // backspace -> undo + mineGui_moves = mineGui_moves.slice(0,-1); + mineGui_updateMine(); + } else if (event.which === 0 && event.keyCode) { + switch (event.keyCode) { + case 33: // page up + if (selMap.selectedIndex > 0) { + selMap.selectedIndex--; + selMap.onchange(); + } + break; + case 34: // page down + if (selMap.selectedIndex+1 < selMap.options.length) { + selMap.selectedIndex++; + selMap.onchange(); + } + break; + case 37: // left + mineGui_move('L'); + break; + case 38: // up + mineGui_move('U'); + break; + case 39: // right + mineGui_move('R'); + break; + case 40: // down + mineGui_move('D'); + break; + default: + handled = false; + break; + } + } else if (event.charCode) { + console.log(event.charCode); + cmd = String.fromCharCode(event.charCode).toUpperCase(); + if (validMoves[cmd]) { + mineGui_move(cmd); + } else if (cmd == 'R') { // redraw + mineGui_updateMine(); + } else if (cmd == 'C') { // clear + mineGui_moves = ""; + mineGui_updateMine(); + } else { + handled = false; + } + } else { + handled = false; + } + if (handled) event.preventDefault(); + }; + for (k in mineMaps) { + if (mineMaps.hasOwnProperty(k)) { + var entry = document.createElement("option"); + entry.textContent = k; + selMap.appendChild(entry); + } + //entry.add + } + selMap.selectedIndex = 0; + selMap.onchange(); +} + +function mineGui_move(cmd) { + for (i = 0; i < cmd.length; ++i) { + if (mineGui_mine.state != Mine.ALIVE) break; + mineGui_moves += cmd[i]; + mineGui_mine.move(cmd[i]); + } + mineGui_show(); +} + +function mineGui_setMap(map, ndx) { + mineGui_movesBackup[mineGui_curndx] = mineGui_moves; + mineGui_curmap = map; + mineGui_curndx = ndx; + mineGui_moves = mineGui_movesBackup[mineGui_curndx]; + if (!mineGui_moves) mineGui_moves = ""; + mineGui_updateMine(); +} + +function mineGui_updateMine() { + // redo moves + mineGui_mine = new Mine(mineGui_curmap); + var cmd = mineGui_moves; + mineGui_moves = ""; + for (i = 0; i < cmd.length; ++i) { + if (mineGui_mine.state != Mine.ALIVE) break; + mineGui_moves += cmd[i]; + mineGui_mine.move(cmd[i]); + } + mineGui_show(); +} + +function mineGui_show() { + var waterLevel = Math.max(mineGui_mine.meta.Water, 0); + var map = mineGui_mine.toString().split(/\n/); + if (waterLevel > 0) { + document.getElementById("mineGui_mineMapWater").textContent = map.splice(-waterLevel).join("\n"); + } else { + document.getElementById("mineGui_mineMapWater").textContent = ""; + } + document.getElementById("mineGui_mineMap").textContent = map.join("\n"); + document.getElementById("mineGui_moves").value = mineGui_moves; + var state = ""; + switch (mineGui_mine.state) { + case Mine.ALIVE: + state = "Still mining"; + break; + case Mine.LOST: + state = "Robot broken - " + mineGui_mine.reason; + break; + case Mine.ABORTED: + state = "Aborted"; + break; + case Mine.WON: + state = "Won - " + mineGui_mine.reason; + break; + } + document.getElementById("mineGui_meta").textContent = mineGui_mine.metaText(); + document.getElementById("mineGui_state").textContent = state; + document.getElementById("mineGui_score").textContent = mineGui_mine.score; + document.getElementById("mineGui_scoreMoves").textContent = mineGui_mine.moves; + document.getElementById("mineGui_scoreLambdas").textContent = mineGui_mine.found_lambdas; + document.getElementById("mineGui_belowWater").textContent = mineGui_mine.moves_below_water; +} diff --git a/js/index.html b/js/index.html new file mode 100644 index 0000000..4b7005c --- /dev/null +++ b/js/index.html @@ -0,0 +1,49 @@ + + + + + ICFP Contest 2012 Simulator + +

ICFP Contest 2012 Simulator

+ Links: contest page.
+ This simulator may or may not perform according to specs, who knows...
+ +
+ +
+ Select Map: +
Moves:
+
+

+			

+		
+

+		
+
Score: 0 + + (0 moves, + found 0 Lambdas) + +
+
Status: 0
+
For 0 moves under water
+
+ +
+ +
Help:
+(L)eft/(U)p/(R)ight/(D)own work as expected (letter and arrow keys), (A)bort and (W)ait too.
+Special keys: PageUp/PageDown for map select, Backspace for undo, (C)lear and (R)edraw
+Be careful: a reload looses all custom maps and saved moves.
+	
+ + Build custom map:
+

+	
+	
+	
© 2012 Stefan Bühler. Please do not use my code in contest submissions :)
+ + + \ No newline at end of file diff --git a/js/maps.js b/js/maps.js new file mode 100644 index 0000000..043f288 --- /dev/null +++ b/js/maps.js @@ -0,0 +1,2 @@ +var mineMaps = {"contest10":"#############################\n#..........................\\#\n#..\\\\###...#.... ###.#\n#..\\*\\\\\\.. #.... ..##\\\\..\\#.#\n#..\\*\\.... #.... ..#\\#....#.#\n#...\\###.. #.... ....#....#.#\n#... ..... ..... .####......#\n#\\\\. #.... .......#\n#... #..#. .....*\\ ##.......#\n#.#....... ...#.. ....######\n#. ...#... ...#.\\ ....#..* #\n##........ ...#.. #....#.#\\\\#\n#.....*... .....*\\#\\\\.....*.#\n#.***.* .......*\\****.....#.#\n#.\\\\\\.. ................ .#\n#.##### .###### ##### #\n#....\\\\.................... #\n#....****...#.##.....\\\\\\\\..\\#\n#....\\\\\\\\...#.........*....\\#\n#....\\\\\\\\...#.\\\\. #\\###.\\#\n#.... ..#.... ...#\\\\\\\\. #\n#........ ..#.... ...#..... #\n#........ ........#R#\n###########################L#\n","contest1":"######\n#. *R#\n# \\.#\n#\\ * #\nL .\\#\n######\n","contest2":"#######\n#..***#\n#..\\\\\\#\n#...**#\n#.*.*\\#\nLR....#\n#######\n","contest3":"########\n#..R...#\n#..*...#\n#..#...#\n#.\\.\\..L\n####**.#\n#\\.....#\n#\\..* .#\n########\n","contest4":"#########\n#.*..#\\.#\n#.\\..#\\.L\n#.R .##.#\n#.\\ ...#\n#..\\ ..#\n#...\\ ##\n#....\\ \\#\n#########\n","contest5":"############\n#..........#\n#.....*....#\n#..\\\\\\\\\\\\..#\n#. ....#\n#..\\\\\\\\\\\\\\.#\n#..\\.. .#\n#..\\.. ....#\n#..... ..* #\n#..### ### #\n#...R#\\#\\\\.#\n######L#####\n","contest6":"###############\n#\\\\\\.......** #\n#\\\\#.#####...##\n#\\\\#.....*##. #\n#\\#####\\...## #\n#\\......####* #\n#\\.######* #.\\#\n#\\.#. *...##.##\n#\\##. .. *...#\n#\\...... L#.#.#\n###########.#.#\n#\\..........#.#\n##.##########.#\n#R.#\\.........#\n###############\n","contest7":" #######\n ## *#\n ##R *##\n ##\\\\\\\\##\n ##....##\n ##..\\ . ##\n ## . L . ##\n ##\\\\\\# #\\\\\\\\##\n ###### #######\n","contest8":"##############\n#\\\\... ......#\n###.#. ...*..#\n #.#. ... ..#\n### #. \\ ..#\n#. .#..... **#######\n#.#\\#..... ..\\\\\\*. #\n#*\\\\#.###. ####\\\\\\ #\n#\\\\.#. ...## \\ #\n#\\#.#..... ....# \\ # \n###.#..... ....# ##\n#\\\\.#..... ....#\\ # \n########.. ..###*####\n#......... .........#\n#......... ....***..#\n#..\\\\\\\\\\ # ####.....#\n#........*R..\\\\\\ .#\n##########L##########\n","contest9":" #L#######\n #*** \\\\ #\n #\\\\\\ .. #\n#########.## ##########\n#.......\\ ..........* .#\n#*******\\......#....#\\\\ .#\n###\\.\\\\\\...**..#....... *#\n#*****\\\\ .\\\\..## #\\.#\n######### .... ##########\n # #\n ####*#### \n #.......#\n######### \\\\\\\\*##########\n#*\\\\ **# *..*\\ \\\\\\\\\\#\n#.\\**\\*** .....**.# \\\\##\\#\n#\\R...... .\\\\.. \\\\\\\\\\#\n##########################\n","flood1":"###########\n#....R....#\n#.*******.#\n#.\\\\\\\\\\\\\\.#\n#. .#\n#..*\\\\\\*..#\n#.#*\\\\\\*#.#\n#########L#\n\nWater 1\nFlooding 8 \nWaterproof 5\n","flood2":"#######\n#..***#\n#..\\\\\\#\n#...**#\n#.*.*\\#\nLR....#\n#######\n\nFlooding 5\nWaterproof 3\n","flood3":"############\n#..........#\n#.....*....#\n#..\\\\\\\\\\\\..#\n#. ....#\n#..\\\\\\\\\\\\\\.#\n#..\\.. .#\n#..\\.. ....#\n#..... ..* #\n#..### ### #\n#...R#\\#\\\\.#\n######L#####\n\nWaterproof 10 \nFlooding 10\nWater 2\n","flood4":"########################\n#.....................\\#\n#......*\\ ...........#\n#......*... ......* ...#\n#.. \\\\... .*..... ...#\n#.. ....... ....... ...#\n#.. ....... .\\\\.... ...# ######\n#. .... .....\\...# #\\\\\\\\#\n#\\\\\\......... .........# #....#\n########### R ########*#####\n #.......... ........***\\\\\\#\n #.......... ............**#\n #.......... ......... *.*\\#\n #....\\\\.... ....\\\\..... ..L\n #.....................****#\n #........\\*...............#\n #........... .........#\n #.........................#\n ###########################\n\nWater 1\nFlooding 20\nWaterproof 10\n","flood5":"#########\n#.*..#\\.#\n#.\\..#\\.L\n#.R .##.#\n#.\\ ...#\n#..\\ ..#\n#...\\ ##\n#....\\ \\#\n#########\n\nWater 2\nFlooding 11\nWaterproof 5\n"}; +if (typeof exports !== "undefined") {exports.maps = mineMaps;} diff --git a/js/mapsToJson.js b/js/mapsToJson.js new file mode 100755 index 0000000..7cfb692 --- /dev/null +++ b/js/mapsToJson.js @@ -0,0 +1,14 @@ +#!/usr/bin/env node + +var fs = require('fs'); +var path = require('path'); + +var i, map, maps = {}, fname; +for (i = 2; i < process.argv.length; ++i) { + fname = process.argv[i]; + map = fs.readFileSync(fname, 'utf-8'); + maps[path.basename(fname, '.map')] = map; +} + +process.stdout.write("var mineMaps = " + JSON.stringify(maps) + ";\n"); +process.stdout.write('if (typeof exports !== "undefined") {exports.maps = mineMaps;}' + "\n"); diff --git a/js/mine.js b/js/mine.js new file mode 100644 index 0000000..195d596 --- /dev/null +++ b/js/mine.js @@ -0,0 +1,287 @@ + +var Mine = function() { + var ALIVE = 0, LOST = 1, ABORTED = 2, WON = 3; + + function defaultMeta() { + return { + Water: 0, + Flooding: 0, + Waterproof: 10, + } + } + + 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.lift = this.robot = false; + 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; + } + } + } + + // if (this.lift === false) throw "Need a lift"; + if (this.robot === false) throw "Need a robot"; + + // meta data + this.meta = defaultMeta(); + for (i = 0; i < lines.length; ++i) { + if (0 == lines[i].length) continue; + pair = lines[i].split(/ +/); + this.meta[pair[0]] = pair.splice(1).join(" "); + } + 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; + 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; + switch (this.map[this.robot.y][this.robot.x+n]) { + case '#': return false; + case ' ': + case '.': + case '\\': return true; + case 'L': return 0 == this.lambdas; + 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; + } + 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; + switch (this.map[this.robot.y+n][this.robot.x]) { + case '#': return false; + case ' ': + case '.': + case '\\': return true; + case 'L': return 0 == this.lambdas; + case '*': return false; + " style="} + break; + case 'W': + case 'A': + return true; + } + return false; + }; + + Mine.prototype.move = function(command) { + if (this.state != ALIVE) return false; + var n; + 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] = ' '; + switch (this.map[this.robot.y][this.robot.x+n]) { + case '*': + this.map[this.robot.y][this.robot.x+2*n] = '*'; + break; + case '\\': + this.lambdas--; + this.found_lambdas++; + break; + case 'L': + this._foundLift(); + break; + } + 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] = ' '; + switch (this.map[this.robot.y+n][this.robot.x]) { + case '\\': + this.lambdas--; + this.found_lambdas++; + break; + case 'L': + this._foundLift(); + break; + } + this.robot.y += n; + this.map[this.robot.y][this.robot.x] = 'R'; + break; + case 'A': + this._abort(); + return; + } + } + this.moves++; + + 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.meta['Water']) { + this.moves_below_water++; + if (this.moves_below_water > this.meta['Waterproof']) this._drown(); + } else { + this.moves_below_water = 0; + } + if (this.meta['Flooding'] > 0 && 0 == (this.moves % this.meta['Flooding'])) { + this.meta['Water']++; + } + + 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 = []; + for (k in this.meta) { + if (this.meta.hasOwnProperty(k)) keys.push(k); + } + keys.sort(); + for (k = 0; k < keys.length; ++k) { + lines[k] = keys[k] + " " + this.meta[keys[k]]; + } + return lines.join("\n"); + }; + + return Mine; +}(); + +if (typeof exports !== "undefined") { + exports.Mine = Mine; +} diff --git a/js/test.js b/js/test.js new file mode 100644 index 0000000..141ce64 --- /dev/null +++ b/js/test.js @@ -0,0 +1,9 @@ + +var Mine = require('./mine').Mine; +var maps = require('./maps').maps; + +console.log(maps); + +var m = new Mine(maps['flood1']); + +console.log(m);