Reorganise code. Warn about uninitialised AI.
<!DOCTYPE html>
<html>
<head>
<title>2048 AI</title>
<meta name="viewport" content="width=device-width; initial-scale=1.0"/>
<meta charset="utf-8"/>
<script src="rule.js"></script>
<script src="board.js"></script>
<script src="perf.js"></script>
<script src="ai.js"></script>
<style>
body {
font-size: 11pt;
}
h1, div.area {
text-align: center;
margin: 10px auto;
}
#board {
margin: 10px auto;
}
#board td {
width: 40px;
height: 40px;
border: 1px solid red;
margin: 0;
text-align: center;
}
div.ai-control, div.move-control, div.score-conrol {
border: 1px red solid;
display: inline-block;
margin: 10px 0 2px 0;
background-color: cornsilk;
}
div.move-control, div.score-conrol {
padding: 3px;
}
div.move-control button.suggestion {
background-color: cyan;
}
div.ai-control {
padding: 2px 2px 2px 1em;
}
div.ai-control:before {
content: "AI: ";
margin-left: -.5em;
padding: 0;
}
.report > .name {
font-weight: bold;
}
div.ai {
border: 1px red solid;
margin-top: 20px;
padding: 2px;
position: relative;
background-color: cornsilk;
}
div.ai.enabled {
background-color: wheat;
}
div.ai > h5 {
margin: 2px 0 2px 6em;
}
button.ai {
position: absolute;
left: 1em;
top: -1em;
}
div.ai > div.option {
display: inline-block;
float: left;
margin: 1px 4px;
padding: 2px;
border: 1px solid tan;
border-radius: 4px;
}
div.ai > div.option > input.int {
text-align: right;
max-width: 4em;
margin-rght: 2px;
}
.clearfix {
clear: both;
}
#test {
display: none;
}
</style>
</head>
<body>
<h1>2048</h1>
<div id="score-area" class="area">
<div class="score-conrol">
Score: <span id="score">0</span>, Max: <span id="max">0</span>, Turn: <span id="turn">0</span>, Speed: <span id="speed">0</span> t/s
</div>
</div>
<div id="message-area" class="area"></div>
<table id="board">
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
<div id="control-area" class="area">
<div>
<button id="start">Start</button>
<span><input type="checkbox" id="2048" checked> 2048</span>
</div>
<div class="ai-control">
<button id="suggest">Suggest</button>
<button id="step">Step</button>
<button id="finish">Finish</button>
</div>
<div class="clearfix"></div>
<div class="move-control">
<table>
<tr>
<td></td><td><button id="up">up</button></td><td></td>
</tr>
<tr>
<td><button id="left">left</button></td><td><button id="down">down</button></td><td><button id="right">right</button></td>
</tr>
</table>
<button id="test">test</button>
</div>
<h1>AIs</h1>
<div>
<div class="ai" id="ai-blind-random">
<button class="ai">enable</button>
<h5>bling random</h5>
</div>
<div class="ai" id="ai-blind-weight-random">
<button class="ai">enable</button>
<h5>bling weight random</h5>
<div class="option">
<input type="text" name="left" class="int" pattern="[0-9]*" value="1"/> left weight
</div>
<div class="option">
<input type="text" name="right" class="int" pattern="[0-9]*" value="1"/> right weight
</div>
<div class="option">
<input type="text" name="up" class="int" pattern="[0-9]*" value="1"/> up weight
</div>
<div class="option">
<input type="text" name="down" class="int" pattern="[0-9]*" value="1"/> down weight
</div>
<div class="clearfix"></div>
</div>
<div class="ai" id="ai-blind-cycle">
<button class="ai">enable</button>
<h5>blind cycle</h5>
<div class="option">
<input type="checkbox" name="clockwise"/> clockwise
</div>
<div class="option">
<input type="checkbox" name="whilePossible"/> max move in one direction
</div>
<div class="clearfix"></div>
</div>
<div class="ai" id="ai-next-max-value">
<button class="ai">enable</button>
<h5>next merge makes max value</h5>
</div>
<div class="ai" id="ai-deep-max-score">
<button class="ai">enable</button>
<h5>deep merges without simulation make max score</h5>
</div>
<div class="ai" id="ai-deep-max-score-corner">
<button class="ai">enable</button>
<h5>deep merges without simulation make max score + bonus if max value at corner/edge</h5>
</div>
<div class="ai" id="ai-expectimax">
<button class="ai">enable</button>
<h5>expectimax</h5>
</div>
</div>
</div>
<div id="report-area" class="area">
<h1>Reports</h1>
<div class="report" class="area">
<div class="name">next max score</div>
<table>
<tr>
<th>Max</th>
<th>Count</th>
<th>%</th>
</tr>
<tr>
<td>128</td><td>8</td><td><div style="background: red; width: 100px">x</div></td>
</tr>
<tr>
<td>256</td><td>4</td><td><div style="background: red; width: 50px">x</div></td>
</tr>
<tr>
<td>512</td><td>12</td><td><div style="background: red; width: 150px">x</div></td>
</tr>
</table>
</div>
</div>
<script>
"use strict";
var boardDom = document.getElementById("board");
var ui = {};
ui.board = {};
ui.board.val2048Dom = document.getElementById('2048');
ui.board.val2048Dom.addEventListener("click", function(event) {
ui.board.update(board.current);
event.target.blur();
});
ui.board.val2048 = function(val) {
if (ui.board.val2048Dom.checked)
return Math.pow(2, val);
return val;
}
/* 'val' typesafe. */
ui.board.set = function(i, j, val) {
if (typeof val !== 'number')
val = 0;
var dom = boardDom.querySelectorAll("tr")[i].querySelectorAll("td")[j];
if (0 < val && val < 15) {
dom.innerHTML = ui.board.val2048(val);
var rgb = 'rgb(255,'+ (255-5*val-2*(val%2)) +','+ (255-10*val-5*((val-1)%2)) +')';
} else {
dom.innerHTML = '';
var rgb = 'white';
}
dom.style.backgroundColor = rgb;
}
ui.board.update = function(brd) {
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
ui.board.set(i, j, brd[i][j]);
}
}
}
ui.board.handleEdit = function(tdDom, i, j) {
if (!tdDom.firstChild || tdDom.firstChild.nodeType === Node.TEXT_NODE) {
if (tdDom.firstChild)
tdDom.removeChild(tdDom.firstChild);
var inputDom = document.createElement('input');
inputDom.value = board.current[i][j];
inputDom.style.width = '1em';
tdDom.appendChild(inputDom);
inputDom.focus();
inputDom.select();
inputDom.addEventListener("blur", function(tdDom, i, j) {
return function(event) {
ui.board.handleEdit(tdDom, i, j);
return false;
}
}(tdDom, i, j));
} else {
var inputDom = tdDom.firstChild;
var val = parseInt(inputDom.value);
if (0 <= val && val < 15) {
board.current[i][j] = val;
ui.board.set(i, j, val);
} else {
ui.board.set(i, j, board.current[i][j]);
}
}
}
ui.board.enableEdit = function() {
var trDoms = boardDom.querySelectorAll("tr");
for (var i = 0; i < 4; i++) {
var tdDoms = trDoms[i].querySelectorAll("td");
for (var j = 0; j < 4; j++) {
var tdDom = tdDoms[j];
tdDom.addEventListener("click", function(tdDom, i, j) {
return function(event) {
ui.board.handleEdit(tdDom, i, j);
return false;
}
}(tdDom, i, j));
tdDom.addEventListener("keyup", function(tdDom, i, j) {
return function(event) {
if (event.keyCode === 13)
ui.board.handleEdit(tdDom, i, j);
}
}(tdDom, i, j));
}
}
}
ui.board.enableEdit();
////////////////////////////////////////////////////////////////
// Score statistic.
ui.score = {};
var scoreDom = document.getElementById("score");
var maxDom = document.getElementById("max");
var speedDom = document.getElementById("speed");
var turnDom = document.getElementById("turn");
ui.score.clear = function(brd) {
scoreDom.innerHTML = '0';
maxDom.innerHTML = '0';
speedDom.innerHTML = '0';
turnDom.innerHTML = '0';
}
ui.score.update = function(brd) {
var score = board.score(brd);
scoreDom.innerHTML = '' + score.score;
maxDom.innerHTML = '' + score.max;
}
ui.score.speed = function(speed, turn) {
speedDom.innerHTML = '' + speed;
turnDom.innerHTML = '' + turn;
}
////////////////////////////////////////////////////////////////
// Suggestion UI.
ui.suggestion = {};
ui.suggestion.btns = document.querySelectorAll('div.move-control button');
ui.suggestion.clear = function() {
for (var i = 0; i < ui.suggestion.btns.length; i++)
ui.suggestion.btns[i].classList.remove('suggestion');
}
ui.suggestion.set = function(dir) {
document.getElementById(dir).classList.add('suggestion');
}
////////////////////////////////////////////////////////////////
// Game flow.
ui.game = {};
ui.game.dirs = ["up", "right", "down", "left"];
var messageDom = document.getElementById("message-area");
ui.game.clearMessage = function() {
messageDom.innerHTML = "";
}
ui.game.setMessage = function(msg) {
messageDom.innerHTML = msg;
}
ui.game.checkGameOver = function() {
if (board.gameOver(board.current)) {
ui.game.setMessage("Game over!");
return true;
} else {
return false;
}
}
ui.game.checkMoveValid = function(move) {
var tmpBrd = board.create();
board.copy(board.current, tmpBrd);
if (ui.game.dirs.indexOf(move) === -1) {
ui.game.setMessage("AI can't find move!");
return false;
}
if ( ! board.move[move].call(null, tmpBrd)) {
ui.game.setMessage("AI move "+move+" is ivalid!");
return false;
}
return true;
}
ui.game.beginStep = function() {
ui.suggestion.clear();
ui.game.clearMessage();
if (ui.game.checkGameOver())
return false;
return true;
}
ui.game.finishStep = function() {
board.putRandom(board.current);
ui.board.update(board.current);
ui.score.update(board.current);
localStorage.savedBoard = board.current;
}
////////////////////////////////////////////////////////////////
// Actions.
ui.action = {};
ui.action.start = function() {
ui.score.clear();
ui.suggestion.clear();
ui.game.clearMessage();
board.current = board.create();
board.putRandom(board.current);
ui.board.update(board.current);
}
document.getElementById("start").addEventListener("click", ui.action.start);
for (var i = 0; i < ui.game.dirs.length; i++) {
var dir = ui.game.dirs[i];
ui.action[dir] = function(dir) {
return function() {
if (ui.game.checkGameOver())
return;
ui.suggestion.clear();
var updated = board.move[dir](board.current);
if (updated) {
ui.game.finishStep();
ui.ai.current && ui.ai.current.cleanup();
}
}
} (dir);
document.getElementById(dir).addEventListener("click", ui.action[dir]);
}
document.body.addEventListener("keydown", function(event) {
if (document.activeElement.value)
return false;
var key = event.keyCode || event.which;
switch (key) {
case 38: ui.action.up(); break;
case 40: ui.action.down(); break;
case 37: ui.action.left(); break;
case 39: ui.action.right(); break;
}
event.preventDefault();
return false;
});
document.getElementById("test").addEventListener("click", function() {
board.move.replaceByBoardJS(ui.brdEngine);
});
ui.action.suggest = function() {
if ( ! ui.ai.current) {
ui.game.setMessage('Select AI!');
return;
}
if ( ! ui.game.beginStep())
return;
var tmpBrd = board.create();
board.copy(board.current, tmpBrd);
var move = ui.ai.current.analyse(tmpBrd);
ui.ai.current.cleanup();
if ( ! ui.game.checkMoveValid(move))
return;
ui.suggestion.set(move);
}
document.getElementById("suggest").addEventListener("click", ui.action.suggest);
ui.action.step = function() {
if ( ! ui.ai.current) {
ui.game.setMessage('Select AI!');
return;
}
if ( ! ui.game.beginStep())
return;
var tmpBrd = board.create();
board.copy(board.current, tmpBrd);
var move = ui.ai.current.analyse(tmpBrd);
ui.ai.current.cleanup();
if ( ! ui.game.checkMoveValid(move))
return;
board.move[move].call(null, board.current);
ui.game.finishStep();
}
document.getElementById("step").addEventListener("click", ui.action.step);
ui.action.finish = function() {
if ( ! ui.ai.current) {
ui.game.setMessage('Select AI!');
return;
}
ui.game.beginStep();
var step = 0;
var tsFrom = new Date().getTime();
while (!board.gameOver(board.current)) {
var tmpBrd = board.create();
board.copy(board.current, tmpBrd);
var move = ui.ai.current.analyse(tmpBrd);
if (typeof move === 'undefined') {
ui.game.setMessage("I don't know how to move!");
return;
}
var updated = board.move[move].call(null, board.current);
if (updated) {
board.putRandom(board.current);
} else {
ui.game.finishStep();
ui.game.setMessage("Wrong move!");
return;
}
step++;
}
var tsTo = new Date().getTime();
ui.game.finishStep();
ui.score.speed(step*1000.0/(tsTo-tsFrom), step);
ui.game.setMessage("Game over!");
ui.ai.current && ui.ai.current.cleanup();
}
document.getElementById("finish").addEventListener("click", ui.action.finish);
////////////////////////////////////////////////////////////////
// Register AIs.
ui.ai = {};
ui.ai.current = null;
ui.ai.algList = {
"ai-blind-random": function() {
return new ai.blindRandom(ui.brdEngine);
},
"ai-blind-weight-random": function(aiDom) {
var cfg = {};
cfg.left = aiDom.querySelectorAll("input[name='left']")[0].value;
cfg.right = aiDom.querySelectorAll("input[name='right']")[0].value;
cfg.up = aiDom.querySelectorAll("input[name='up']")[0].value;
cfg.down = aiDom.querySelectorAll("input[name='down']")[0].value;
return new ai.blindWeightRandom(ui.brdEngine);
},
"ai-blind-cycle": function(aiDom) {
var cfg = {};
cfg.clockwise = aiDom.querySelectorAll("input[name='clockwise']")[0].checked;
cfg.whilePossible = aiDom.querySelectorAll("input[name='whilePossible']")[0].checked;
return new ai.blindCycle(ui.brdEngine, cfg);
},
"ai-next-max-score": function() {
return new ai.nextMaxScore(ui.brdEngine);
},
"ai-next-max-value": function() {
return new ai.nextMaxValue(ui.brdEngine);
},
"ai-deep-max-score": function() {
return new ai.deepMaxScore(ui.brdEngine);
},
"ai-deep-max-score-corner": function() {
return new ai.deepMaxScoreCorner(ui.brdEngine);
},
"ai-expectimax": function() {
return new ai.expectimax(ui.brdEngine);
},
// "": function() {
// return new ai.(ui.brdEngine);
// },
};
ui.ai.domList = document.querySelectorAll('div.ai');
for (var i = 0; i < ui.ai.domList.length; i++) {
ui.ai.domList[i].querySelectorAll('button.ai')[0].addEventListener("click", function (event) {
ui.ai.enable(event.target.parentNode);
});
}
ui.ai.moveToTop = function(aiDom) {
for (var i = 0; i < ui.ai.domList.length; i++) {
ui.ai.domList[i].classList.remove('enabled');
}
var rootDom = aiDom.parentNode;
rootDom.removeChild(aiDom);
rootDom.insertBefore(aiDom, rootDom.firstChild);
aiDom.classList.add('enabled');
}
ui.ai.enable = function(aiDom) {
if (ui.ai.current)
ui.ai.current.cleanup();
ui.ai.moveToTop(aiDom);
ui.ai.current = ui.ai.algList[aiDom.id](aiDom);
}
ui.brdEngine = BoardArr2d; // TODO make user selectable
if (localStorage.savedBoard) {
board.current = localStorage.savedBoard;
ui.board.update(board.current);
} else {
ui.action.start();
}
</script>
</body>
</html>