From 57233a133b5c3f20edead79921c4d0b4f7d647b3 Mon Sep 17 00:00:00 2001 From: dbc Date: Sun, 8 Jun 2025 00:22:48 +0800 Subject: [PATCH] feat: Challenge mode --- backend/cmd/server/static/js/canvas-game.js | 40 +- backend/cmd/server/templates/game.html | 97 +++- backend/cmd/server/templates/leaderboard.html | 115 ++++- backend/internal/database/gorm.go | 49 ++ backend/internal/database/interface.go | 1 + backend/internal/database/postgres.go | 134 +++++- backend/internal/game/engine.go | 435 ++++++++++++++++-- backend/internal/handlers/leaderboard.go | 43 +- backend/internal/websocket/handlers.go | 106 ++++- backend/migrations/001_initial_schema.sql | 86 ++-- backend/pkg/models/game.go | 89 +++- backend/pkg/models/gorm_models.go | 83 +++- 12 files changed, 1097 insertions(+), 181 deletions(-) diff --git a/backend/cmd/server/static/js/canvas-game.js b/backend/cmd/server/static/js/canvas-game.js index c9b78a3..8e1b6c5 100644 --- a/backend/cmd/server/static/js/canvas-game.js +++ b/backend/cmd/server/static/js/canvas-game.js @@ -9,6 +9,8 @@ class CanvasGame { this.score = 0; this.victory = false; this.gameOver = false; + this.gameMode = 'classic'; + this.disabledCell = null; // Canvas settings this.setupCanvas(); @@ -17,6 +19,7 @@ class CanvasGame { this.colors = { background: '#faf8ef', empty: 'rgba(238, 228, 218, 0.35)', + disabled: 'rgba(119, 110, 101, 0.3)', text: '#776e65', textLight: '#f9f6f2', tiles: { @@ -193,6 +196,8 @@ class CanvasGame { this.score = gameState.score; this.victory = gameState.victory; this.gameOver = gameState.game_over; + this.gameMode = gameState.game_mode || 'classic'; + this.disabledCell = gameState.disabled_cell || null; // Update score display const scoreElement = document.getElementById('score'); @@ -311,6 +316,11 @@ class CanvasGame { } } + // Draw disabled cell overlay if in challenge mode + if (this.gameMode === 'challenge' && this.disabledCell) { + this.drawDisabledCell(this.disabledCell.row, this.disabledCell.col); + } + // Draw tiles with values (skip animated tiles) for (let row = 0; row < 4; row++) { for (let col = 0; col < 4; col++) { @@ -402,6 +412,30 @@ class CanvasGame { this.ctx.fill(); } + drawDisabledCell(row, col) { + const x = this.padding + col * (this.tileSize + this.gap); + const y = this.padding + row * (this.tileSize + this.gap); + + // Draw disabled cell overlay + this.ctx.fillStyle = this.colors.disabled; + const borderRadius = Math.min(this.tileSize * 0.1, 6); + this.drawRoundedRect(x, y, this.tileSize, this.tileSize, borderRadius); + this.ctx.fill(); + + // Draw X pattern to indicate disabled + this.ctx.strokeStyle = '#776e65'; + this.ctx.lineWidth = 3; + this.ctx.lineCap = 'round'; + + const margin = this.tileSize * 0.25; + this.ctx.beginPath(); + this.ctx.moveTo(x + margin, y + margin); + this.ctx.lineTo(x + this.tileSize - margin, y + this.tileSize - margin); + this.ctx.moveTo(x + this.tileSize - margin, y + margin); + this.ctx.lineTo(x + margin, y + this.tileSize - margin); + this.ctx.stroke(); + } + drawTile(row, col, value, scale = 1, opacity = 1, offsetX = 0, offsetY = 0) { const x = this.padding + col * (this.tileSize + this.gap) + offsetX; const y = this.padding + row * (this.tileSize + this.gap) + offsetY; @@ -488,12 +522,14 @@ class CanvasGame { } } - newGame() { + newGame(gameMode = 'classic') { this.hideGameOverlay(); if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'new_game', - data: {} + data: { + game_mode: gameMode + } })); } } diff --git a/backend/cmd/server/templates/game.html b/backend/cmd/server/templates/game.html index 2678ec8..d0be1a8 100644 --- a/backend/cmd/server/templates/game.html +++ b/backend/cmd/server/templates/game.html @@ -42,6 +42,20 @@

2048

+ +
+
+ + +
+
+
@@ -119,9 +133,27 @@

2048

// Global functions for UI function startNewGame() { if (window.canvasGame) { - window.canvasGame.newGame(); + const selectedMode = document.querySelector('.mode-btn.active').dataset.mode; + window.canvasGame.newGame(selectedMode); } } + + // Game mode selection + document.addEventListener('DOMContentLoaded', function() { + const modeButtons = document.querySelectorAll('.mode-btn'); + + modeButtons.forEach(button => { + button.addEventListener('click', function() { + // Remove active class from all buttons + modeButtons.forEach(btn => btn.classList.remove('active')); + // Add active class to clicked button + this.classList.add('active'); + + // Start new game with selected mode + startNewGame(); + }); + }); + }); diff --git a/backend/cmd/server/templates/leaderboard.html b/backend/cmd/server/templates/leaderboard.html index 3f04d87..332b3ed 100644 --- a/backend/cmd/server/templates/leaderboard.html +++ b/backend/cmd/server/templates/leaderboard.html @@ -23,6 +23,19 @@

🏆 2048 Leaderboards

+ +
+ + +
+ +
@@ -38,45 +51,64 @@

🏆 2048 Leaderboards