diff --git a/.gitignore b/.gitignore index c2eabec..cca1ed7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .venv -.env \ No newline at end of file +.env +db.sqlite +flask_session \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..145aa9e --- /dev/null +++ b/dockerfile @@ -0,0 +1,25 @@ +FROM python:alpine + +# Set the working directory +WORKDIR /app + +# Copy the requirements file into the container +COPY requirements.txt . + +# Install the required packages +RUN pip install --no-cache-dir -r requirements.txt +RUN pip install gunicorn + +# Copy the rest of the application code into the container +COPY src src +COPY templates templates +COPY static static + +# Expose the port the app runs on +EXPOSE 5000 + +# Set environment variables +ENV FLASK_APP=main.py + +# run the application +ENTRYPOINT [ "gunicorn", "-b", ":5000", "--access-logfile", "-", "--error-logfile", "-", "src.main:app" ] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f87e99e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +python-dotenv +flask-session +requests +flask \ No newline at end of file diff --git a/src/__pycache__/database.cpython-313.pyc b/src/__pycache__/database.cpython-313.pyc index 1a92a2f..7d2b662 100644 Binary files a/src/__pycache__/database.cpython-313.pyc and b/src/__pycache__/database.cpython-313.pyc differ diff --git a/src/database.py b/src/database.py index 0a0eef5..5b03fdc 100644 --- a/src/database.py +++ b/src/database.py @@ -18,6 +18,8 @@ class Database: def insert_snake(self, name, score): old_score = self.get_snake_score(name) + print(f"Old score for {name}: {old_score}") + print(f"New score for {name}: {score}") if old_score is not None and score <= old_score: return @@ -28,7 +30,7 @@ class Database: self.connection.commit() def get_snake_score(self, name): - self.cursor.execute('SELECT score FROM snake WHERE name = ?', (name,)) + self.cursor.execute('SELECT score FROM snake WHERE name = ? ORDER BY score DESC LIMIT 1', (name,)) result = self.cursor.fetchone() return result[0] if result else None diff --git a/src/main.py b/src/main.py index 09ed11e..5c298b2 100644 --- a/src/main.py +++ b/src/main.py @@ -1,8 +1,12 @@ -from flask import Flask, request, render_template +from flask import Flask, request, render_template, send_from_directory from flask_session import Session from dotenv import load_dotenv from os import getenv as env -import logging, database, requests +import logging, requests +try: + import src.database as database +except ImportError: + import database logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') @@ -11,8 +15,9 @@ load_dotenv() app = Flask( __name__, - template_folder=env('TEMPLATE_FOLDER', default='templates'), - static_folder=env('STATIC_FOLDER', default='static'), + template_folder=env('TEMPLATE_FOLDER', default='../templates'), + static_folder=env('STATIC_FOLDER', default='../static'), + static_url_path=env('STATIC_URL_PATH', default='/static') ) app.config["SESSION_PERMANENT"] = True app.config["SESSION_TYPE"] = "filesystem" @@ -28,48 +33,73 @@ def index(): return render_template('index.html') -@app.route('/snake/submit', methods=['POST']) +@app.route('/robots.txt') +@app.route('/sitemap.xml') +@app.route('/favicon.ico') +def web_stuffs(): + return send_from_directory( + app.static_folder, + request.path[1:], + ) + + +@app.route('/404') +@app.errorhandler(404) +def not_found(): + unformatted_scores = db.get_snake_scores() + scores = [{'position': i + 1, 'name': score[1], 'score': score[2]} for i, score in enumerate(unformatted_scores)] + return render_template('404.html', scores=scores) + + +@app.route('/404/submit', methods=['POST']) def snake_submit(): - data = request.json - if not data or 'name' not in data or 'score' not in data: - logging.error("Invalid data received: %s", data) - return {'error': 'Invalid data'}, 400 + unformatted_scores = db.get_snake_scores() + scores = [{'position': i + 1, 'name': score[1], 'score': score[2]} for i, score in enumerate(unformatted_scores)] + + data = request.form + username = data.get('username', '').strip() + score = data.get('snake-score', '').strip() + token = data.get('cap-token', '').strip() + + if not username or not score or not token: + logging.error("Missing required fields: username=%s, score=%s, token=%s", username, score, token) + return render_template('404.html', scores=scores, error='Missing required fields'), 400 - name = data.get('name', '') - cap = data.get('cap', '') - score = data.get('score', -1) + try: + score = int(score) + except ValueError: + logging.error("Invalid score value: %s", score) + return render_template('404.html', scores=scores, error='Invalid score value'), 400 + + if score <= 0 or score > 10000 or len(username) < 3 or len(username) > 20: + logging.error("Invalid score or username length: score=%s, username=%s", score, username) + return render_template('404.html', scores=scores, error='Invalid score or username length'), 400 cap_response = requests.post( env('CAP_VERIFY_URL', default='https:////siteverify'), json={ 'secret': env('CAP_SECRET', default=''), - 'response': cap, + 'response': token, } ) if cap_response.status_code != 200 or not cap_response.json().get('success', "false") != "true": logging.error("Captcha verification failed: %s", cap_response.json()) - return {'error': 'Captcha verification failed'}, 400 - - if not isinstance(name, str) or not isinstance(score, int): - logging.error("Invalid data types: name=%s, score=%s", type(name), type(score)) - return {'error': 'Invalid data types'}, 400 + return render_template('404.html', scores=scores, error='Captcha verification failed'), 400 - if not name or score <= 0 or score > 10000: - logging.error("Invalid name or score: name=%s, score=%s", name, score) - return {'error': 'Invalid name or score'}, 400 - - db.insert_snake(name, score) - logging.info("Snake submitted: name=%s, score=%d", name, score) - return {'success': True, 'message': 'Snake submitted successfully'}, 200 + db.insert_snake(name=username, score=int(score)) + logging.info("Snake submitted: name=%s, score=%d", username, score) - -@app.errorhandler(404) -def page_not_found(e): - logging.error("Page not found: %s", request.path) unformatted_scores = db.get_snake_scores() scores = [{'position': i + 1, 'name': score[1], 'score': score[2]} for i, score in enumerate(unformatted_scores)] - return render_template('404.html', scores=scores), 404 + return render_template('404.html', scores=scores, success='Score submitted successfully!') + + +@app.route('/500') +@app.errorhandler(500) +def internal_error(error="An internal server error occurred."): + logging.error("Internal server error: %s", error) + return render_template('500.html'), 500 if __name__ == '__main__': diff --git a/src/static/js/snake.js b/src/static/js/snake.js deleted file mode 100644 index 146a8e0..0000000 --- a/src/static/js/snake.js +++ /dev/null @@ -1,151 +0,0 @@ -const canvas = document.getElementById('snakeCanvas'); -const ctx = canvas.getContext('2d'); - -const gridSize = 15; -const scale = 200; -canvas.width = gridSize * scale; -canvas.height = gridSize * scale; - -let snake = [{ x: 10, y: 10 }]; -let direction = { x: 0, y: 0 }; -let food = { x: 5, y: 5 }; -let score = 0; -let gameOver = false; - -let token = null; - -function draw() { - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // draw grid of checkerboard pattern - for (let x = 0; x < gridSize; x++) { - for (let y = 0; y < gridSize; y++) { - ctx.fillStyle = (x + y) % 2 === 0 ? 'darkgray' : 'grey'; - ctx.fillRect(x * scale, y * scale, scale, scale); - } - } - - // Draw snake - snake.forEach(segment => { - ctx.fillStyle = (snake.indexOf(segment) % 2 === 0) ? 'green' : 'darkgreen'; - ctx.fillRect(segment.x * scale, segment.y * scale, scale, scale); - }); - - // Draw food - ctx.fillStyle = 'red'; - ctx.fillRect(food.x * scale, food.y * scale, scale, scale); -} - -function update() { - if (gameOver) return; - - // Move snake - const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y }; - - // Add new head - snake.unshift(head); - - // Check for food collision - if (head.x === food.x && head.y === food.y) { - score += 10; // Increase score - placeFood(); - } else { - snake.pop(); // Remove tail if no food eaten - } - - // Check for wall collision - if (head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize) { - gameOver = true; - alert(`Game Over! Your score: ${score}`); - return; - } - - // Check for self collision - for (let i = 1; i < snake.length; i++) { - if (head.x === snake[i].x && head.y === snake[i].y) { - gameOver = true; - alert(`Game Over! Your score: ${score}`); - return; - } - } -} - -function placeFood() { - do { - food.x = Math.floor(Math.random() * gridSize); - food.y = Math.floor(Math.random() * gridSize); - } while (snake.some(segment => segment.x === food.x && segment.y === food.y)); -} - -function changeDirection(event) { - switch (event.key) { - case 'w': - if (direction.y === 0) direction = { x: 0, y: -1 }; - break; - case 's': - if (direction.y === 0) direction = { x: 0, y: 1 }; - break; - case 'a': - if (direction.x === 0) direction = { x: -1, y: 0 }; - break; - case 'd': - if (direction.x === 0) direction = { x: 1, y: 0 }; - break; - } -} - -function gameLoop() { - if (!gameOver) { - update(); - draw(); - setTimeout(gameLoop, 100); - } -} - -document.addEventListener('keydown', changeDirection); -// Start the game loop -gameLoop(); -// Initial draw -draw(); - - -const widget = document.querySelector("#cap"); - -widget.addEventListener("solve", function (e) { - token = e.detail.token; - console.log("Captcha solved, token:", token); -}); - -// Function to submit score to the server -document.getElementById("snakeForm").addEventListener('submit', function(event) { - event.preventDefault(); - - if (!token) { - alert('Please complete the CAPTCHA before submitting your score.'); - return; - } - - // post request to server with score - fetch('/snake/submit', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - score: score, - name: document.getElementById('name').value, - cap: token - }) - }).then(response => { - if (response.ok) { - alert('Score submitted successfully!'); - window.location.reload(); - } else { - alert('Failed to submit score, check console for details.'); - response.json().then(data => console.error(data)); - } - }).catch(error => { - console.error('Error submitting score:', error); - alert('Error submitting score, check console for details.'); - }); -}); \ No newline at end of file diff --git a/src/static/content/Irken-Like-AllCaps.woff b/static/content/Irken-Like-AllCaps.woff similarity index 100% rename from src/static/content/Irken-Like-AllCaps.woff rename to static/content/Irken-Like-AllCaps.woff diff --git a/src/static/content/background.png b/static/content/background.png similarity index 100% rename from src/static/content/background.png rename to static/content/background.png diff --git a/src/static/content/buttons.txt b/static/content/buttons.txt similarity index 100% rename from src/static/content/buttons.txt rename to static/content/buttons.txt diff --git a/src/static/content/haj.gif b/static/content/haj.gif similarity index 100% rename from src/static/content/haj.gif rename to static/content/haj.gif diff --git a/src/static/content/icon.webp b/static/content/icon.webp similarity index 100% rename from src/static/content/icon.webp rename to static/content/icon.webp diff --git a/src/static/css/404.css b/static/css/404.css similarity index 67% rename from src/static/css/404.css rename to static/css/404.css index d3e572f..ff24cf5 100644 --- a/src/static/css/404.css +++ b/static/css/404.css @@ -1,7 +1,8 @@ canvas#snakeCanvas { - padding: 15px; - width: 100%; + margin: 15px; box-sizing: border-box; + border: 2px solid var(--secondary-background-color); + border-radius: 10px; } form { @@ -76,4 +77,37 @@ form button[type="submit"] { #snakeLeaderboard li:nth-child(even) { background-color: var(--secondary-background-color); +} + +dialog { + width: 90%; + max-width: 500px; + padding: 20px; + background-color: var(--background-color); + color: var(--text-color); + border: 2px solid var(--secondary-background-color); + border-radius: 8px; +} + +dialog button { + padding: 10px; + width: 100%; + box-sizing: border-box; + border: 2px solid var(--secondary-background-color); + border-radius: 6px; + background-color: var(--secondary-background-color-but-slightly-transparent); + color: var(--text-color); + font-weight: bold; + cursor: pointer; + margin-top: 10px; +} + +dialog h2 { + margin: 0; + font-size: 1.5rem; +} + +dialog p { + margin: 0; + font-size: 1rem; } \ No newline at end of file diff --git a/static/css/500.css b/static/css/500.css new file mode 100644 index 0000000..2f74df6 --- /dev/null +++ b/static/css/500.css @@ -0,0 +1,8 @@ +.bluescreen { + background-color: #0077D6; + color: #fff; + padding:40px; + display: flex; + flex-direction: column; + gap: 20px; +} \ No newline at end of file diff --git a/src/static/css/index.css b/static/css/base.css similarity index 100% rename from src/static/css/index.css rename to static/css/base.css diff --git a/src/static/css/cap.css b/static/css/cap.css similarity index 100% rename from src/static/css/cap.css rename to static/css/cap.css diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..0d875df Binary files /dev/null and b/static/favicon.ico differ diff --git a/src/static/js/index.js b/static/js/base.js similarity index 100% rename from src/static/js/index.js rename to static/js/base.js diff --git a/static/js/snake.js b/static/js/snake.js new file mode 100644 index 0000000..61a4edc --- /dev/null +++ b/static/js/snake.js @@ -0,0 +1,174 @@ +const canvas = document.getElementById('snakeCanvas'); +const ctx = canvas.getContext('2d'); + +const gridSize = 20; +const tileSize = 100; +const snakeSize = 60; +const foodSize = 80; +canvas.width = gridSize * tileSize; +canvas.height = gridSize * tileSize; + +let snake = [{ x: 10, y: 10 }, { x: 10, y: 11 }, { x: 10, y: 12 }]; +let direction = { x: 0, y: 0 }; +let food = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) }; +let score = 0; +let gameOver = false; + +function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // draw grid of checkerboard pattern + for (let x = 0; x < gridSize; x++) { + for (let y = 0; y < gridSize; y++) { + ctx.fillStyle = (x + y) % 2 === 0 ? + getComputedStyle(document.documentElement).getPropertyValue('--background-color') : + getComputedStyle(document.documentElement).getPropertyValue('--secondary-background-color'); + ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize); + } + } + + // Draw snake + snake.forEach(segment => { + let nextVec = { x: 0, y: 0 }; + // if there is a segment after the current segment + if (snake.indexOf(segment) < snake.length - 1) { + const nextSegment = snake[snake.indexOf(segment) + 1]; + nextVec.x = nextSegment.x - segment.x; + nextVec.y = nextSegment.y - segment.y; + } + + ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--primary-color'); + if (nextVec.x === 0 && nextVec.y === 0) { + ctx.fillRect( + segment.x * tileSize + (tileSize - snakeSize) / 2, + segment.y * tileSize + (tileSize - snakeSize) / 2, + snakeSize, + snakeSize + ); + } else if (nextVec.x > 0 || nextVec.y > 0) { + ctx.fillRect( + segment.x * tileSize + (tileSize - snakeSize) / 2, + segment.y * tileSize + (tileSize - snakeSize) / 2, + snakeSize + nextVec.x * (tileSize - snakeSize), + snakeSize + nextVec.y * (tileSize - snakeSize) + ); + } else { + ctx.fillRect( + segment.x * tileSize + (tileSize - snakeSize) / 2 + nextVec.x * (tileSize - snakeSize), + segment.y * tileSize + (tileSize - snakeSize) / 2 + nextVec.y * (tileSize - snakeSize), + snakeSize + Math.abs(nextVec.x) * (tileSize - snakeSize), + snakeSize + Math.abs(nextVec.y) * (tileSize - snakeSize) + ); + } + + }); + + // Draw food + ctx.fillStyle = '#ff4d4d'; + ctx.fillRect( + food.x * tileSize + (tileSize - foodSize) / 2, + food.y * tileSize + (tileSize - foodSize) / 2, + foodSize, + foodSize + ); +} + +function update() { + if (gameOver) return; + + // Move snake + const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y }; + + // Add new head + snake.unshift(head); + + // Check for food collision + if (head.x === food.x && head.y === food.y) { + score += 10; // Increase score + placeFood(); + } else { + snake.pop(); // Remove tail if no food eaten + } + + // Check for wall collision + if (head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize) { + gameOver = true; + return; + } + + // Check for self collision + for (let i = 1; i < snake.length; i++) { + if (head.x === snake[i].x && head.y === snake[i].y) { + gameOver = true; + return; + } + } +} + +function placeFood() { + do { + food.x = Math.floor(Math.random() * gridSize); + food.y = Math.floor(Math.random() * gridSize); + } while (snake.some(segment => segment.x === food.x && segment.y === food.y)); +} + +function changeDirection(event) { + switch (event.key) { + case 'w': + if (direction.y === 0) direction = { x: 0, y: -1 }; + break; + case 's': + if (direction.y === 0) direction = { x: 0, y: 1 }; + break; + case 'a': + if (direction.x === 0) direction = { x: -1, y: 0 }; + break; + case 'd': + if (direction.x === 0) direction = { x: 1, y: 0 }; + break; + } +} + +// Menu to start the game +function menu() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--background-color'); + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.textAlign = 'center'; + ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--text-color'); + ctx.font = '200px Arial'; + ctx.fillText('Snake Game', canvas.width / 2, canvas.height / 2); + ctx.font = '100px Arial'; + ctx.fillText('Press W/A/S/D to move', canvas.width / 2, canvas.height / 2 + 100); + ctx.fillText('Click to start', canvas.width / 2, canvas.height / 2 + 200); + + canvas.addEventListener('click', startGame); +} + +function gameLoop() { + if (!gameOver) { + update(); + draw(); + setTimeout(gameLoop, 100); + } else { + document.removeEventListener('keydown', changeDirection); + document.getElementById('snake-score').value = score; + alert(`Game Over! Your score: ${score}`); + menu(); + } +} + +function startGame() { + snake = [{ x: 10, y: 10 }, { x: 10, y: 11 }, { x: 10, y: 12 }]; + direction = { x: 1, y: 0 }; + food = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) }; + score = 0; + gameOver = false; + canvas.removeEventListener('click', startGame); + document.addEventListener('keydown', changeDirection); + gameLoop(); +} + +menu(); \ No newline at end of file diff --git a/src/templates/robots.txt b/static/robots.txt similarity index 100% rename from src/templates/robots.txt rename to static/robots.txt diff --git a/src/templates/sitemap.xml b/static/sitemap.xml similarity index 100% rename from src/templates/sitemap.xml rename to static/sitemap.xml diff --git a/src/templates/404.html b/templates/404.html similarity index 63% rename from src/templates/404.html rename to templates/404.html index add651c..6ac41f9 100644 --- a/src/templates/404.html +++ b/templates/404.html @@ -14,16 +14,17 @@

It seems like the thing you are looking for is not here :[

- while you're here, why not play some snake? (use wasd to move, not mobile compatable :<) + while you're here, why not play some snake?

Submit score

-
- - + + + +
@@ -40,6 +41,19 @@
+{% if error %} + +

Error

+

{{ error }}

+ +
+ +{% endif %} {% endblock %} {% block scripts %} diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..ab6dfe4 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block title %}500 - Internal Server Error{% endblock %} +{% block description %}An unexpected error occurred on the server.{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+

:(

+

+ Oopsie Woopsie! Uwu We made a fucky wucky!! A wittle fucko boingo! The code monkeys at our headquarters are working VEWY HAWD to fix this! +

+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/base.html b/templates/base.html similarity index 95% rename from src/templates/base.html rename to templates/base.html index 956d598..dbe782e 100644 --- a/src/templates/base.html +++ b/templates/base.html @@ -5,7 +5,7 @@ {% block title %}Alfie's basement{% endblock %} - + @@ -26,7 +26,7 @@

Things to see :3

@@ -60,7 +61,7 @@
      |\      _,,,---,,_
ZZZzz /,`.-'`' -. ;-;;,_
|,4- ) )-,_. ,\ ( `'-'
'---''(_/--' `-'\_)
- haj + haj
@@ -92,6 +93,6 @@
{% block scripts %}{% endblock %} - + diff --git a/src/templates/index.html b/templates/index.html similarity index 100% rename from src/templates/index.html rename to templates/index.html