Connect-4-Game / index.html
kimhyunwoo's picture
Update index.html
fc163a4 verified
<!DOCTYPE html>
<html>
<head>
<title>Connect-4</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<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>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background-color: #f5f5f5; /* Very Light Gray */
overflow: hidden;
}
#game-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 600px;
}
#game-title {
font-size: 2em;
font-weight: bold;
margin-bottom: 0.5em;
text-align: center;
color: #444; /* Darker Gray */
user-select: none;
}
#game-instructions {
text-align: center;
margin-bottom: 1em;
color: #666; /* Medium Gray */
user-select: none;
}
#game-board {
border-radius: 8px; /* Slightly Rounded Corners */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Very Subtle Shadow */
border: 1px solid #ddd; /* Light Gray Border */
background-color: #fff; /* White Background */
touch-action: none;
}
#ai-first {
margin-top: 1em;
padding: 10px 20px;
font-size: 1em;
border: 1px solid #ccc; /* Light Gray Border */
border-radius: 5px;
background-color: #f9f9f9; /* Almost White */
color: #555; /* Dark Gray Text */
cursor: pointer;
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
user-select: none;
}
#ai-first:hover {
background-color: #eee; /* Slightly Darker on Hover */
border-color: #bbb; /* Darker Border on Hover */
color: #333;
}
#ai-first:active {
transform: translateY(1px);
background-color: #ddd;
}
/* Loading indicator */
#loading-indicator {
display: none;
margin-top: 20px;
border: 6px solid #f3f3f3;
border-top: 6px solid #999; /* Medium Gray */
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite; /* Faster Spin */
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 600px) {
#game-title {
font-size: 1.5em;
}
#game-instructions {
font-size: 0.9em;
}
}
</style>
</head>
<body>
<div id="game-container">
<h1 id="game-title">Connect Four</h1>
<p id="game-instructions">Try to connect four stones in a row, a column, or a diagonal.</p>
<canvas id="game-board"></canvas>
<button type="button" id="ai-first">AI goes first</button>
<div id="loading-indicator"></div>
</div>
<script>
// ... (rest of your JavaScript code, with changes below) ...
function BoardGame(agent, num_rows, num_cols) {
this.agent = agent;
this.audio = new Audio('/stone.ogg'); // Consider a more subtle sound.
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 = 1; // Initial scale
this.calculateBoardScale = function() {
const containerWidth = document.getElementById("game-container").offsetWidth;
const containerHeight = window.innerHeight - document.getElementById("game-instructions").offsetHeight
- document.getElementById("ai-first").offsetHeight - 80;
const canvasWidth = containerWidth * 0.95; // Use 95% of container width
const canvasHeight = canvasWidth * (num_rows + 1) / (num_cols + 1);
if(canvasHeight > containerHeight) {
const adjustedCanvasHeight = containerHeight * 0.9;
const adjustedCanvasWidth = adjustedCanvasHeight * (num_cols+1) / (num_rows+1);
this.board_scale = adjustedCanvasHeight / (num_rows + 1);
this_.canvas_ctx.canvas.width = adjustedCanvasWidth;
this_.canvas_ctx.canvas.height = adjustedCanvasHeight;
}
else{
this.board_scale = canvasWidth / (num_cols + 1);
this_.canvas_ctx.canvas.width = canvasWidth;
this_.canvas_ctx.canvas.height = canvasHeight;
}
this_.canvas_ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
this_.canvas_ctx.scale(this.board_scale, this.board_scale);
this_.canvas_ctx.translate(0.5, 0.5);
}
this.calculateBoardScale();
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 () {
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++) {
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 () {
document.getElementById("loading-indicator").style.display = "block";
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.execute(normalized_obs, ["output_0", "output_1"]);
const action = await tf.argMax(action_logits).array();
document.getElementById("loading-indicator").style.display = "none";
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) {
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) {
console.error("AI play error:", e);
});
};
document.getElementById("ai-first").onclick = function () {
this_.reset();
this_.ai_player = 1;
this_.ai_play();
};
this.handleClick = function(x, y) {
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();
}
}
document.getElementById("game-board").addEventListener('touchstart', function(e) {
e.preventDefault();
var rect = this.getBoundingClientRect();
var touch = e.touches[0];
var x = touch.clientX - rect.left;
var y = touch.clientY - rect.top;
this_.handleClick(x, y);
}, false);
document.getElementById("game-board").addEventListener('click', function (e) {
var rect = this.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
this_.handleClick(x,y);
}, 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 = "rgba(0, 0, 0, 0.1)"; // Very Light Border
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.clearRect(-1, -1, num_cols + 1, num_rows + 1);
ctx.fillStyle = "#fff"; // White board
ctx.fillRect(-0.5, -0.5, num_cols, num_rows);
ctx.lineWidth = 0.1 / 2;
ctx.strokeStyle = "rgba(0, 0, 0, 0.1)"; // Very subtle grid lines
for (let i = 0; i < this.num_cols; i++) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, this.num_rows-1);
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.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) ? "#555" : "#bbb"; // Dark Gray and Light Gray
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;
let previewColor = (this.who_play == -1) ? "rgba(187, 187, 187, 0.5)" : "rgba(85, 85, 85, 0.5)"; // Semi-transparent grays
this.draw_stone(x, y, previewColor);
}
};
this.handleMove = function(e) {
let rect = this.canvas_ctx.canvas.getBoundingClientRect();
let x, y;
if (e.type === 'mousemove') {
x = e.clientX - rect.left;
y = e.clientY - rect.top;
} else if (e.type === 'touchmove') {
e.preventDefault();
if (e.touches.length > 0) {
x = e.touches[0].clientX - rect.left;
y = e.touches[0].clientY - rect.top;
} else {
return;
}
} else {
return;
}
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;
this_.render();
}
document.getElementById("game-board").onmousemove = function (e) {
this_.handleMove(e);
};
document.getElementById("game-board").addEventListener('touchmove', function(e) {
this_.handleMove(e);
}, false);
window.addEventListener('resize', function() {
this_.calculateBoardScale();
this_.render();
});
};
const modelUrl = '/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();
}).catch(error => {
console.error("Error loading model:", error);
document.getElementById("game-instructions").textContent = "Failed to load the AI. Please try refreshing the page.";
document.getElementById("ai-first").disabled = true;
});
});
</script>
</body>
</html>