Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions backend/cmd/server/static/js/canvas-game.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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: {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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++) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
}));
}
}
Expand Down
97 changes: 96 additions & 1 deletion backend/cmd/server/templates/game.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ <h1 class="game-title">2048</h1>
</div>
</div>

<!-- Game Mode Selector -->
<div class="game-mode-selector">
<div class="mode-buttons">
<button id="classic-mode-btn" class="mode-btn active" data-mode="classic">
<span class="mode-title">Classic</span>
<span class="mode-desc">Standard 2048 game</span>
</button>
<button id="challenge-mode-btn" class="mode-btn" data-mode="challenge">
<span class="mode-title">Challenge</span>
<span class="mode-desc">One cell disabled</span>
</button>
</div>
</div>

<!-- Game Board -->
<div class="game-board-container">
<canvas id="game-canvas" class="game-canvas"></canvas>
Expand Down Expand Up @@ -119,9 +133,27 @@ <h1 class="game-title">2048</h1>
// 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();
});
});
});
</script>

<style>
Expand Down Expand Up @@ -234,6 +266,51 @@ <h1 class="game-title">2048</h1>
background: #776e65;
}

.game-mode-selector {
margin-bottom: 20px;
}

.mode-buttons {
display: flex;
gap: 10px;
justify-content: center;
}

.mode-btn {
background: #f8f8f8;
border: 2px solid #ddd;
border-radius: 8px;
padding: 12px 20px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
}

.mode-btn:hover {
border-color: #8f7a66;
background: #f0f0f0;
}

.mode-btn.active {
background: #8f7a66;
border-color: #8f7a66;
color: white;
}

.mode-title {
font-weight: 600;
font-size: 1rem;
margin-bottom: 4px;
}

.mode-desc {
font-size: 0.8rem;
opacity: 0.8;
}

.game-board-container {
position: relative;
margin-bottom: 20px;
Expand Down Expand Up @@ -449,6 +526,24 @@ <h1 class="game-title">2048</h1>
padding: 6px 12px;
font-size: 0.8rem;
}

.mode-buttons {
flex-direction: column;
gap: 8px;
}

.mode-btn {
min-width: auto;
padding: 10px 15px;
}

.mode-title {
font-size: 1rem;
}

.mode-desc {
font-size: 0.75rem;
}
}
</style>
</body>
Expand Down
115 changes: 102 additions & 13 deletions backend/cmd/server/templates/leaderboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ <h1 class="page-title">🏆 2048 Leaderboards</h1>
</header>

<div class="leaderboard-container">
<!-- Game Mode Selection -->
<div class="game-mode-tabs">
<button class="mode-tab-btn active" data-mode="classic" onclick="switchGameMode('classic')">
<span class="mode-title">Classic</span>
<span class="mode-desc">Standard 2048</span>
</button>
<button class="mode-tab-btn" data-mode="challenge" onclick="switchGameMode('challenge')">
<span class="mode-title">Challenge</span>
<span class="mode-desc">One cell disabled</span>
</button>
</div>

<!-- Time Period Selection -->
<div class="leaderboard-tabs">
<button class="tab-btn active" data-type="daily" onclick="switchLeaderboard('daily')">Daily</button>
<button class="tab-btn" data-type="weekly" onclick="switchLeaderboard('weekly')">Weekly</button>
Expand All @@ -38,45 +51,64 @@ <h1 class="page-title">🏆 2048 Leaderboards</h1>

<script>
let currentType = 'daily';
let currentMode = 'classic';
let cache = new Map();

// Load initial leaderboard
loadLeaderboard('daily');

function switchGameMode(mode) {
// Update active mode tab
document.querySelectorAll('.mode-tab-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-mode="${mode}"]`).classList.add('active');

currentMode = mode;
// Clear cache when switching modes
cache.clear();

// Reload current leaderboard with new mode
loadLeaderboard(currentType);
}

function switchLeaderboard(type) {
// Update active tab
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-type="${type}"]`).classList.add('active');

// Load leaderboard
loadLeaderboard(type);
}

async function loadLeaderboard(type) {
currentType = type;


// Create cache key that includes both type and mode
const cacheKey = `${type}_${currentMode}`;

// Check cache first
if (cache.has(type)) {
displayLeaderboard(cache.get(type));
if (cache.has(cacheKey)) {
displayLeaderboard(cache.get(cacheKey));
return;
}

// Show loading state
showLoading();

try {
const response = await fetch(`/api/public/leaderboard?type=${type}&limit=50`);
const response = await fetch(`/api/public/leaderboard?type=${type}&game_mode=${currentMode}&limit=50`);
if (!response.ok) {
throw new Error('Failed to fetch leaderboard');
}

const data = await response.json();

// Cache the data
cache.set(type, data);
cache.set(cacheKey, data);

// Display the leaderboard
displayLeaderboard(data);
} catch (error) {
Expand Down Expand Up @@ -192,6 +224,53 @@ <h1 class="page-title">🏆 2048 Leaderboards</h1>
padding: 0 20px;
}

.game-mode-tabs {
display: flex;
gap: 15px;
margin-bottom: 20px;
justify-content: center;
}

.mode-tab-btn {
background: white;
border: 2px solid #ddd;
border-radius: 10px;
padding: 15px 25px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
min-width: 140px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.mode-tab-btn:hover {
border-color: #8f7a66;
background: #f8f8f8;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
}

.mode-tab-btn.active {
background: #8f7a66;
border-color: #8f7a66;
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}

.mode-title {
font-weight: 600;
font-size: 1.1rem;
margin-bottom: 4px;
}

.mode-desc {
font-size: 0.85rem;
opacity: 0.8;
}

.leaderboard-tabs {
display: flex;
background: white;
Expand Down Expand Up @@ -326,11 +405,21 @@ <h1 class="page-title">🏆 2048 Leaderboards</h1>
.page-title {
font-size: 2rem;
}


.game-mode-tabs {
flex-direction: column;
gap: 10px;
}

.mode-tab-btn {
min-width: auto;
padding: 12px 20px;
}

.leaderboard-tabs {
flex-wrap: wrap;
}

.tab-btn {
flex: 1 1 50%;
}
Expand Down
Loading