Connect-4-Game / index.html
ntt123's picture
Update index.html
23a091d
raw
history blame
8.53 kB
<!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) {
// important: correct mouse position:
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>