Spaces:
Running
Running
| <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> |