Spaces:
Running
Running
<html> | |
<head> | |
<title>Connect-4 game</title> | |
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs/dist/tf.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm/dist/tf-backend-wasm.js"></script> | |
</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(agent, num_rows, num_cols) { | |
this.agent = agent; | |
this.audio = new Audio('/static/ntt123/Connect-4-Game/stone.ogg'); | |
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.get = function (row, col) { | |
return this.board[this.num_cols * row + col]; | |
} | |
this.is_terminated = function () { | |
// check if the game is terminated | |
if (this.board.some((x) => x == 0) == false) return true; | |
for (let i = 0; i < this.num_rows; i++) { | |
for (let j = 0; j < this.num_cols; j++) { | |
// check winner at cell i, j | |
var p = this.get(i, j); | |
if (p == 0) continue; | |
for (let [dx, dy] of [[1, 0], [0, 1], [1, 1], [-1, 1]]) { | |
var count = 0; | |
for (let k = 0; k < 4; k++) { | |
const u = i + dx * k; | |
const v = j + dy * k; | |
if (u < 0 || u >= this.num_rows) break; | |
if (v < 0 || v >= this.num_cols) break; | |
if (this.get(u, v) != p) break; | |
count = count + 1; | |
} | |
if (count >= 4) return true; | |
} | |
} | |
} | |
return false; | |
}; | |
this.submit_board = async function () { | |
await new Promise(r => setTimeout(r, 1000)); | |
if (this_.is_terminated()) return { "terminated": true, "action": -1 }; | |
const obs = tf.tensor(this_.board, [this_.num_rows, this_.num_cols], 'float32'); | |
const normalized_obs = tf.mul(obs, this_.ai_player); | |
const [action_logits, value] = this_.agent.predict(normalized_obs); | |
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(); this_.render(); }, 3000); | |
}; | |
this.ai_play = function () { | |
this_.submit_board().then( | |
function (info) { | |
document.body.style.cursor = 'default'; | |
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_.audio.play(); | |
this_.who_play = -this_.who_play; | |
this_.render(); | |
} | |
if (this_.is_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_.audio.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 = '/static/ntt123/Connect-4-Game/model.json'; | |
const init_fn = async function () { | |
await tf.setBackend('wasm'); | |
const model = await tf.loadGraphModel(modelUrl); | |
return model; | |
}; | |
document.addEventListener("DOMContentLoaded", function (event) { | |
init_fn().then(function (agent) { | |
game = new BoardGame(agent, 6, 7); | |
game.render(); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |