|
<!DOCTYPE html> |
|
<html> |
|
|
|
<head> |
|
<title>Connect-4 game</title> |
|
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs/dist/tf.min.js"/> |
|
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm/dist/tf-backend-wasm.js"/> |
|
|
|
</head> |
|
|
|
<body> |
|
<p style="user-select: none; font-family: sans-serif;"> |
|
Try to connect four stones in a row, a column, or a diagonal. |
|
</p> |
|
<p><button type="button" style="user-select: none;" id="ai-first">AI goes first</button></p> |
|
<canvas id="game-board" width="600px" height="600px"></canvas> |
|
<script> |
|
function BoardGame(num_rows, num_cols) { |
|
this.num_cols = num_cols; |
|
this.num_rows = num_rows; |
|
var this_ = this; |
|
this.canvas_ctx = document.getElementById("game-board").getContext("2d"); |
|
this.board_scale = 30; |
|
document.getElementById("game-board").height = this.board_scale * (num_rows + 1); |
|
document.getElementById("game-board").width = this.board_scale * (num_cols + 1); |
|
this.canvas_ctx.scale(this.board_scale, this.board_scale); |
|
this.canvas_ctx.translate(1, 1); |
|
this.reset = function () { |
|
this.board = new Array(num_rows * num_cols); |
|
for (let i = 0; i < this.board.length; i++) this.board[i] = 0; |
|
this.mouse_x = -1; |
|
this.mouse_y = -1; |
|
this.who_play = 1; |
|
this.ai_player = -1; |
|
this.game_ended = false; |
|
}; |
|
this.reset(); |
|
this.render_text = function (text) { |
|
this.canvas_ctx.fillStyle = "black"; |
|
this.canvas_ctx.font = '1px monospace'; |
|
this.canvas_ctx.fillText(text, 1, 1); |
|
}; |
|
this.submit_board = async function (action_) { |
|
const obs = tf.tensor(this_.board, [this_.num_rows, this_.num_cols], 'float32'); |
|
const normalized_obs = tf.mul(obs, this_.ai_player); |
|
normalized_obs.print(); |
|
const [action_logits, value] = this_.agent.predict(normalized_obs); |
|
action_logits.print(); |
|
const action = await tf.argMax(action_logits).array(); |
|
return { |
|
"terminated": false, |
|
"action": action, |
|
}; |
|
}; |
|
this.end_game = function () { |
|
this.game_ended = true; |
|
setTimeout(function() {this_.reset();}, 3000); |
|
}; |
|
this.ai_play = function() { |
|
this_.submit_board().then( |
|
function (info) { |
|
document.body.style.cursor = 'default'; |
|
console.log(info); |
|
let x = info["action"]; |
|
if (x != -1) { |
|
let [_, y] = this_.get_candidate(x); |
|
let i = y * this_.num_cols + x; |
|
this_.board[i] = this_.who_play; |
|
this_.who_play = -this_.who_play; |
|
this_.render(); |
|
} |
|
if (info["terminated"] == true) { |
|
this_.end_game(); |
|
} |
|
} |
|
).catch(function (e) { }); |
|
}; |
|
document.getElementById("ai-first").onclick = function() { |
|
this_.reset(); |
|
this_.ai_player = 1; |
|
this_.ai_play(); |
|
}; |
|
document.getElementById("game-board").addEventListener('click', function (e) { |
|
var rect = this.getBoundingClientRect(); |
|
var x = e.clientX - rect.left; |
|
var y = e.clientY - rect.top; |
|
var loc_x = Math.floor(x / this_.board_scale - 0.5); |
|
var loc_y = Math.floor(y / this_.board_scale - 0.5); |
|
this_.mouse_x = loc_x; |
|
this_.mouse_y = this_.get_candidate(this_.mouse_x)[1]; |
|
|
|
if ( |
|
this_.mouse_x >= 0 && |
|
this_.mouse_y >= 0 && |
|
this_.mouse_x < this_.num_cols && |
|
this_.mouse_y < this_.num_rows && |
|
this_.game_ended == false |
|
) { |
|
if (this_.who_play == this_.ai_player) return false; |
|
let i = this_.mouse_y * this_.num_cols + this_.mouse_x; |
|
if (this_.board[i] != 0) return false; |
|
this_.board[i] = this_.who_play; |
|
this_.who_play = -this_.who_play; |
|
this_.render(); |
|
this_.ai_play(); |
|
} |
|
}, false); |
|
this.draw_stone = function (x, y, color) { |
|
let ctx = this.canvas_ctx; |
|
y = this.num_rows - 1 - y; |
|
ctx.beginPath(); |
|
ctx.arc(x, y, 0.40, 0, 2 * Math.PI, false); |
|
ctx.fillStyle = color; |
|
ctx.fill(); |
|
ctx.lineWidth = 0.02; |
|
ctx.strokeStyle = "black"; |
|
ctx.stroke(); |
|
}; |
|
this.get_candidate = function (x) { |
|
for (let i = 0; i < this.num_rows; i++) { |
|
let idx = i * this.num_cols + x; |
|
if (this.board[idx] == 0) return [x, i]; |
|
} |
|
return [-1, -1]; |
|
}; |
|
this.render = function () { |
|
let ctx = this.canvas_ctx; |
|
ctx.fillStyle = "#b8891c"; |
|
ctx.fillRect(-1, -1, num_cols + 1, num_rows + 1); |
|
ctx.fillStyle = "#b8891c"; |
|
ctx.fillRect(0, 0, num_cols - 1, num_rows - 1); |
|
ctx.lineWidth = 0.1 / 5; |
|
for (let i = 0; i < this.num_cols; i++) { |
|
ctx.beginPath(); |
|
ctx.moveTo(i, 0); |
|
ctx.lineTo(i, this.num_rows - 1); |
|
ctx.strokeStyle = "black"; |
|
ctx.stroke(); |
|
} |
|
for (let i = 0; i < this.num_rows; i++) { |
|
ctx.beginPath(); |
|
ctx.moveTo(0, i); |
|
ctx.lineTo(this.num_cols - 1, i); |
|
ctx.strokeStyle = "black"; |
|
ctx.stroke(); |
|
} |
|
for (let i = 0; i < this.board.length; i++) { |
|
let x = i % this.num_cols; |
|
let y = Math.floor(i / this.num_cols); |
|
if (this.board[i] == 0) continue; |
|
let color = (this.board[i] == 1) ? "#3a352d" : "#f0ece4"; |
|
this.draw_stone(x, y, color); |
|
} |
|
if ( |
|
this.mouse_x >= 0 && |
|
this.mouse_y >= 0 && |
|
this.mouse_x < this.num_cols && |
|
this.mouse_y < this.num_rows |
|
) { |
|
let [x, y] = this.get_candidate(this.mouse_x); |
|
if (x == -1) return; |
|
this.mouse_x = x; |
|
this.mouse_y = y; |
|
if (this.who_play == -1) |
|
this.draw_stone(x, y, "#bda051"); |
|
else |
|
this.draw_stone(x, y, "#6d6051"); |
|
} |
|
}; |
|
document.getElementById("game-board").onmousemove = function (e) { |
|
|
|
var rect = this.getBoundingClientRect(); |
|
var x = e.clientX - rect.left; |
|
var y = e.clientY - rect.top; |
|
var loc_x = Math.floor(x / this_.board_scale - 0.5); |
|
var loc_y = Math.floor(y / this_.board_scale - 0.5); |
|
this_.mouse_x = loc_x; |
|
this_.mouse_y = loc_y; |
|
setTimeout(function () { this_.render(); }); |
|
}; |
|
}; |
|
const modelUrl = 'policy/model.json'; |
|
const init_fn = async function() { |
|
const model = await tf.loadGraphModel(modelUrl); |
|
await tf.setBackend('wasm'); |
|
return model; |
|
}; |
|
document.addEventListener("DOMContentLoaded", function (event) { |
|
init_fn().then(function (agent) { |
|
game = new BoardGame(agent, 6, 7); |
|
game.render(); |
|
}); |
|
}); |
|
</script> |
|
</body> |
|
|
|
</html> |