diff --git a/app.py b/app.py new file mode 100644 index 0000000..e250dba --- /dev/null +++ b/app.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# encoding: utf-8 +from flask import Flask, render_template, jsonify, request +import random + +app = Flask(__name__) + +def init(): + matrix = [0 for i in range(16)] + random_lst = random.sample(range(16), 2) + matrix[random_lst[0]] = matrix[random_lst[1]] = 2 + return matrix + +def move(matrix, direction): + mergedList = [] + if direction == 'w': + for i in range(16): + j = i + while j - 4 >= 0: + if matrix[j-4] == 0: + matrix[j-4] = matrix[j] + matrix[j] = 0 + elif matrix[j-4] == matrix[j] and j not in mergedList: + matrix[j-4] *= 2 + matrix[j] = 0 + mergedList.append(j-4) + mergedList.append(j) + j -= 4 + elif direction == 's': + for i in range(15, -1, -1): + j = i + while j + 4 < 16: + if matrix[j+4] == 0: + matrix[j+4] = matrix[j] + matrix[j] = 0 + elif matrix[j+4] == matrix[j] and j not in mergedList: + matrix[j+4] *= 2 + matrix[j] = 0 + mergedList.append(j) + mergedList.append(j+4) + j += 4 + elif direction == 'a': + for i in range(16): + j = i + while j % 4 != 0: + if matrix[j-1] == 0: + matrix[j-1] = matrix[j] + matrix[j] = 0 + elif matrix[j-1] == matrix[j] and j not in mergedList: + matrix[j-1] *= 2 + matrix[j] = 0 + mergedList.append(j-1) + mergedList.append(j) + j -= 1 + else: + for i in range(15, -1, -1): + j = i + while j % 4 != 3: + if matrix[j+1] == 0: + matrix[j+1] = matrix[j] + matrix[j] = 0 + elif matrix[j+1] == matrix[j] and j not in mergedList: + matrix[j+1] *= 2 + matrix[j] = 0 + mergedList.append(j) + mergedList.append(j+1) + j += 1 + return matrix + +def insert(matrix): + getZeroIndex = [] + for i in range(16): + if matrix[i] == 0: + getZeroIndex.append(i) + if not getZeroIndex: + return matrix + randomZeroIndex = random.choice(getZeroIndex) + max_num = max(matrix) + if max_num > 128: + matrix[randomZeroIndex] = random.choice([4, 8, 16, 32]) + else: + matrix[randomZeroIndex] = random.choice([4, 8]) + return matrix + +def isOver(matrix): + if 0 in matrix: + return False + else: + for i in range(16): + if i % 4 != 3: + if matrix[i] == matrix[i+1]: + return False + if i < 12: + if matrix[i] == matrix[i+4]: + return False + return True + +game_state = { + 'matrix': init(), + 'history': [], + 'score': 0 +} + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/new_game') +def new_game(): + game_state['matrix'] = init() + game_state['history'] = [] + game_state['score'] = 0 + return jsonify({ + 'matrix': game_state['matrix'], + 'score': game_state['score'], + 'game_over': False, + 'win': False + }) + +@app.route('/move/') +def handle_move(direction): + old_matrix = list(game_state['matrix']) + game_state['matrix'] = move(game_state['matrix'], direction) + + changed = game_state['matrix'] != old_matrix + + if changed: + game_state['history'].append(old_matrix) + game_state['matrix'] = insert(game_state['matrix']) + game_state['score'] = sum(game_state['matrix']) + + game_over = isOver(game_state['matrix']) + win = 2048 in game_state['matrix'] + + return jsonify({ + 'matrix': game_state['matrix'], + 'score': game_state['score'], + 'game_over': game_over, + 'win': win, + 'changed': changed + }) + +@app.route('/back') +def handle_back(): + if game_state['history']: + game_state['matrix'] = game_state['history'].pop() + game_state['score'] = sum(game_state['matrix']) + return jsonify({ + 'matrix': game_state['matrix'], + 'score': game_state['score'], + 'game_over': isOver(game_state['matrix']), + 'win': 2048 in game_state['matrix'] + }) + +@app.route('/state') +def get_state(): + return jsonify({ + 'matrix': game_state['matrix'], + 'score': game_state['score'], + 'game_over': isOver(game_state['matrix']), + 'win': 2048 in game_state['matrix'] + }) + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/static/game.js b/static/game.js new file mode 100644 index 0000000..6f8165b --- /dev/null +++ b/static/game.js @@ -0,0 +1,107 @@ +class Game2048 { + constructor() { + this.board = document.getElementById('game-board'); + this.scoreElement = document.getElementById('score'); + this.gameOverOverlay = document.getElementById('game-over'); + this.winOverlay = document.getElementById('win-overlay'); + this.setupEventListeners(); + this.init(); + } + + init() { + fetch('/new_game') + .then(response => response.json()) + .then(data => { + this.renderBoard(data.matrix); + this.updateScore(data.score); + this.hideOverlays(); + }); + } + + setupEventListeners() { + document.addEventListener('keydown', (e) => this.handleKeyPress(e)); + document.getElementById('new-game-btn').addEventListener('click', () => this.init()); + document.getElementById('restart-btn').addEventListener('click', () => this.init()); + document.getElementById('new-game-win-btn').addEventListener('click', () => this.init()); + document.getElementById('continue-btn').addEventListener('click', () => this.hideOverlays()); + document.getElementById('back-btn').addEventListener('click', () => this.undo()); + } + + handleKeyPress(e) { + const keyMap = { + 'ArrowUp': 'w', 'ArrowDown': 's', 'ArrowLeft': 'a', 'ArrowRight': 'd', + 'w': 'w', 'W': 'w', + 's': 's', 'S': 's', + 'a': 'a', 'A': 'a', + 'd': 'd', 'D': 'd' + }; + + if (keyMap[e.key]) { + e.preventDefault(); + this.move(keyMap[e.key]); + } else if (e.key === 'b' || e.key === 'B') { + e.preventDefault(); + this.undo(); + } + } + + move(direction) { + fetch(`/move/${direction}`) + .then(response => response.json()) + .then(data => { + this.renderBoard(data.matrix); + this.updateScore(data.score); + + if (data.win && !this.winShown) { + this.showWin(); + this.winShown = true; + } + + if (data.game_over) { + this.showGameOver(); + } + }); + } + + undo() { + fetch('/back') + .then(response => response.json()) + .then(data => { + this.renderBoard(data.matrix); + this.updateScore(data.score); + this.hideOverlays(); + }); + } + + renderBoard(matrix) { + this.board.innerHTML = ''; + matrix.forEach((value, index) => { + const tile = document.createElement('div'); + tile.className = `tile tile-${value > 2048 ? 'super' : value}`; + tile.textContent = value === 0 ? '' : value; + this.board.appendChild(tile); + }); + } + + updateScore(score) { + this.scoreElement.textContent = score; + } + + showGameOver() { + this.gameOverOverlay.classList.remove('hidden'); + } + + showWin() { + this.winOverlay.classList.remove('hidden'); + } + + hideOverlays() { + this.gameOverOverlay.classList.add('hidden'); + this.winOverlay.classList.add('hidden'); + this.winShown = false; + } +} + +document.addEventListener('DOMContentLoaded', () => { + new Game2048(); +}); diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..c2a3a2b --- /dev/null +++ b/static/style.css @@ -0,0 +1,229 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Clear Sans', 'Helvetica Neue', Arial, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; +} + +.container { + text-align: center; +} + +h1 { + color: #fff; + font-size: 4rem; + font-weight: bold; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + margin-bottom: 20px; +} + +.info-panel { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + margin-bottom: 20px; +} + +.score-box { + background: rgba(255, 255, 255, 0.2); + border-radius: 10px; + padding: 10px 25px; + display: flex; + flex-direction: column; + backdrop-filter: blur(10px); +} + +.score-box .label { + color: rgba(255, 255, 255, 0.8); + font-size: 0.8rem; + font-weight: bold; +} + +.score-box .value { + color: #fff; + font-size: 1.5rem; + font-weight: bold; +} + +.controls { + display: flex; + gap: 10px; +} + +.btn { + background: linear-gradient(145deg, #f39c12, #e67e22); + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + font-size: 1rem; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); +} + +.btn:active { + transform: translateY(0); +} + +.game-board { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; + background: rgba(255, 255, 255, 0.15); + padding: 15px; + border-radius: 15px; + backdrop-filter: blur(10px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +.tile { + width: 90px; + height: 90px; + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + font-weight: bold; + border-radius: 10px; + transition: all 0.15s ease; + animation: pop 0.2s ease; +} + +@keyframes pop { + 0% { transform: scale(0.8); opacity: 0; } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); opacity: 1; } +} + +.tile-0 { background: rgba(255, 255, 255, 0.1); } +.tile-2 { background: #eee4da; color: #776e65; } +.tile-4 { background: #ede0c8; color: #776e65; } +.tile-8 { background: #f2b179; color: #f9f6f2; } +.tile-16 { background: #f59563; color: #f9f6f2; } +.tile-32 { background: #f67c5f; color: #f9f6f2; } +.tile-64 { background: #f65e3b; color: #f9f6f2; } +.tile-128 { background: #edcf72; color: #f9f6f2; font-size: 1.8rem; } +.tile-256 { background: #edcc61; color: #f9f6f2; font-size: 1.8rem; } +.tile-512 { background: #edc850; color: #f9f6f2; font-size: 1.8rem; } +.tile-1024 { background: #edc53f; color: #f9f6f2; font-size: 1.5rem; } +.tile-2048 { background: #edc22e; color: #f9f6f2; font-size: 1.5rem; box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.5); } +.tile-super { background: #3c3a32; color: #f9f6f2; font-size: 1.2rem; } + +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 100; + backdrop-filter: blur(5px); +} + +.overlay.hidden { + display: none; +} + +.overlay-content { + background: white; + padding: 40px 60px; + border-radius: 15px; + text-align: center; + animation: slideIn 0.3s ease; +} + +.overlay-content.win { + background: linear-gradient(145deg, #edc22e, #f39c12); +} + +.overlay-content.win h2, +.overlay-content.win p { + color: white; +} + +@keyframes slideIn { + from { + transform: translateY(-50px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.overlay-content h2 { + font-size: 2rem; + margin-bottom: 20px; + color: #333; +} + +.overlay-content p { + margin-bottom: 20px; + color: #666; +} + +.overlay-content .btn { + margin: 5px; +} + +.instructions { + margin-top: 25px; + color: rgba(255, 255, 255, 0.9); + font-size: 0.95rem; +} + +.instructions p { + margin: 5px 0; +} + +.instructions strong { + background: rgba(255, 255, 255, 0.2); + padding: 2px 8px; + border-radius: 4px; +} + +@media (max-width: 500px) { + .tile { + width: 70px; + height: 70px; + font-size: 1.5rem; + } + + .tile-128, .tile-256, .tile-512 { + font-size: 1.3rem; + } + + .tile-1024, .tile-2048 { + font-size: 1.1rem; + } + + h1 { + font-size: 3rem; + } + + .info-panel { + flex-direction: column; + gap: 15px; + } +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a5b0548 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,44 @@ + + + + + + 2048 Game + + + +
+

2048

+
+
+ SCORE + 0 +
+
+ + +
+
+
+ + +
+

Use Arrow Keys or WASD to move tiles.

+

Press B to undo last move.

+
+
+ + +