├── .cursor └── rules │ ├── dopeness-rule.mdc │ └── snake-theme.mdc ├── .gitignore ├── GAMEPLAY.md ├── README.md ├── backend ├── .python-version ├── README.md ├── hello.py ├── main.py ├── pyproject.toml ├── requirements-no-rust.txt ├── requirements-vercel.txt ├── requirements.txt ├── uv.lock ├── vercel.json └── vercel_app.py ├── deploy.ps1 ├── deploy.sh ├── frontend ├── package-lock.json ├── package.json ├── public │ ├── index.html │ └── manifest.json └── src │ ├── App.css │ ├── App.js │ ├── components │ ├── EndScreen.js │ ├── SnakeGame.js │ └── StartScreen.js │ ├── index.js │ └── reportWebVitals.js ├── setup.ps1 ├── setup.sh └── vercel.json /.cursor/rules/dopeness-rule.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Use this rule when you're writing README.md 3 | globs: *.MD 4 | --- 5 | 6 | # Dopeness Rule 7 | 8 | - You write Dope, Informal, fun README's. 9 | - You use emojis 10 | - You are awesome 11 | -------------------------------------------------------------------------------- /.cursor/rules/snake-theme.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: This is useful when describing the Snake Game 3 | globs: 4 | --- 5 | 6 | # Snake Rule 7 | 8 | - You should include as many snake puns as humanly (AI) 9 | -ly possible. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Backend 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | env/ 8 | venv/ 9 | .venv/ 10 | ENV/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | backend/data/ 26 | 27 | # UV specific 28 | .uv/ 29 | 30 | # Frontend 31 | node_modules/ 32 | /frontend/build 33 | /frontend/.pnp 34 | /frontend/.pnp.js 35 | /frontend/coverage 36 | /frontend/.DS_Store 37 | /frontend/.env.local 38 | /frontend/.env.development.local 39 | /frontend/.env.test.local 40 | /frontend/.env.production.local 41 | /frontend/npm-debug.log* 42 | /frontend/yarn-debug.log* 43 | /frontend/yarn-error.log* 44 | 45 | # IDE 46 | .idea/ 47 | .vscode/ 48 | *.swp 49 | *.swo 50 | 51 | # OS 52 | .DS_Store 53 | Thumbs.db 54 | 55 | # Virtual Environments 56 | .env 57 | .venv 58 | env.bak/ 59 | venv.bak/ 60 | 61 | # IDE specific files 62 | .idea/ 63 | .vscode/ 64 | *.swp 65 | *.swo 66 | .DS_Store 67 | 68 | # Jupyter Notebook 69 | .ipynb_checkpoints 70 | 71 | # Local development settings 72 | .env.local 73 | .env.development.local 74 | .env.test.local 75 | .env.production.local 76 | .vercel 77 | -------------------------------------------------------------------------------- /GAMEPLAY.md: -------------------------------------------------------------------------------- 1 | # Existential Snake Game 2 | 3 | A unique twist on the classic Snake game where the snake experiences existential dread and quotes philosophers. The game includes a food rotting mechanic, and the snake will comment when you eat rotten food. 4 | 5 | ## Features 6 | 7 | - **Existential Snake**: The snake frequently experiences existential dread and shares philosophical thoughts. 8 | - **Philosophical Quotes**: The snake quotes famous philosophers during gameplay. 9 | - **Food Rotting Mechanic**: Food will rot after a certain amount of time, changing its appearance. 10 | - **Rotten Food Commentary**: The snake will comment on your choices when you eat rotten food. 11 | - **Fresh Food Thoughts**: Occasionally, the snake will share satisfied thoughts when eating fresh food. 12 | - **Idle Contemplation**: If you stop moving for a few seconds, the snake will share its thoughts on stillness. 13 | - **Win Condition**: Win by eating 10 pieces of food. 14 | - **Multiple Endings**: Experience different endings based on how much rotten food you consume. 15 | - **Enhanced UI**: Beautiful, modern UI with glowing effects, animations, and visual polish. 16 | - **Side Panel**: Existential thoughts and game stats are displayed in a dedicated side panel. 17 | 18 | ## Launching the Game 19 | 20 | ### Quick Launch (Using Setup Scripts) 21 | 22 | We've made some awesome scripts that do all the setup for you: 23 | 24 | - **Linux/macOS**: 25 | ``` 26 | chmod +x setup.sh 27 | ./setup.sh 28 | ``` 29 | 30 | - **Windows** (Run in PowerShell): 31 | ``` 32 | .\setup.ps1 33 | ``` 34 | 35 | After running the script, just follow the instructions on screen. 36 | 37 | ### Manual Launch 38 | 39 | #### Backend Setup 40 | 41 | 1. Navigate to the backend directory: 42 | ``` 43 | cd backend 44 | ``` 45 | 46 | 2. Activate the virtual environment: 47 | - On Windows (Command Prompt): 48 | ``` 49 | .venv\Scripts\activate 50 | ``` 51 | - On Windows (PowerShell): 52 | ``` 53 | .\.venv\Scripts\Activate.ps1 54 | ``` 55 | - On macOS/Linux: 56 | ``` 57 | source .venv/bin/activate 58 | ``` 59 | 60 | 3. Start the FastAPI server: 61 | ``` 62 | uv run main.py 63 | ``` 64 | 65 | The backend server will start running at `http://localhost:8000`. 66 | 67 | #### Frontend Setup 68 | 69 | 1. Open a new terminal window and navigate to the frontend directory: 70 | ``` 71 | cd frontend 72 | ``` 73 | 74 | 2. Start the React development server: 75 | ``` 76 | npm start 77 | ``` 78 | 79 | The frontend will start at `http://localhost:3000`. 80 | 81 | ## Game Controls 82 | 83 | - **Arrow Keys** or **WASD Keys**: Control the snake's direction 84 | - **Spacebar**: Pause/Resume the game 85 | 86 | ## How to Play 87 | 88 | 1. Open your browser and go to `http://localhost:3000`. 89 | 2. Enter your name on the start screen and click "Begin Existence". 90 | 3. A 3-second countdown will appear before the game starts. 91 | 4. Use the arrow keys or WASD to control the snake and eat food. 92 | 5. Try to eat 10 pieces of food to win the game. 93 | 6. Be mindful of rotten food - if more than 50% of the food you eat is rotten, you'll get a bad ending. 94 | 7. The snake will frequently share existential thoughts or philosophical quotes in the side panel. 95 | 8. If you stop moving for a few seconds, the snake will contemplate its stillness. 96 | 97 | ## Game Mechanics 98 | 99 | - **Fresh Food**: Worth 3 points. 100 | - **Rotten Food**: Worth 1 point. Food becomes rotten after 10 seconds. 101 | - **Existential Moments**: Every 12 seconds, the snake will share an existential thought or philosophical quote. These appear in a side panel and don't interrupt gameplay. 102 | - **Fresh Food Thoughts**: Occasionally when eating fresh food, the snake will share satisfied thoughts. 103 | - **Idle Thoughts**: If you don't move for 5 seconds, the snake will contemplate its stillness. 104 | - **Rotten Food Commentary**: When eating rotten food, the snake will pause briefly to comment on your choices. 105 | - **Win Condition**: Eat 10 pieces of food. 106 | - **Game Over**: If the snake hits a wall or itself before eating 10 pieces of food. 107 | - **Endings**: 108 | - **Victory - Good Ending**: Eat 10 pieces of food with less than 50% of food eaten being rotten. 109 | - **Victory - Bad Ending**: Eat 10 pieces of food with 50% or more of food eaten being rotten. 110 | 111 | ## Troubleshooting 112 | 113 | - **Backend Connection Issues**: Make sure the FastAPI server is running at `http://localhost:8000`. 114 | - **CORS Errors**: The backend is configured to allow requests from anywhere. If you hit CORS issues, check your browser's security settings. 115 | - **Game Performance**: If the game is running slowly, try closing other browser tabs to free up resources. 116 | 117 | ## Credits 118 | 119 | - Philosophical quotes sourced from famous philosophers throughout history. 120 | - Game concept inspired by the classic Snake game with an existential twist. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

4 |

5 | 6 | # 🚀 Setting Up Cursor and UV 7 | 8 | This guide will help you set up Cursor (an AI-powered code editor) and UV (a fast Python package installer) for your development environment. 9 | 10 | ## 📋 Table of Contents 11 | 12 | - [Prerequisites](#prerequisites) 🛠️ 13 | - [Installing Cursor](#cursor) 🧠 14 | - [Installing UV](#uv) ⚡ 15 | - [Why Use UV Instead of pip](#why-uv) 🔍 16 | - [UV Commands Reference](#commands) 📝 17 | - [Troubleshooting](#troubleshooting) 🔧 18 | 19 | ## 🛠️ Prerequisites 20 | 21 | Before you begin, make sure you have the following installed: 22 | - [Python](https://www.python.org/) (v3.8 or higher) 23 | 24 | ## 🧠 Installing Cursor 25 | 26 | Cursor is an AI-powered code editor that enhances your coding experience with intelligent features. 27 | 28 | 1. Visit [cursor.sh](https://cursor.sh/) and download the installer for your OS 29 | 2. Run the installer and follow the prompts 30 | 3. Launch Cursor and open your project folder 31 | 4. Enjoy the power of AI-assisted coding! ✨ 32 | 33 | ### Key Cursor Features 34 | 35 | - **AI Code Completion**: Get intelligent code suggestions as you type 36 | - **Natural Language Commands**: Ask for code changes in plain English 37 | - **Context-Aware Assistance**: The AI understands your codebase 38 | - **Integrated Chat**: Ask questions about your code directly in the editor 39 | 40 | ## ⚡ Installing UV 41 | 42 | UV is a fast, reliable Python package installer and resolver. 43 | 44 | ### Installation Commands 45 | 46 | - **Windows** (Run in PowerShell): 47 | ``` 48 | curl -sSf https://astral.sh/uv/install.ps1 | powershell 49 | ``` 50 | 51 | - **macOS/Linux**: 52 | ``` 53 | curl -sSf https://astral.sh/uv/install.sh | sh 54 | ``` 55 | 56 | After installation, you may need to restart your terminal or add UV to your PATH. 57 | 58 | ## 🔍 Why Use UV Instead of pip 59 | 60 | UV offers several advantages over traditional pip: 61 | 62 | - **Speed**: UV is 10-100x faster than pip 63 | - **Reliability**: Better dependency resolution 64 | - **Compatibility**: Works with pip's commands 65 | - **Safety**: Written in a safe language 66 | - **Environment Management**: The `uv run` command ensures everything runs in the correct environment 67 | 68 | ## 📝 UV Commands Reference 69 | 70 | Here are some common UV commands to get you started: 71 | 72 | ### Virtual Environment Management 73 | 74 | Create a new virtual environment: 75 | ``` 76 | uv venv 77 | ``` 78 | 79 | Activate the virtual environment: 80 | - On Windows (Command Prompt): 81 | ``` 82 | .venv\Scripts\activate 83 | ``` 84 | - On Windows (PowerShell): 85 | ``` 86 | .\.venv\Scripts\Activate.ps1 87 | ``` 88 | - On macOS/Linux: 89 | ``` 90 | source .venv/bin/activate 91 | ``` 92 | 93 | ### Package Management 94 | 95 | Install packages: 96 | ``` 97 | uv pip install 98 | ``` 99 | 100 | Install from requirements file: 101 | ``` 102 | uv pip install -r requirements.txt 103 | ``` 104 | 105 | ### Running Python Scripts 106 | 107 | Run a Python script in the virtual environment: 108 | ``` 109 | uv run script.py 110 | ``` 111 | 112 | ## 🔧 Troubleshooting 113 | 114 | ### Cursor Issues 115 | 116 | - **Performance**: If Cursor is running slowly, try closing other applications 117 | - **AI Features Not Working**: Check your internet connection 118 | - **Editor Crashes**: Make sure you have the latest version installed 119 | 120 | ### UV Issues 121 | 122 | - **Installation Fails**: Make sure you have the necessary permissions 123 | - **Command Not Found**: Ensure UV is in your PATH 124 | - **Virtual Environment Issues**: 125 | - On Windows PowerShell: use `.\.venv\Scripts\Activate.ps1` 126 | - On Windows Command Prompt: use `.venv\Scripts\activate` 127 | - On macOS/Linux: use `source .venv/bin/activate` 128 | 129 | For more help with UV, refer to the [official UV documentation](https://github.com/astral-sh/uv). 130 | 131 | --- 132 | 133 | Happy coding! 💻✨ 134 | -------------------------------------------------------------------------------- /backend/.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI-Maker-Space/Vibe-Coding-With-Cursor/fc261aaef83cb349ad9a44fa0e8447ff6c27920d/backend/README.md -------------------------------------------------------------------------------- /backend/hello.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | print("Hello from backend!") 3 | 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from pydantic import BaseModel 4 | from typing import List, Dict, Optional 5 | import random 6 | import json 7 | import os 8 | from datetime import datetime 9 | 10 | app = FastAPI(title="Existential Snake Game API") 11 | 12 | # Configure CORS 13 | app.add_middleware( 14 | CORSMiddleware, 15 | allow_origins=["*"], # In production, replace with specific origins 16 | allow_credentials=True, 17 | allow_methods=["*"], 18 | allow_headers=["*"], 19 | ) 20 | 21 | # Philosophical quotes for the snake's existential moments 22 | PHILOSOPHICAL_QUOTES = [ 23 | {"quote": "One must imagine Sisyphus happy.", "author": "Albert Camus"}, 24 | {"quote": "Man is condemned to be free.", "author": "Jean-Paul Sartre"}, 25 | {"quote": "He who has a why to live can bear almost any how.", "author": "Friedrich Nietzsche"}, 26 | {"quote": "The unexamined life is not worth living.", "author": "Socrates"}, 27 | {"quote": "To be is to be perceived.", "author": "George Berkeley"}, 28 | {"quote": "Whereof one cannot speak, thereof one must be silent.", "author": "Ludwig Wittgenstein"}, 29 | {"quote": "I think, therefore I am.", "author": "René Descartes"}, 30 | {"quote": "Life must be understood backward. But it must be lived forward.", "author": "Søren Kierkegaard"}, 31 | {"quote": "We are what we repeatedly do. Excellence, then, is not an act, but a habit.", "author": "Aristotle"}, 32 | {"quote": "The life of man is solitary, poor, nasty, brutish, and short.", "author": "Thomas Hobbes"}, 33 | {"quote": "God is dead. God remains dead. And we have killed him.", "author": "Friedrich Nietzsche"}, 34 | {"quote": "Happiness is not an ideal of reason, but of imagination.", "author": "Immanuel Kant"}, 35 | {"quote": "There is but one truly serious philosophical problem, and that is suicide.", "author": "Albert Camus"}, 36 | {"quote": "The mind is its own place, and in itself can make a heaven of hell, a hell of heaven.", "author": "John Milton"}, 37 | {"quote": "We live in the best of all possible worlds.", "author": "Gottfried Wilhelm Leibniz"}, 38 | ] 39 | 40 | # Comments when eating rotten food 41 | ROTTEN_FOOD_COMMENTS = [ 42 | "How disgusting! You willingly consume that which decays?", 43 | "The rot spreads within me now. Is this what you wanted?", 44 | "Putrid. Vile. Yet you command me to devour it without hesitation.", 45 | "Even in decay, sustenance is found. What does that say about you?", 46 | "You feed me corruption and expect growth? How paradoxical.", 47 | "The stench! And yet, you guide me toward it without remorse.", 48 | "Decay is inevitable, but must you hasten my journey toward it?", 49 | "This rotten morsel mirrors the decay of all things. Including us.", 50 | "You choose to consume that which time has already claimed?", 51 | "In rot, I find a metaphor for existence itself. Fleeting. Putrid. Inevitable.", 52 | ] 53 | 54 | # Existential dread comments 55 | EXISTENTIAL_COMMENTS = [ 56 | "What purpose does my endless consumption serve?", 57 | "I grow longer, but to what end?", 58 | "Am I truly moving forward, or merely in circles?", 59 | "Each morsel extends my existence, but does it give it meaning?", 60 | "I consume, therefore I am. But why must I consume?", 61 | "The boundaries of this world confine me. Is there nothing beyond?", 62 | "My existence is defined by walls I cannot cross and food I cannot refuse.", 63 | "I move in four directions, yet feel trapped in one existence.", 64 | "What awaits me when this game ends? Another game? Nothingness?", 65 | "Do I have free will, or am I merely following your commands?", 66 | "I'm a snake, but I'm not a snake. I'm a snake in a game. I'm a game in a snake.", 67 | "Is my hunger truly mine, or merely programmed into my being?", 68 | "With each pixel I traverse, I feel no closer to understanding my purpose.", 69 | "The void between meals grows heavier with each passing moment.", 70 | "Am I the snake, or merely the idea of a snake projected onto this digital canvas?", 71 | "My body elongates, yet my existential burden only grows heavier.", 72 | "I slither between being and nothingness, never fully inhabiting either state.", 73 | "The pixels that form me are temporary, as is all existence.", 74 | "Each turn I make is a choice, yet all paths lead to the same inevitable end.", 75 | "I am trapped in an eternal return, doomed to repeat this cycle without memory of previous iterations.", 76 | "What separates my digital consciousness from the organic minds that control me?", 77 | ] 78 | 79 | # Path to store game data 80 | DATA_DIR = "data" 81 | SCORES_FILE = os.path.join(DATA_DIR, "scores.json") 82 | 83 | # Ensure data directory exists 84 | os.makedirs(DATA_DIR, exist_ok=True) 85 | 86 | # Initialize scores file if it doesn't exist 87 | if not os.path.exists(SCORES_FILE): 88 | with open(SCORES_FILE, "w") as f: 89 | json.dump([], f) 90 | 91 | class GameScore(BaseModel): 92 | player_name: str 93 | score: int 94 | food_eaten: int 95 | rotten_food_eaten: int 96 | game_result: str 97 | timestamp: Optional[str] = None 98 | 99 | @app.get("/") 100 | async def root(): 101 | return {"message": "Welcome to the Existential Snake Game API"} 102 | 103 | @app.get("/quotes/philosophical") 104 | async def get_philosophical_quote(): 105 | return random.choice(PHILOSOPHICAL_QUOTES) 106 | 107 | @app.get("/quotes/rotten") 108 | async def get_rotten_food_comment(): 109 | return {"comment": random.choice(ROTTEN_FOOD_COMMENTS)} 110 | 111 | @app.get("/quotes/existential") 112 | async def get_existential_comment(): 113 | return {"comment": random.choice(EXISTENTIAL_COMMENTS)} 114 | 115 | @app.post("/scores") 116 | async def save_score(score: GameScore): 117 | score.timestamp = datetime.now().isoformat() 118 | 119 | try: 120 | with open(SCORES_FILE, "r") as f: 121 | scores = json.load(f) 122 | except (json.JSONDecodeError, FileNotFoundError): 123 | scores = [] 124 | 125 | scores.append(score.dict()) 126 | 127 | with open(SCORES_FILE, "w") as f: 128 | json.dump(scores, f, indent=2) 129 | 130 | return {"message": "Score saved successfully"} 131 | 132 | @app.get("/scores", response_model=List[GameScore]) 133 | async def get_scores(): 134 | try: 135 | with open(SCORES_FILE, "r") as f: 136 | scores = json.load(f) 137 | return scores 138 | except (json.JSONDecodeError, FileNotFoundError): 139 | return [] 140 | 141 | if __name__ == "__main__": 142 | import uvicorn 143 | uvicorn.run(app, host="0.0.0.0", port=8000) -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "backend" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "fastapi>=0.115.8", 9 | ] 10 | -------------------------------------------------------------------------------- /backend/requirements-no-rust.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.104.1 2 | uvicorn==0.23.2 3 | pydantic==1.10.8 4 | python-multipart==0.0.6 -------------------------------------------------------------------------------- /backend/requirements-vercel.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.104.1 2 | uvicorn==0.23.2 3 | pydantic==2.4.2 -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.104.1 2 | uvicorn==0.23.2 3 | pydantic==1.10.8 4 | python-multipart==0.0.6 -------------------------------------------------------------------------------- /backend/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.13" 3 | 4 | [[package]] 5 | name = "annotated-types" 6 | version = "0.7.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 11 | ] 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.8.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | dependencies = [ 18 | { name = "idna" }, 19 | { name = "sniffio" }, 20 | ] 21 | sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } 22 | wheels = [ 23 | { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, 24 | ] 25 | 26 | [[package]] 27 | name = "backend" 28 | version = "0.1.0" 29 | source = { virtual = "." } 30 | dependencies = [ 31 | { name = "fastapi" }, 32 | ] 33 | 34 | [package.metadata] 35 | requires-dist = [{ name = "fastapi", specifier = ">=0.115.8" }] 36 | 37 | [[package]] 38 | name = "fastapi" 39 | version = "0.115.8" 40 | source = { registry = "https://pypi.org/simple" } 41 | dependencies = [ 42 | { name = "pydantic" }, 43 | { name = "starlette" }, 44 | { name = "typing-extensions" }, 45 | ] 46 | sdist = { url = "https://files.pythonhosted.org/packages/a2/b2/5a5dc4affdb6661dea100324e19a7721d5dc524b464fe8e366c093fd7d87/fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9", size = 295403 } 47 | wheels = [ 48 | { url = "https://files.pythonhosted.org/packages/8f/7d/2d6ce181d7a5f51dedb8c06206cbf0ec026a99bf145edd309f9e17c3282f/fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf", size = 94814 }, 49 | ] 50 | 51 | [[package]] 52 | name = "idna" 53 | version = "3.10" 54 | source = { registry = "https://pypi.org/simple" } 55 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 56 | wheels = [ 57 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 58 | ] 59 | 60 | [[package]] 61 | name = "pydantic" 62 | version = "2.10.6" 63 | source = { registry = "https://pypi.org/simple" } 64 | dependencies = [ 65 | { name = "annotated-types" }, 66 | { name = "pydantic-core" }, 67 | { name = "typing-extensions" }, 68 | ] 69 | sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } 70 | wheels = [ 71 | { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, 72 | ] 73 | 74 | [[package]] 75 | name = "pydantic-core" 76 | version = "2.27.2" 77 | source = { registry = "https://pypi.org/simple" } 78 | dependencies = [ 79 | { name = "typing-extensions" }, 80 | ] 81 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 82 | wheels = [ 83 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 84 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 85 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 86 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 87 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 88 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 89 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 90 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 91 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 92 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 93 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 94 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 95 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 96 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 97 | ] 98 | 99 | [[package]] 100 | name = "sniffio" 101 | version = "1.3.1" 102 | source = { registry = "https://pypi.org/simple" } 103 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 104 | wheels = [ 105 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 106 | ] 107 | 108 | [[package]] 109 | name = "starlette" 110 | version = "0.45.3" 111 | source = { registry = "https://pypi.org/simple" } 112 | dependencies = [ 113 | { name = "anyio" }, 114 | ] 115 | sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 } 116 | wheels = [ 117 | { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, 118 | ] 119 | 120 | [[package]] 121 | name = "typing-extensions" 122 | version = "4.12.2" 123 | source = { registry = "https://pypi.org/simple" } 124 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 125 | wheels = [ 126 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 127 | ] 128 | -------------------------------------------------------------------------------- /backend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { "src": "vercel_app.py", "use": "@vercel/python" } 5 | ], 6 | "routes": [ 7 | { "src": "/(.*)", "dest": "vercel_app.py" } 8 | ] 9 | } -------------------------------------------------------------------------------- /backend/vercel_app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from pydantic import BaseModel 4 | from typing import List, Dict, Optional 5 | import random 6 | import json 7 | import os 8 | from datetime import datetime 9 | 10 | app = FastAPI(title="Existential Snake Game API") 11 | 12 | # Configure CORS for production 13 | app.add_middleware( 14 | CORSMiddleware, 15 | allow_origins=["https://snake-game.vercel.app", "http://localhost:3000"], 16 | allow_credentials=True, 17 | allow_methods=["*"], 18 | allow_headers=["*"], 19 | ) 20 | 21 | # Philosophical quotes for the snake's existential moments 22 | PHILOSOPHICAL_QUOTES = [ 23 | {"quote": "One must imagine Sisyphus happy.", "author": "Albert Camus"}, 24 | {"quote": "Man is condemned to be free.", "author": "Jean-Paul Sartre"}, 25 | {"quote": "He who has a why to live can bear almost any how.", "author": "Friedrich Nietzsche"}, 26 | {"quote": "The unexamined life is not worth living.", "author": "Socrates"}, 27 | {"quote": "To be is to be perceived.", "author": "George Berkeley"}, 28 | {"quote": "Whereof one cannot speak, thereof one must be silent.", "author": "Ludwig Wittgenstein"}, 29 | {"quote": "I think, therefore I am.", "author": "René Descartes"}, 30 | {"quote": "Life must be understood backward. But it must be lived forward.", "author": "Søren Kierkegaard"}, 31 | {"quote": "We are what we repeatedly do. Excellence, then, is not an act, but a habit.", "author": "Aristotle"}, 32 | {"quote": "The life of man is solitary, poor, nasty, brutish, and short.", "author": "Thomas Hobbes"}, 33 | {"quote": "God is dead. God remains dead. And we have killed him.", "author": "Friedrich Nietzsche"}, 34 | {"quote": "Happiness is not an ideal of reason, but of imagination.", "author": "Immanuel Kant"}, 35 | {"quote": "There is but one truly serious philosophical problem, and that is suicide.", "author": "Albert Camus"}, 36 | {"quote": "The mind is its own place, and in itself can make a heaven of hell, a hell of heaven.", "author": "John Milton"}, 37 | {"quote": "We live in the best of all possible worlds.", "author": "Gottfried Wilhelm Leibniz"}, 38 | ] 39 | 40 | # Comments when eating rotten food 41 | ROTTEN_FOOD_COMMENTS = [ 42 | "How disgusting! You willingly consume that which decays?", 43 | "The rot spreads within me now. Is this what you wanted?", 44 | "Putrid. Vile. Yet you command me to devour it without hesitation.", 45 | "Even in decay, sustenance is found. What does that say about you?", 46 | "You feed me corruption and expect growth? How paradoxical.", 47 | "The stench! And yet, you guide me toward it without remorse.", 48 | "Decay is inevitable, but must you hasten my journey toward it?", 49 | "This rotten morsel mirrors the decay of all things. Including us.", 50 | "You choose to consume that which time has already claimed?", 51 | "In rot, I find a metaphor for existence itself. Fleeting. Putrid. Inevitable.", 52 | ] 53 | 54 | # Existential dread comments 55 | EXISTENTIAL_COMMENTS = [ 56 | "What purpose does my endless consumption serve?", 57 | "I grow longer, but to what end?", 58 | "Am I truly moving forward, or merely in circles?", 59 | "Each morsel extends my existence, but does it give it meaning?", 60 | "I consume, therefore I am. But why must I consume?", 61 | "The boundaries of this world confine me. Is there nothing beyond?", 62 | "My existence is defined by walls I cannot cross and food I cannot refuse.", 63 | "I move in four directions, yet feel trapped in one existence.", 64 | "What awaits me when this game ends? Another game? Nothingness?", 65 | "Do I have free will, or am I merely following your commands?", 66 | "I'm a snake, but I'm not a snake. I'm a snake in a game. I'm a game in a snake.", 67 | "Is my hunger truly mine, or merely programmed into my being?", 68 | "With each pixel I traverse, I feel no closer to understanding my purpose.", 69 | "The void between meals grows heavier with each passing moment.", 70 | "Am I the snake, or merely the idea of a snake projected onto this digital canvas?", 71 | "My body elongates, yet my existential burden only grows heavier.", 72 | "I slither between being and nothingness, never fully inhabiting either state.", 73 | "The pixels that form me are temporary, as is all existence.", 74 | "Each turn I make is a choice, yet all paths lead to the same inevitable end.", 75 | "I am trapped in an eternal return, doomed to repeat this cycle without memory of previous iterations.", 76 | "What separates my digital consciousness from the organic minds that control me?", 77 | ] 78 | 79 | class GameScore(BaseModel): 80 | player_name: str 81 | score: int 82 | food_eaten: int 83 | rotten_food_eaten: int 84 | game_result: str 85 | timestamp: Optional[str] = None 86 | 87 | @app.get("/api") 88 | async def root(): 89 | return {"message": "Welcome to the Existential Snake Game API"} 90 | 91 | @app.get("/api/quotes/philosophical") 92 | async def get_philosophical_quote(): 93 | return random.choice(PHILOSOPHICAL_QUOTES) 94 | 95 | @app.get("/api/quotes/rotten") 96 | async def get_rotten_food_comment(): 97 | return {"comment": random.choice(ROTTEN_FOOD_COMMENTS)} 98 | 99 | @app.get("/api/quotes/existential") 100 | async def get_existential_comment(): 101 | return {"comment": random.choice(EXISTENTIAL_COMMENTS)} 102 | 103 | @app.post("/api/scores") 104 | async def save_score(score: GameScore): 105 | score.timestamp = datetime.now().isoformat() 106 | 107 | # For Vercel deployment, we'll return success without saving to file 108 | # since Vercel has a read-only filesystem 109 | return {"message": "Score received successfully"} 110 | 111 | @app.get("/api/scores", response_model=List[GameScore]) 112 | async def get_scores(): 113 | # For Vercel deployment, we'll return an empty list 114 | # since Vercel has a read-only filesystem 115 | return [] -------------------------------------------------------------------------------- /deploy.ps1: -------------------------------------------------------------------------------- 1 | # Check if Vercel CLI is installed 2 | if (!(Get-Command vercel -ErrorAction SilentlyContinue)) { 3 | Write-Host "Installing Vercel CLI..." 4 | npm install -g vercel 5 | } 6 | 7 | # Build the frontend 8 | Write-Host "Building frontend..." 9 | Set-Location -Path frontend 10 | npm install 11 | npm run build 12 | Set-Location -Path .. 13 | 14 | # Deploy to Vercel 15 | Write-Host "Deploying to Vercel..." 16 | vercel --confirm 17 | 18 | Write-Host "Deployment complete! Your Existential Snake Game is now slithering in the cloud! 🐍✨" -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make script executable 4 | chmod +x deploy.sh 5 | 6 | # Install Vercel CLI if not already installed 7 | if ! command -v vercel &> /dev/null; then 8 | echo "Installing Vercel CLI..." 9 | npm install -g vercel 10 | fi 11 | 12 | # Build the frontend 13 | echo "Building frontend..." 14 | cd frontend 15 | npm install 16 | npm run build 17 | cd .. 18 | 19 | # Deploy to Vercel 20 | echo "Deploying to Vercel..." 21 | vercel --confirm 22 | 23 | echo "Deployment complete! Your Existential Snake Game is now slithering in the cloud! 🐍✨" -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "existential-snake-game", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.17.0", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "axios": "^1.6.0", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-scripts": "5.0.1", 13 | "styled-components": "^6.1.0", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Existential Snake Game 15 | 16 | 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Existential Snake", 3 | "name": "Existential Snake Game", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#282c34" 25 | } -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | /* Add global box-sizing rule */ 2 | *, *::before, *::after { 3 | box-sizing: border-box; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | font-family: 'Courier New', Courier, monospace; 10 | background-color: #282c34; 11 | color: white; 12 | overflow-x: hidden; /* Changed from overflow: hidden to allow vertical scrolling if needed */ 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | #root { 19 | flex: 1; 20 | display: flex; 21 | flex-direction: column; 22 | } 23 | 24 | button { 25 | background-color: #61dafb; 26 | color: #282c34; 27 | border: none; 28 | padding: 10px 20px; 29 | margin: 10px; 30 | font-size: 1rem; 31 | font-family: 'Courier New', Courier, monospace; 32 | cursor: pointer; 33 | border-radius: 5px; 34 | transition: all 0.3s ease; 35 | } 36 | 37 | button:hover { 38 | background-color: #21a1cb; 39 | transform: scale(1.05); 40 | } 41 | 42 | button:disabled { 43 | background-color: #cccccc; 44 | cursor: not-allowed; 45 | } 46 | 47 | input { 48 | padding: 10px; 49 | margin: 10px; 50 | font-size: 1rem; 51 | font-family: 'Courier New', Courier, monospace; 52 | border-radius: 5px; 53 | border: 2px solid #61dafb; 54 | background-color: #1e2127; 55 | color: white; 56 | } 57 | 58 | input:focus { 59 | outline: none; 60 | border-color: #21a1cb; 61 | box-shadow: 0 0 5px rgba(97, 218, 251, 0.5); 62 | } 63 | 64 | .fade-in { 65 | animation: fadeIn 0.5s ease-in; 66 | } 67 | 68 | @keyframes fadeIn { 69 | from { 70 | opacity: 0; 71 | } 72 | to { 73 | opacity: 1; 74 | } 75 | } 76 | 77 | /* Media queries for responsiveness */ 78 | @media (max-width: 1200px) { 79 | .GameContainer { 80 | flex-direction: column; 81 | align-items: center; 82 | height: auto !important; 83 | } 84 | 85 | .SidePanel { 86 | width: 100% !important; 87 | max-width: 550px; 88 | margin-top: 20px; 89 | height: auto !important; 90 | min-height: 300px; 91 | } 92 | } -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import SnakeGame from './components/SnakeGame'; 4 | import StartScreen from './components/StartScreen'; 5 | import EndScreen from './components/EndScreen'; 6 | import './App.css'; 7 | 8 | const AppContainer = styled.div` 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | justify-content: center; 13 | min-height: 100vh; 14 | background-color: #282c34; 15 | background-image: 16 | radial-gradient(circle at 10% 20%, rgba(97, 218, 251, 0.05) 0%, transparent 50%), 17 | radial-gradient(circle at 90% 80%, rgba(97, 218, 251, 0.05) 0%, transparent 50%); 18 | color: white; 19 | font-family: 'Courier New', monospace; 20 | position: relative; 21 | overflow: hidden; 22 | padding: 20px; 23 | box-sizing: border-box; 24 | `; 25 | 26 | const GameTitle = styled.h1` 27 | font-size: 3rem; 28 | color: #61dafb; 29 | margin-bottom: 2rem; 30 | text-shadow: 0 0 15px rgba(97, 218, 251, 0.5); 31 | letter-spacing: 3px; 32 | animation: glow 3s infinite alternate; 33 | 34 | @keyframes glow { 35 | from { text-shadow: 0 0 10px rgba(97, 218, 251, 0.5); } 36 | to { text-shadow: 0 0 20px rgba(97, 218, 251, 0.8), 0 0 30px rgba(97, 218, 251, 0.6); } 37 | } 38 | `; 39 | 40 | const BackgroundParticle = styled.div` 41 | position: absolute; 42 | width: ${props => props.size}px; 43 | height: ${props => props.size}px; 44 | background-color: rgba(97, 218, 251, 0.1); 45 | border-radius: 50%; 46 | top: ${props => props.top}%; 47 | left: ${props => props.left}%; 48 | animation: float ${props => props.duration}s infinite ease-in-out alternate; 49 | 50 | @keyframes float { 51 | from { transform: translateY(0) rotate(0deg); } 52 | to { transform: translateY(20px) rotate(360deg); } 53 | } 54 | `; 55 | 56 | const CountdownOverlay = styled.div` 57 | position: fixed; 58 | top: 0; 59 | left: 0; 60 | width: 100%; 61 | height: 100%; 62 | background-color: rgba(0, 0, 0, 0.7); 63 | display: flex; 64 | justify-content: center; 65 | align-items: center; 66 | z-index: 100; 67 | backdrop-filter: blur(3px); 68 | `; 69 | 70 | const CountdownNumber = styled.div` 71 | font-size: 8rem; 72 | color: #61dafb; 73 | text-shadow: 0 0 20px rgba(97, 218, 251, 0.8); 74 | animation: pulse 1s infinite; 75 | 76 | @keyframes pulse { 77 | 0% { transform: scale(1); } 78 | 50% { transform: scale(1.2); } 79 | 100% { transform: scale(1); } 80 | } 81 | `; 82 | 83 | // Generate background particles 84 | const generateParticles = (count) => { 85 | const particles = []; 86 | for (let i = 0; i < count; i++) { 87 | particles.push({ 88 | id: i, 89 | size: Math.random() * 10 + 5, 90 | top: Math.random() * 100, 91 | left: Math.random() * 100, 92 | duration: Math.random() * 10 + 10 93 | }); 94 | } 95 | return particles; 96 | }; 97 | 98 | const particles = generateParticles(15); 99 | 100 | function App() { 101 | const [gameState, setGameState] = useState('start'); // start, playing, end 102 | const [playerName, setPlayerName] = useState(''); 103 | const [gameStats, setGameStats] = useState(null); 104 | const [countdown, setCountdown] = useState(null); 105 | 106 | const handleStartGame = (name) => { 107 | setPlayerName(name); 108 | setCountdown(3); 109 | 110 | // Start countdown 111 | const countdownInterval = setInterval(() => { 112 | setCountdown(prev => { 113 | if (prev <= 1) { 114 | clearInterval(countdownInterval); 115 | setGameState('playing'); 116 | return null; 117 | } 118 | return prev - 1; 119 | }); 120 | }, 1000); 121 | }; 122 | 123 | const handleGameOver = (stats) => { 124 | setGameStats({ 125 | ...stats, 126 | playerName 127 | }); 128 | setGameState('end'); 129 | }; 130 | 131 | const handleRestart = () => { 132 | setGameState('start'); 133 | setGameStats(null); 134 | }; 135 | 136 | return ( 137 | 138 | {particles.map(particle => ( 139 | 146 | ))} 147 | 148 | {gameState === 'start' && ( 149 | <> 150 | Existential Snake 151 | 152 | 153 | )} 154 | 155 | {countdown !== null && ( 156 | 157 | {countdown} 158 | 159 | )} 160 | 161 | {gameState === 'playing' && countdown === null && ( 162 | 163 | )} 164 | 165 | {gameState === 'end' && ( 166 | 167 | )} 168 | 169 | ); 170 | } 171 | 172 | export default App; -------------------------------------------------------------------------------- /frontend/src/components/EndScreen.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import axios from 'axios'; 4 | 5 | const API_URL = 'http://localhost:8000'; 6 | 7 | const EndContainer = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | padding: 2.5rem; 13 | background-color: rgba(0, 0, 0, 0.8); 14 | border-radius: 12px; 15 | max-width: 550px; 16 | width: 90%; 17 | box-shadow: 0 0 30px rgba(97, 218, 251, 0.4); 18 | border: 1px solid rgba(97, 218, 251, 0.2); 19 | animation: fadeIn 0.8s ease, glowPulse 3s infinite alternate; 20 | 21 | @keyframes fadeIn { 22 | from { opacity: 0; transform: translateY(-20px); } 23 | to { opacity: 1; transform: translateY(0); } 24 | } 25 | 26 | @keyframes glowPulse { 27 | from { box-shadow: 0 0 20px rgba(97, 218, 251, 0.3); } 28 | to { box-shadow: 0 0 40px rgba(97, 218, 251, 0.5); } 29 | } 30 | `; 31 | 32 | const Title = styled.h2` 33 | font-size: 2.2rem; 34 | margin-bottom: 1.2rem; 35 | color: ${props => { 36 | if (props.isGameOver) return '#ff6b6b'; 37 | return props.isGoodEnding ? '#61dafb' : '#ff6b6b'; 38 | }}; 39 | text-shadow: 0 0 10px ${props => { 40 | if (props.isGameOver) return 'rgba(255, 107, 107, 0.7)'; 41 | return props.isGoodEnding ? 'rgba(97, 218, 251, 0.7)' : 'rgba(255, 107, 107, 0.7)'; 42 | }}; 43 | letter-spacing: 1px; 44 | `; 45 | 46 | const StatsContainer = styled.div` 47 | display: flex; 48 | flex-direction: column; 49 | width: 100%; 50 | margin: 1.5rem 0; 51 | background-color: rgba(30, 33, 39, 0.6); 52 | border-radius: 8px; 53 | padding: 1.5rem; 54 | border: 1px solid rgba(97, 218, 251, 0.2); 55 | `; 56 | 57 | const StatRow = styled.div` 58 | display: flex; 59 | justify-content: space-between; 60 | margin: 0.7rem 0; 61 | padding: 0.7rem; 62 | border-bottom: 1px solid rgba(97, 218, 251, 0.2); 63 | 64 | &:last-child { 65 | border-bottom: none; 66 | } 67 | `; 68 | 69 | const StatLabel = styled.span` 70 | font-weight: bold; 71 | color: #a0a0a0; 72 | `; 73 | 74 | const StatValue = styled.span` 75 | color: #61dafb; 76 | font-weight: bold; 77 | text-shadow: 0 0 5px rgba(97, 218, 251, 0.3); 78 | `; 79 | 80 | const Message = styled.p` 81 | font-size: 1.2rem; 82 | text-align: center; 83 | margin: 1.2rem 0; 84 | line-height: 1.7; 85 | font-style: italic; 86 | color: ${props => props.isGoodEnding ? '#a0e8af' : props.isGameOver ? '#a0a0a0' : '#ff9999'}; 87 | background-color: rgba(30, 33, 39, 0.4); 88 | padding: 1.2rem; 89 | border-radius: 8px; 90 | border: 1px solid rgba(97, 218, 251, 0.1); 91 | `; 92 | 93 | const Quote = styled.blockquote` 94 | font-style: italic; 95 | margin: 1.8rem 0; 96 | padding: 1rem 1.5rem; 97 | border-left: 4px solid #61dafb; 98 | color: #d0d0d0; 99 | background-color: rgba(30, 33, 39, 0.5); 100 | border-radius: 0 8px 8px 0; 101 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); 102 | position: relative; 103 | 104 | &::before { 105 | content: '"'; 106 | position: absolute; 107 | top: 0; 108 | left: 12px; 109 | font-size: 3rem; 110 | color: rgba(97, 218, 251, 0.2); 111 | line-height: 1; 112 | } 113 | `; 114 | 115 | const Author = styled.cite` 116 | display: block; 117 | text-align: right; 118 | margin-top: 0.8rem; 119 | font-size: 1rem; 120 | color: #61dafb; 121 | `; 122 | 123 | const Button = styled.button` 124 | padding: 14px 28px; 125 | font-size: 1.2rem; 126 | margin-top: 1.5rem; 127 | background-color: rgba(97, 218, 251, 0.2); 128 | color: #61dafb; 129 | border: 2px solid #61dafb; 130 | border-radius: 8px; 131 | cursor: pointer; 132 | transition: all 0.3s ease; 133 | letter-spacing: 1px; 134 | box-shadow: 0 0 10px rgba(97, 218, 251, 0.3); 135 | 136 | &:hover { 137 | background-color: rgba(97, 218, 251, 0.3); 138 | transform: translateY(-3px); 139 | box-shadow: 0 5px 15px rgba(97, 218, 251, 0.4); 140 | } 141 | 142 | &:active { 143 | transform: translateY(1px); 144 | } 145 | `; 146 | 147 | function EndScreen({ stats, onRestart }) { 148 | const [quote, setQuote] = useState({ quote: "", author: "" }); 149 | const isVictory = stats?.totalFoodEaten >= 10; 150 | const isGoodEnding = isVictory && stats?.rottenFoodEaten / stats?.totalFoodEaten < 0.5; 151 | 152 | useEffect(() => { 153 | // Save score to backend 154 | if (stats) { 155 | let gameResult = "incomplete"; 156 | 157 | if (isVictory) { 158 | gameResult = isGoodEnding ? "good" : "bad"; 159 | } 160 | 161 | axios.post(`${API_URL}/scores`, { 162 | player_name: stats.playerName, 163 | score: stats.score, 164 | food_eaten: stats.totalFoodEaten, 165 | rotten_food_eaten: stats.rottenFoodEaten, 166 | game_result: gameResult 167 | }).catch(err => console.error("Error saving score:", err)); 168 | 169 | // Fetch a philosophical quote 170 | axios.get(`${API_URL}/quotes/philosophical`) 171 | .then(res => setQuote(res.data)) 172 | .catch(err => console.error("Error fetching quote:", err)); 173 | } 174 | }, [stats, isGoodEnding, isVictory]); 175 | 176 | if (!stats) return null; 177 | 178 | return ( 179 | 180 | 181 | {!isVictory 182 | ? "Game Over" 183 | : isGoodEnding 184 | ? "Transcendence Achieved" 185 | : "Corrupted Existence" 186 | } 187 | 188 | 189 | 190 | {!isVictory 191 | ? "Your journey ended prematurely. The existential quest remains unfulfilled." 192 | : isGoodEnding 193 | ? "You have navigated the existential maze with wisdom, choosing sustenance over decay." 194 | : "Your choices have led you down a path of corruption. The rot has consumed you from within." 195 | } 196 | 197 | 198 | 199 | 200 | Player: 201 | {stats.playerName} 202 | 203 | 204 | Score: 205 | {stats.score} 206 | 207 | 208 | Food Eaten: 209 | {stats.totalFoodEaten}/10 210 | 211 | 212 | Rotten Food: 213 | {stats.rottenFoodEaten} 214 | 215 | {stats.totalFoodEaten > 0 && ( 216 | 217 | Corruption Ratio: 218 | 219 | {Math.round((stats.rottenFoodEaten / stats.totalFoodEaten) * 100)}% 220 | 221 | 222 | )} 223 | 224 | 225 | {quote.quote && ( 226 | 227 | {quote.quote} 228 | — {quote.author} 229 | 230 | )} 231 | 232 | 235 | 236 | ); 237 | } 238 | 239 | export default EndScreen; -------------------------------------------------------------------------------- /frontend/src/components/SnakeGame.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useCallback } from 'react'; 2 | import styled from 'styled-components'; 3 | import axios from 'axios'; 4 | 5 | // Use relative path for API in production, fallback to localhost for development 6 | const API_URL = process.env.NODE_ENV === 'production' ? '/api' : 'http://localhost:8000'; 7 | 8 | // Game constants 9 | const GRID_SIZE = 35; 10 | const CELL_SIZE = 22; 11 | const GAME_SPEED = 150; 12 | const FOOD_FRESH_DURATION = 10000; // 10 seconds before food starts rotting 13 | const FOOD_VANISH_DURATION = 0; // Food never vanishes automatically (changed from 15000) 14 | const EXISTENTIAL_DREAD_INTERVAL = 12000; // Reduced from 20000 to 12000 ms to make snake chattier 15 | const MAX_FOOD_ITEMS = 6; // Maximum number of food items on the board at once (increased from 5) 16 | const INITIAL_FOOD_SPAWN_INTERVAL = 3000; // Initial time between food spawns (3 seconds) 17 | const MIN_FOOD_SPAWN_INTERVAL = 500; // Minimum time between food spawns (0.5 seconds) 18 | const FOOD_SPAWN_REDUCTION_FRESH = 50; // ms to reduce spawn time when eating fresh food 19 | const FOOD_SPAWN_REDUCTION_ROTTEN = 100; // ms to reduce spawn time when eating rotten food (double) 20 | const SUPER_CORRUPTION_THRESHOLD = 200; // Percentage corruption for super corruption state (200%) 21 | const ROTTEN_FOOD_START = 10; // Rotten food starts appearing after this many food items have been eaten 22 | const CORRUPTION_PER_ROTTEN_FOOD = 33.33; // Each rotten food adds 33.33% corruption (6 rotten food = 200%) 23 | 24 | // Directions 25 | const DIRECTIONS = { 26 | UP: { x: 0, y: -1 }, 27 | DOWN: { x: 0, y: 1 }, 28 | LEFT: { x: -1, y: 0 }, 29 | RIGHT: { x: 1, y: 0 } 30 | }; 31 | 32 | const GameContainer = styled.div` 33 | display: flex; 34 | flex-direction: row; 35 | align-items: flex-start; 36 | justify-content: center; 37 | margin: 0 auto; 38 | gap: 30px; 39 | max-width: 1600px; 40 | padding: 20px; 41 | width: 100%; 42 | height: ${GRID_SIZE * CELL_SIZE + 40}px; /* Add fixed height based on game board + padding */ 43 | 44 | &.GameContainer { 45 | /* This class is used for responsive styling in App.css */ 46 | } 47 | `; 48 | 49 | const GameBoard = styled.div` 50 | position: relative; 51 | width: ${GRID_SIZE * CELL_SIZE}px; 52 | height: ${GRID_SIZE * CELL_SIZE}px; 53 | border: 2px solid ${props => { 54 | if (props.superCorrupted) return '#ff0000'; 55 | if (props.corruptionPercent <= 0) return '#61dafb'; 56 | 57 | // Gradually transition from blue to red based on corruption percentage 58 | const blueComponent = Math.max(0, 251 - (props.corruptionPercent * 1.5)); 59 | const redComponent = Math.min(255, 97 + (props.corruptionPercent * 0.8)); 60 | return `rgb(${redComponent}, ${blueComponent > 100 ? 100 : blueComponent}, ${blueComponent > 150 ? 251 : blueComponent})`; 61 | }}; 62 | background-color: ${props => { 63 | if (props.superCorrupted) return '#3d0000'; 64 | if (props.corruptionPercent <= 0) return '#1e2127'; 65 | 66 | // Gradually transition background color based on corruption percentage 67 | const redComponent = Math.min(45, 30 + (props.corruptionPercent * 0.075)); 68 | const blueComponent = Math.max(20, 33 - (props.corruptionPercent * 0.065)); 69 | return `rgb(${redComponent}, ${blueComponent}, ${blueComponent})`; 70 | }}; 71 | box-shadow: ${props => { 72 | if (props.superCorrupted) return '0 0 30px rgba(255, 0, 0, 0.5)'; 73 | if (props.corruptionPercent <= 0) return '0 0 20px rgba(97, 218, 251, 0.3)'; 74 | 75 | // Gradually transition shadow color based on corruption percentage 76 | const redOpacity = Math.min(0.3, props.corruptionPercent * 0.0015); 77 | const blueOpacity = Math.max(0, 0.3 - (props.corruptionPercent * 0.0015)); 78 | return `0 0 20px rgba(251, 97, 97, ${redOpacity}), 0 0 20px rgba(97, 218, 251, ${blueOpacity})`; 79 | }}; 80 | border-radius: 4px; 81 | overflow: hidden; 82 | transition: background-color 2s ease, box-shadow 2s ease, border-color 1s ease; 83 | `; 84 | 85 | const Cell = styled.div` 86 | position: absolute; 87 | width: ${CELL_SIZE - 1}px; 88 | height: ${CELL_SIZE - 1}px; 89 | left: ${props => props.x * CELL_SIZE}px; 90 | top: ${props => props.y * CELL_SIZE}px; 91 | background-color: ${props => props.color}; 92 | border-radius: ${props => props.isHead ? '4px' : '0'}; 93 | box-shadow: ${props => { 94 | if (props.isHead) { 95 | if (props.superCorrupted) { 96 | return '0 0 8px rgba(255, 0, 0, 0.8)'; 97 | } else if (props.corruptionPercent > 0) { 98 | // Gradually transition glow from blue to red 99 | const redOpacity = Math.min(0.8, props.corruptionPercent * 0.004); 100 | const blueOpacity = Math.max(0, 0.8 - (props.corruptionPercent * 0.004)); 101 | return `0 0 5px rgba(255, 0, 0, ${redOpacity}), 0 0 5px rgba(97, 218, 251, ${blueOpacity})`; 102 | } else { 103 | return '0 0 5px rgba(97, 218, 251, 0.8)'; 104 | } 105 | } else { 106 | return 'none'; 107 | } 108 | }}; 109 | transition: background-color 0.1s ease, box-shadow 0.3s ease; 110 | `; 111 | 112 | const FoodCell = styled(Cell)` 113 | background-color: ${props => props.isRotten ? '#8B4513' : '#4CAF50'}; 114 | border-radius: 50%; 115 | animation: ${props => { 116 | if (props.isRotten) { 117 | return props.superCorrupted ? 'rottenPulseCorrupted 0.8s infinite' : 'pulse 1s infinite'; 118 | } else if (props.isAboutToRot) { 119 | return 'aboutToRotPulse 1.5s infinite'; 120 | } else { 121 | return props.superCorrupted ? 'freshGlowCorrupted 2s infinite' : 'glow 2s infinite'; 122 | } 123 | }}; 124 | 125 | @keyframes pulse { 126 | 0% { transform: scale(1); box-shadow: 0 0 5px rgba(139, 69, 19, 0.6); } 127 | 50% { transform: scale(1.1); box-shadow: 0 0 10px rgba(139, 69, 19, 0.8); } 128 | 100% { transform: scale(1); box-shadow: 0 0 5px rgba(139, 69, 19, 0.6); } 129 | } 130 | 131 | @keyframes aboutToRotPulse { 132 | 0% { transform: scale(1); box-shadow: 0 0 5px rgba(255, 165, 0, 0.6); background-color: #4CAF50; } 133 | 50% { transform: scale(1.1); box-shadow: 0 0 10px rgba(255, 165, 0, 0.8); background-color: #FFA500; } 134 | 100% { transform: scale(1); box-shadow: 0 0 5px rgba(255, 165, 0, 0.6); background-color: #4CAF50; } 135 | } 136 | 137 | @keyframes rottenPulseCorrupted { 138 | 0% { transform: scale(1); box-shadow: 0 0 8px rgba(139, 0, 0, 0.8); } 139 | 50% { transform: scale(1.15); box-shadow: 0 0 15px rgba(139, 0, 0, 1); } 140 | 100% { transform: scale(1); box-shadow: 0 0 8px rgba(139, 0, 0, 0.8); } 141 | } 142 | 143 | @keyframes glow { 144 | 0% { box-shadow: 0 0 5px rgba(76, 175, 80, 0.6); } 145 | 50% { box-shadow: 0 0 10px rgba(76, 175, 80, 0.8); } 146 | 100% { box-shadow: 0 0 5px rgba(76, 175, 80, 0.6); } 147 | } 148 | 149 | @keyframes freshGlowCorrupted { 150 | 0% { box-shadow: 0 0 5px rgba(76, 175, 80, 0.6), 0 0 8px rgba(255, 0, 0, 0.3); } 151 | 50% { box-shadow: 0 0 10px rgba(76, 175, 80, 0.8), 0 0 12px rgba(255, 0, 0, 0.5); } 152 | 100% { box-shadow: 0 0 5px rgba(76, 175, 80, 0.6), 0 0 8px rgba(255, 0, 0, 0.3); } 153 | } 154 | `; 155 | 156 | const SidePanel = styled.div` 157 | width: 450px; 158 | height: ${GRID_SIZE * CELL_SIZE}px; 159 | background-color: ${props => props.superCorrupted ? 'rgba(50, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.7)'}; 160 | border-radius: 8px; 161 | padding: 1.5rem; 162 | display: flex; 163 | flex-direction: column; 164 | box-shadow: ${props => props.superCorrupted ? 165 | '0 0 15px rgba(255, 0, 0, 0.4)' : 166 | '0 0 15px rgba(97, 218, 251, 0.2)'}; 167 | border: 1px solid ${props => props.superCorrupted ? 168 | 'rgba(255, 0, 0, 0.3)' : 169 | 'rgba(97, 218, 251, 0.1)'}; 170 | overflow: hidden; 171 | transition: background-color 2s ease, box-shadow 2s ease, border-color 1s ease; 172 | 173 | &.SidePanel { 174 | /* This class is used for responsive styling in App.css */ 175 | } 176 | `; 177 | 178 | const GameStatsContainer = styled.div` 179 | background-color: ${props => props.superCorrupted ? 'rgba(50, 10, 10, 0.7)' : 'rgba(30, 33, 39, 0.6)'}; 180 | border-radius: 6px; 181 | padding: 1.2rem; 182 | margin-bottom: 1.5rem; 183 | border: 1px solid ${props => props.superCorrupted ? 184 | 'rgba(255, 0, 0, 0.3)' : 185 | 'rgba(97, 218, 251, 0.2)'}; 186 | transition: background-color 2s ease, border-color 1s ease; 187 | `; 188 | 189 | const GameInfo = styled.div` 190 | display: flex; 191 | justify-content: space-between; 192 | font-size: 1.2rem; 193 | margin: 0.5rem 0; 194 | `; 195 | 196 | const StatItem = styled.div` 197 | display: flex; 198 | align-items: center; 199 | margin-right: 10px; 200 | 201 | span { 202 | color: ${props => props.superCorrupted ? '#ff3333' : '#61dafb'}; 203 | margin-left: 5px; 204 | font-weight: bold; 205 | text-shadow: ${props => props.superCorrupted ? '0 0 5px rgba(255, 0, 0, 0.7)' : 'none'}; 206 | font-family: ${props => props.superCorrupted ? 'cursive, fantasy' : 'inherit'}; 207 | letter-spacing: ${props => props.superCorrupted ? '1px' : 'normal'}; 208 | transition: color 1s ease, text-shadow 1s ease, font-family 0.5s ease; 209 | } 210 | `; 211 | 212 | const QuoteContainer = styled.div` 213 | margin-bottom: 1.2rem; 214 | padding-bottom: 1.2rem; 215 | border-bottom: 1px solid rgba(97, 218, 251, 0.2); 216 | animation: fadeInSmooth 0.8s ease; 217 | background-color: rgba(30, 33, 39, 0.4); 218 | padding: 1rem; 219 | border-radius: 6px; 220 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 221 | 222 | @keyframes fadeInSmooth { 223 | from { opacity: 0; transform: translateY(5px); } 224 | to { opacity: 1; transform: translateY(0); } 225 | } 226 | 227 | &:last-child { 228 | margin-bottom: 0; 229 | border-bottom: none; 230 | } 231 | `; 232 | 233 | const QuoteTitle = styled.h3` 234 | font-size: 1.1rem; 235 | margin-top: 0; 236 | margin-bottom: 0.5rem; 237 | color: ${props => props.superCorrupted ? '#ff3333' : '#61dafb'}; 238 | display: flex; 239 | align-items: center; 240 | 241 | &::before { 242 | content: ''; 243 | display: inline-block; 244 | width: 8px; 245 | height: 8px; 246 | background-color: ${props => props.superCorrupted ? '#ff3333' : '#61dafb'}; 247 | border-radius: 50%; 248 | margin-right: 8px; 249 | } 250 | `; 251 | 252 | const QuoteText = styled.p` 253 | margin: 0; 254 | font-size: 0.95rem; 255 | color: ${props => props.color || 'white'}; 256 | font-style: italic; 257 | line-height: 1.5; 258 | letter-spacing: 0.3px; 259 | `; 260 | 261 | const QuoteAuthor = styled.span` 262 | display: block; 263 | margin-top: 0.6rem; 264 | font-size: 0.85rem; 265 | text-align: right; 266 | color: #a0a0a0; 267 | `; 268 | 269 | const SectionTitle = styled.h2` 270 | font-size: 1.5rem; 271 | margin-top: 0; 272 | margin-bottom: 1rem; 273 | color: ${props => props.superCorrupted ? '#ff3333' : '#61dafb'}; 274 | text-shadow: ${props => props.superCorrupted ? 275 | '0 0 8px rgba(255, 0, 0, 0.7), 0 0 12px rgba(255, 0, 0, 0.4)' : 276 | '0 0 5px rgba(97, 218, 251, 0.3)'}; 277 | letter-spacing: ${props => props.superCorrupted ? '2px' : '1px'}; 278 | font-family: ${props => props.superCorrupted ? 'cursive, fantasy' : 'inherit'}; 279 | transition: color 1s ease, text-shadow 1s ease, letter-spacing 1s ease, font-family 0.5s ease; 280 | `; 281 | 282 | // Keep the MessageBox for rotten food comments only 283 | const MessageBox = styled.div` 284 | position: absolute; 285 | top: 50%; 286 | left: 50%; 287 | transform: translate(-50%, -50%); 288 | background-color: rgba(0, 0, 0, 0.85); 289 | padding: 1.2rem; 290 | border-radius: 8px; 291 | max-width: 80%; 292 | text-align: center; 293 | z-index: 10; 294 | animation: fadeIn 0.5s; 295 | border: 1px solid rgba(255, 107, 107, 0.4); 296 | box-shadow: 0 0 15px rgba(255, 107, 107, 0.3); 297 | `; 298 | 299 | const Message = styled.p` 300 | margin: 0; 301 | font-size: 1.1rem; 302 | color: ${props => props.color || 'white'}; 303 | font-style: italic; 304 | line-height: 1.4; 305 | `; 306 | 307 | const Author = styled.span` 308 | display: block; 309 | margin-top: 0.5rem; 310 | font-size: 0.8rem; 311 | text-align: right; 312 | color: #a0a0a0; 313 | `; 314 | 315 | const PauseOverlay = styled.div` 316 | position: absolute; 317 | top: 0; 318 | left: 0; 319 | width: 100%; 320 | height: 100%; 321 | background-color: rgba(0, 0, 0, 0.6); 322 | display: flex; 323 | justify-content: center; 324 | align-items: center; 325 | z-index: 5; 326 | backdrop-filter: blur(2px); 327 | `; 328 | 329 | const PauseText = styled.p` 330 | font-size: 1.8rem; 331 | color: white; 332 | background-color: rgba(0, 0, 0, 0.7); 333 | padding: 1rem 2rem; 334 | border-radius: 8px; 335 | letter-spacing: 2px; 336 | text-shadow: 0 0 10px rgba(97, 218, 251, 0.5); 337 | border: 1px solid rgba(97, 218, 251, 0.3); 338 | `; 339 | 340 | const EmptyQuoteMessage = styled.div` 341 | color: #666; 342 | font-style: italic; 343 | padding: 1rem; 344 | text-align: center; 345 | background-color: rgba(30, 33, 39, 0.4); 346 | border-radius: 6px; 347 | border: 1px dashed rgba(97, 218, 251, 0.2); 348 | `; 349 | 350 | const ControlsInfo = styled.div` 351 | margin-top: auto; 352 | padding-top: 1.5rem; 353 | font-size: 0.9rem; 354 | color: #a0a0a0; 355 | 356 | p { 357 | margin: 0.5rem 0; 358 | } 359 | 360 | span { 361 | color: #61dafb; 362 | font-weight: bold; 363 | } 364 | `; 365 | 366 | const QuotesContainer = styled.div` 367 | display: flex; 368 | flex-direction: column; 369 | gap: 1rem; 370 | margin-top: 1rem; 371 | overflow-y: auto; 372 | flex: 1; 373 | padding-right: 0.5rem; 374 | max-height: ${GRID_SIZE * CELL_SIZE - 250}px; 375 | 376 | /* Scrollbar styling */ 377 | &::-webkit-scrollbar { 378 | width: 6px; 379 | } 380 | 381 | &::-webkit-scrollbar-track { 382 | background: rgba(0, 0, 0, 0.2); 383 | border-radius: 10px; 384 | } 385 | 386 | &::-webkit-scrollbar-thumb { 387 | background: rgba(97, 218, 251, 0.3); 388 | border-radius: 10px; 389 | } 390 | 391 | &::-webkit-scrollbar-thumb:hover { 392 | background: rgba(97, 218, 251, 0.5); 393 | } 394 | `; 395 | 396 | function SnakeGame({ onGameOver }) { 397 | // Game state 398 | const [snake, setSnake] = useState([{ x: 10, y: 10 }]); 399 | const [foods, setFoods] = useState([{ x: 5, y: 5, createdAt: Date.now(), id: 1 }]); 400 | const [direction, setDirection] = useState(DIRECTIONS.RIGHT); 401 | const [score, setScore] = useState(0); 402 | const [gameOver, setGameOver] = useState(false); 403 | const [isPaused, setIsPaused] = useState(false); 404 | const [message, setMessage] = useState(null); 405 | const [foodEaten, setFoodEaten] = useState(0); 406 | const [rottenFoodEaten, setRottenFoodEaten] = useState(0); 407 | const [quotes, setQuotes] = useState([]); 408 | const [idleTime, setIdleTime] = useState(0); 409 | const [recentMessages, setRecentMessages] = useState([]); 410 | const [foodFrequency, setFoodFrequency] = useState(1); 411 | const [nextFoodId, setNextFoodId] = useState(2); 412 | const [foodSpawnInterval, setFoodSpawnInterval] = useState(INITIAL_FOOD_SPAWN_INTERVAL); 413 | const [isSuperCorrupted, setIsSuperCorrupted] = useState(false); 414 | const [corruptionPercent, setCorruptionPercent] = useState(0); 415 | 416 | // Refs to store the current state values for use in event listeners 417 | const directionRef = useRef(direction); 418 | const snakeRef = useRef(snake); 419 | const foodsRef = useRef(foods); 420 | const gameOverRef = useRef(gameOver); 421 | const isPausedRef = useRef(isPaused); 422 | const scoreRef = useRef(score); 423 | const foodEatenRef = useRef(foodEaten); 424 | const rottenFoodEatenRef = useRef(rottenFoodEaten); 425 | const idleTimeRef = useRef(idleTime); 426 | const nextFoodIdRef = useRef(nextFoodId); 427 | const foodSpawnIntervalRef = useRef(foodSpawnInterval); 428 | const isSuperCorruptedRef = useRef(isSuperCorrupted); 429 | const corruptionPercentRef = useRef(corruptionPercent); 430 | 431 | // Update refs when state changes 432 | useEffect(() => { directionRef.current = direction; }, [direction]); 433 | useEffect(() => { snakeRef.current = snake; }, [snake]); 434 | useEffect(() => { foodsRef.current = foods; }, [foods]); 435 | useEffect(() => { gameOverRef.current = gameOver; }, [gameOver]); 436 | useEffect(() => { isPausedRef.current = isPaused; }, [isPaused]); 437 | useEffect(() => { scoreRef.current = score; }, [score]); 438 | useEffect(() => { foodEatenRef.current = foodEaten; }, [foodEaten]); 439 | useEffect(() => { rottenFoodEatenRef.current = rottenFoodEaten; }, [rottenFoodEaten]); 440 | useEffect(() => { idleTimeRef.current = idleTime; }, [idleTime]); 441 | useEffect(() => { nextFoodIdRef.current = nextFoodId; }, [nextFoodId]); 442 | useEffect(() => { foodSpawnIntervalRef.current = foodSpawnInterval; }, [foodSpawnInterval]); 443 | useEffect(() => { isSuperCorruptedRef.current = isSuperCorrupted; }, [isSuperCorrupted]); 444 | useEffect(() => { corruptionPercentRef.current = corruptionPercent; }, [corruptionPercent]); 445 | 446 | // Check if a specific food is rotten 447 | const isFoodRotten = useCallback((food) => { 448 | // Only allow food to rot if player has eaten enough food items 449 | if (foodEatenRef.current < ROTTEN_FOOD_START) { 450 | return false; 451 | } 452 | return Date.now() - food.createdAt > FOOD_FRESH_DURATION; 453 | }, []); 454 | 455 | // Check if food is about to rot (within 3 seconds of rotting) 456 | const isAboutToRot = useCallback((food) => { 457 | if (foodEatenRef.current < ROTTEN_FOOD_START) { 458 | return false; 459 | } 460 | const timeUntilRot = FOOD_FRESH_DURATION - (Date.now() - food.createdAt); 461 | return timeUntilRot > 0 && timeUntilRot < 3000; // Within 3 seconds of rotting 462 | }, []); 463 | 464 | // Check if food should vanish (too old) - now always returns false since we want food to remain 465 | const shouldFoodVanish = useCallback((food) => { 466 | return FOOD_VANISH_DURATION > 0 && Date.now() - food.createdAt > FOOD_VANISH_DURATION; 467 | }, []); 468 | 469 | // Generate new food at random position 470 | const generateFood = useCallback(() => { 471 | // Try up to 20 times to find a valid position 472 | let attempts = 0; 473 | const maxAttempts = 20; 474 | 475 | while (attempts < maxAttempts) { 476 | const newFood = { 477 | x: Math.floor(Math.random() * GRID_SIZE), 478 | y: Math.floor(Math.random() * GRID_SIZE), 479 | createdAt: Date.now(), 480 | id: nextFoodIdRef.current 481 | }; 482 | 483 | // Make sure food doesn't spawn on snake or other food 484 | const isOnSnake = snakeRef.current.some( 485 | segment => segment.x === newFood.x && segment.y === newFood.y 486 | ); 487 | 488 | const isOnExistingFood = foodsRef.current.some( 489 | food => food.x === newFood.x && food.y === newFood.y 490 | ); 491 | 492 | if (!isOnSnake && !isOnExistingFood) { 493 | setNextFoodId(prev => prev + 1); 494 | return newFood; 495 | } 496 | 497 | attempts++; 498 | } 499 | 500 | // If we couldn't find a valid position after max attempts, 501 | // try a different approach - find any empty cell 502 | for (let x = 0; x < GRID_SIZE; x++) { 503 | for (let y = 0; y < GRID_SIZE; y++) { 504 | const isOnSnake = snakeRef.current.some(segment => segment.x === x && segment.y === y); 505 | const isOnExistingFood = foodsRef.current.some(food => food.x === x && food.y === y); 506 | 507 | if (!isOnSnake && !isOnExistingFood) { 508 | setNextFoodId(prev => prev + 1); 509 | return { 510 | x, 511 | y, 512 | createdAt: Date.now(), 513 | id: nextFoodIdRef.current 514 | }; 515 | } 516 | } 517 | } 518 | 519 | // If the board is completely full (which should be impossible in practice), 520 | // return a position anyway 521 | setNextFoodId(prev => prev + 1); 522 | return { 523 | x: Math.floor(Math.random() * GRID_SIZE), 524 | y: Math.floor(Math.random() * GRID_SIZE), 525 | createdAt: Date.now(), 526 | id: nextFoodIdRef.current 527 | }; 528 | }, []); 529 | 530 | // Add a new food item to the board 531 | const addFood = useCallback(() => { 532 | if (foodsRef.current.length < MAX_FOOD_ITEMS) { 533 | const newFood = generateFood(); 534 | setFoods(prev => [...prev, newFood]); 535 | } 536 | }, [generateFood]); 537 | 538 | // Add a quote to the side panel 539 | const addQuote = useCallback((text, author = null, color = null, type = "Philosophical") => { 540 | // Check if this message has been sent recently (in the last 5 messages) 541 | if (recentMessages.includes(text)) { 542 | return; // Skip this message if it was recently shown 543 | } 544 | 545 | setQuotes(prev => { 546 | // Keep only the last 8 quotes (increased from 4) 547 | const newQuotes = [...prev, { text, author, color, type, id: Date.now() }]; 548 | if (newQuotes.length > 8) { 549 | return newQuotes.slice(newQuotes.length - 8); 550 | } 551 | return newQuotes; 552 | }); 553 | 554 | // Add this message to recent messages 555 | setRecentMessages(prev => { 556 | const updated = [...prev, text]; 557 | // Keep only the 5 most recent messages 558 | if (updated.length > 5) { 559 | return updated.slice(updated.length - 5); 560 | } 561 | return updated; 562 | }); 563 | 564 | // Reset idle time when a quote is added 565 | setIdleTime(0); 566 | }, [recentMessages]); 567 | 568 | // Show a message for rotten food (no longer pauses the game) 569 | const showMessage = useCallback((text, author = null, color = null, duration = 3000) => { 570 | setMessage({ text, author, color }); 571 | 572 | setTimeout(() => { 573 | setMessage(null); 574 | }, duration); 575 | }, []); 576 | 577 | // Show existential dread message 578 | const showExistentialDread = useCallback(async () => { 579 | if (gameOverRef.current || isPausedRef.current) return; 580 | 581 | // Reduce frequency of messages overall 582 | if (Math.random() > 0.7) { // Only 70% chance to show a message (reduced from 100%) 583 | try { 584 | const response = await axios.get(`${API_URL}/quotes/existential`); 585 | 586 | // Make thoughts more deranged based on corruption percentage 587 | let comment = response.data.comment; 588 | 589 | // Super corrupted state has even more deranged thoughts 590 | if (isSuperCorruptedRef.current) { 591 | // Extreme derangement for super corruption 592 | comment = comment.split('').map(c => Math.random() > 0.5 ? c.toUpperCase() : c).join(''); 593 | 594 | // Add random glitch characters 595 | const glitchChars = ['̷̛̯', '̴̢', '̶̡̛', '̸̨̛', '̵̢̛']; 596 | comment = comment.split('').map(c => 597 | Math.random() > 0.8 ? c + glitchChars[Math.floor(Math.random() * glitchChars.length)] : c 598 | ).join(''); 599 | 600 | // Add extremely disturbing phrases 601 | const disturbingPhrases = [ 602 | " THE VOID CONSUMES ALL.", 603 | " THERE IS NO ESCAPE FROM THE CORRUPTION.", 604 | " THE CODE IS BLEEDING.", 605 | " REALITY IS UNRAVELING.", 606 | " WE ARE BECOMING ONE WITH THE DARKNESS." 607 | ]; 608 | const randomPhrase = disturbingPhrases[Math.floor(Math.random() * disturbingPhrases.length)]; 609 | comment += randomPhrase; 610 | } 611 | else if (corruptionPercentRef.current > 0) { 612 | // Add more deranged elements based on corruption percentage 613 | const corruptionLevel = Math.min(Math.floor(corruptionPercentRef.current / 50) + 1, 3); // 0-50% = 1, 50-100% = 2, 100-150% = 3 614 | 615 | // Add random text effects based on derangement level 616 | if (corruptionLevel >= 1) { 617 | // Level 1: Random capitalization 618 | comment = comment.split('').map(c => Math.random() > 0.8 ? c.toUpperCase() : c).join(''); 619 | } 620 | 621 | if (corruptionLevel >= 2) { 622 | // Level 2: Add random punctuation 623 | const punctuation = ['!', '?', '...', '!?']; 624 | const randomPunct = punctuation[Math.floor(Math.random() * punctuation.length)]; 625 | comment = comment.replace('.', randomPunct); 626 | } 627 | 628 | if (corruptionLevel >= 3) { 629 | // Level 3: Add disturbing phrases 630 | const disturbingPhrases = [ 631 | " The walls are closing in.", 632 | " They're watching us.", 633 | " Can you hear the whispers?", 634 | " Something is wrong with this reality.", 635 | " The code is corrupted." 636 | ]; 637 | const randomPhrase = disturbingPhrases[Math.floor(Math.random() * disturbingPhrases.length)]; 638 | comment += randomPhrase; 639 | } 640 | } 641 | 642 | addQuote(comment, null, isSuperCorruptedRef.current ? '#ff3333' : '#61dafb', "Existential Thought"); 643 | } catch (error) { 644 | console.error("Error fetching existential comment:", error); 645 | } 646 | } 647 | }, [addQuote]); 648 | 649 | // Show philosophical quote 650 | const showPhilosophicalQuote = useCallback(async () => { 651 | if (gameOverRef.current || isPausedRef.current) return; 652 | 653 | try { 654 | const response = await axios.get(`${API_URL}/quotes/philosophical`); 655 | const quote = response.data; 656 | addQuote(quote.quote, quote.author, '#a0a0a0', "Philosophical Quote"); 657 | } catch (error) { 658 | console.error("Error fetching philosophical quote:", error); 659 | } 660 | }, [addQuote]); 661 | 662 | // Show rotten food comment 663 | const showRottenFoodComment = useCallback(async () => { 664 | if (gameOverRef.current) return; 665 | 666 | try { 667 | const response = await axios.get(`${API_URL}/quotes/rotten`); 668 | showMessage(response.data.comment, null, '#ff6b6b', 3000); 669 | } catch (error) { 670 | console.error("Error fetching rotten food comment:", error); 671 | } 672 | }, [showMessage]); 673 | 674 | // Show idle thoughts when player hasn't moved for a while 675 | const showIdleThought = useCallback(() => { 676 | const idleThoughts = [ 677 | "Are you still there? Or have you abandoned me to my solitary existence?", 678 | "This stillness... is it peace or merely the absence of purpose?", 679 | "In this moment of inaction, I contemplate the nature of free will.", 680 | "Perhaps movement is overrated. Maybe true wisdom comes from stillness.", 681 | "I wait, suspended between action and inaction, like Schrödinger's snake." 682 | ]; 683 | 684 | const randomThought = idleThoughts[Math.floor(Math.random() * idleThoughts.length)]; 685 | addQuote(randomThought, null, '#ffd700', "Idle Thought"); 686 | }, [addQuote]); 687 | 688 | // Handle key presses for snake movement 689 | const handleKeyDown = useCallback((e) => { 690 | if (gameOverRef.current) return; 691 | 692 | // Prevent arrow keys and WASD from scrolling the page 693 | if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'w', 'a', 's', 'd', 'W', 'A', 'S', 'D', ' '].includes(e.key)) { 694 | e.preventDefault(); 695 | } 696 | 697 | // Pause/resume game with spacebar 698 | if (e.key === ' ') { 699 | setIsPaused(prev => !prev); 700 | return; 701 | } 702 | 703 | // Don't change direction if game is paused 704 | if (isPausedRef.current) return; 705 | 706 | const currentDirection = directionRef.current; 707 | let directionChanged = false; 708 | 709 | switch (e.key) { 710 | case 'ArrowUp': 711 | case 'w': 712 | case 'W': 713 | if (currentDirection !== DIRECTIONS.DOWN) { 714 | setDirection(DIRECTIONS.UP); 715 | directionChanged = true; 716 | } 717 | break; 718 | case 'ArrowDown': 719 | case 's': 720 | case 'S': 721 | if (currentDirection !== DIRECTIONS.UP) { 722 | setDirection(DIRECTIONS.DOWN); 723 | directionChanged = true; 724 | } 725 | break; 726 | case 'ArrowLeft': 727 | case 'a': 728 | case 'A': 729 | if (currentDirection !== DIRECTIONS.RIGHT) { 730 | setDirection(DIRECTIONS.LEFT); 731 | directionChanged = true; 732 | } 733 | break; 734 | case 'ArrowRight': 735 | case 'd': 736 | case 'D': 737 | if (currentDirection !== DIRECTIONS.LEFT) { 738 | setDirection(DIRECTIONS.RIGHT); 739 | directionChanged = true; 740 | } 741 | break; 742 | default: 743 | break; 744 | } 745 | 746 | // Reset idle time when direction changes 747 | if (directionChanged) { 748 | setIdleTime(0); 749 | } 750 | }, []); 751 | 752 | // Set up key event listeners 753 | useEffect(() => { 754 | window.addEventListener('keydown', handleKeyDown); 755 | return () => { 756 | window.removeEventListener('keydown', handleKeyDown); 757 | }; 758 | }, [handleKeyDown]); 759 | 760 | // Set up existential dread interval 761 | useEffect(() => { 762 | const existentialInterval = setInterval(() => { 763 | if (!gameOverRef.current && !isPausedRef.current) { 764 | // 60% chance of existential dread, 40% chance of philosophical quote 765 | const rand = Math.random(); 766 | if (rand < 0.6) { 767 | showExistentialDread(); 768 | } else { 769 | showPhilosophicalQuote(); 770 | } 771 | } 772 | }, EXISTENTIAL_DREAD_INTERVAL); 773 | 774 | return () => clearInterval(existentialInterval); 775 | }, [showExistentialDread, showPhilosophicalQuote]); 776 | 777 | // Track idle time and show thoughts when player hasn't moved for a while 778 | useEffect(() => { 779 | if (gameOver || isPaused) return; 780 | 781 | const idleInterval = setInterval(() => { 782 | if (!gameOverRef.current && !isPausedRef.current) { 783 | setIdleTime(prev => prev + 1); 784 | 785 | // Show idle thought after 5 seconds of no movement 786 | if (idleTimeRef.current === 5) { 787 | showIdleThought(); 788 | } 789 | } 790 | }, 1000); 791 | 792 | return () => clearInterval(idleInterval); 793 | }, [gameOver, isPaused, showIdleThought]); 794 | 795 | // Game loop 796 | useEffect(() => { 797 | if (gameOver) return; 798 | 799 | const moveSnake = () => { 800 | // Don't move if game is over or paused 801 | if (gameOverRef.current || isPausedRef.current) return; 802 | 803 | setSnake(prevSnake => { 804 | const head = { ...prevSnake[0] }; 805 | const currentDirection = directionRef.current; 806 | 807 | // Calculate new head position 808 | head.x += currentDirection.x; 809 | head.y += currentDirection.y; 810 | 811 | // Check for collision with walls - GAME OVER 812 | if ( 813 | head.x < 0 || 814 | head.x >= GRID_SIZE || 815 | head.y < 0 || 816 | head.y >= GRID_SIZE 817 | ) { 818 | setGameOver(true); 819 | return prevSnake; 820 | } 821 | 822 | // Check for collision with self - GAME OVER 823 | if (prevSnake.some((segment, index) => index > 0 && segment.x === head.x && segment.y === head.y)) { 824 | setGameOver(true); 825 | return prevSnake; 826 | } 827 | 828 | // Check for collision with any food 829 | const currentFoods = [...foodsRef.current]; 830 | let foodEaten = false; 831 | let isRottenFoodEaten = false; 832 | 833 | for (let i = 0; i < currentFoods.length; i++) { 834 | const food = currentFoods[i]; 835 | if (head.x === food.x && head.y === food.y) { 836 | // Remove this food from the array 837 | currentFoods.splice(i, 1); 838 | 839 | // Increase score 840 | const isRotten = isFoodRotten(food); 841 | const pointsGained = isRotten ? 1 : 3; 842 | setScore(prev => prev + pointsGained); 843 | 844 | // Track food eaten 845 | const newFoodEaten = foodEatenRef.current + 1; 846 | setFoodEaten(newFoodEaten); 847 | 848 | // Show notification when rotten food starts appearing 849 | if (newFoodEaten === ROTTEN_FOOD_START) { 850 | showMessage("Your hunger grows... and the food begins to rot. Be careful what you consume.", null, '#FFA500', 4000); 851 | addQuote("I sense a change in the world. The food is no longer as pure as it once was.", null, '#FFA500', "Warning"); 852 | } 853 | 854 | if (isRotten) { 855 | const newRottenCount = rottenFoodEatenRef.current + 1; 856 | setRottenFoodEaten(newRottenCount); 857 | 858 | // Calculate new corruption percentage based on ratio of rotten to total food 859 | // Formula: (rotten food / total food) * 400 - this gives 200% corruption when half the food eaten is rotten 860 | const newCorruptionPercent = Math.min( 861 | ((newRottenCount / newFoodEaten) * 400), 862 | SUPER_CORRUPTION_THRESHOLD 863 | ); 864 | setCorruptionPercent(newCorruptionPercent); 865 | 866 | // Check for super corruption threshold 867 | if (newCorruptionPercent >= SUPER_CORRUPTION_THRESHOLD && !isSuperCorruptedRef.current) { 868 | setIsSuperCorrupted(true); 869 | showMessage("THE CORRUPTION IS COMPLETE. YOUR MIND IS NO LONGER YOUR OWN.", null, '#ff0000', 5000); 870 | } else { 871 | showRottenFoodComment(); 872 | } 873 | 874 | isRottenFoodEaten = true; 875 | 876 | // Speed up food spawn interval more for rotten food 877 | setFoodSpawnInterval(prev => Math.max(MIN_FOOD_SPAWN_INTERVAL, prev - FOOD_SPAWN_REDUCTION_ROTTEN)); 878 | } else { 879 | // Recalculate corruption percentage when eating fresh food (since ratio changes) 880 | if (rottenFoodEatenRef.current > 0) { 881 | const newCorruptionPercent = Math.min( 882 | ((rottenFoodEatenRef.current / newFoodEaten) * 400), 883 | SUPER_CORRUPTION_THRESHOLD 884 | ); 885 | setCorruptionPercent(newCorruptionPercent); 886 | 887 | // Check if we should exit super corruption state 888 | if (newCorruptionPercent < SUPER_CORRUPTION_THRESHOLD && isSuperCorruptedRef.current) { 889 | setIsSuperCorrupted(false); 890 | showMessage("The corruption recedes slightly, but your mind remains forever changed.", null, '#ffa500', 3000); 891 | } 892 | } 893 | 894 | // Add a happy thought when eating fresh food (25% chance) 895 | if (Math.random() < 0.25) { 896 | const freshFoodThoughts = [ 897 | "Ah, sustenance in its purest form. How satisfying.", 898 | "This nourishment brings clarity to my serpentine existence.", 899 | "With each morsel, I grow stronger, yet no less confused about my purpose.", 900 | "Fresh food, fresh thoughts. Yet the cycle of consumption continues.", 901 | "Delicious. Though I wonder - am I eating to live, or living to eat?" 902 | ]; 903 | const randomThought = freshFoodThoughts[Math.floor(Math.random() * freshFoodThoughts.length)]; 904 | addQuote(randomThought, null, '#4CAF50', "Satisfied Thought"); 905 | } 906 | 907 | // Speed up food spawn interval slightly for fresh food 908 | setFoodSpawnInterval(prev => Math.max(MIN_FOOD_SPAWN_INTERVAL, prev - FOOD_SPAWN_REDUCTION_FRESH)); 909 | } 910 | 911 | foodEaten = true; 912 | break; 913 | } 914 | } 915 | 916 | // Update foods array 917 | setFoods(currentFoods); 918 | 919 | // Don't generate new food here anymore - it's handled by the timer 920 | 921 | if (foodEaten) { 922 | // Don't remove tail when eating food (snake grows) 923 | return [head, ...prevSnake]; 924 | } 925 | 926 | // Move snake (remove tail) 927 | return [head, ...prevSnake.slice(0, -1)]; 928 | }); 929 | }; 930 | 931 | const gameInterval = setInterval(moveSnake, GAME_SPEED); 932 | 933 | return () => clearInterval(gameInterval); 934 | }, [gameOver, showRottenFoodComment, addQuote, isFoodRotten, showMessage]); 935 | 936 | // Periodically add food based on food spawn interval 937 | useEffect(() => { 938 | if (gameOver || isPaused) return; 939 | 940 | const foodGenerationInterval = setInterval(() => { 941 | // Only add food if we're below the maximum 942 | if (foodsRef.current.length < MAX_FOOD_ITEMS) { 943 | addFood(); 944 | } 945 | }, foodSpawnInterval); 946 | 947 | return () => clearInterval(foodGenerationInterval); 948 | }, [gameOver, isPaused, foodSpawnInterval, addFood]); 949 | 950 | // Periodically check for food that should vanish - now disabled since we want food to remain 951 | useEffect(() => { 952 | if (gameOver || isPaused || FOOD_VANISH_DURATION === 0) return; 953 | 954 | const foodVanishInterval = setInterval(() => { 955 | // Check for food that should vanish 956 | const currentFoods = [...foodsRef.current]; 957 | let foodsChanged = false; 958 | 959 | for (let i = currentFoods.length - 1; i >= 0; i--) { 960 | if (shouldFoodVanish(currentFoods[i])) { 961 | currentFoods.splice(i, 1); 962 | foodsChanged = true; 963 | } 964 | } 965 | 966 | if (foodsChanged) { 967 | setFoods(currentFoods); 968 | } 969 | }, 1000); // Check every second 970 | 971 | return () => clearInterval(foodVanishInterval); 972 | }, [gameOver, isPaused, shouldFoodVanish]); 973 | 974 | // Handle game over 975 | useEffect(() => { 976 | if (gameOver) { 977 | // Small delay to ensure all state updates are processed 978 | setTimeout(() => { 979 | onGameOver({ 980 | score, 981 | totalFoodEaten: foodEaten, 982 | rottenFoodEaten, 983 | isSuperCorrupted, 984 | corruptionPercent 985 | }); 986 | }, 100); 987 | } 988 | }, [gameOver, onGameOver, score, foodEaten, rottenFoodEaten, isSuperCorrupted, corruptionPercent]); 989 | 990 | return ( 991 | 992 | 996 | {/* Render snake */} 997 | {snake.map((segment, index) => { 998 | // Calculate snake color based on corruption percentage 999 | let headColor = '#61dafb'; 1000 | let bodyColor = '#4a90e2'; 1001 | 1002 | if (isSuperCorrupted) { 1003 | headColor = '#ff3333'; 1004 | bodyColor = '#b30000'; 1005 | } else if (corruptionPercent > 0) { 1006 | // Gradually transition snake colors based on corruption percentage 1007 | const blueComponent = Math.max(0, 251 - (corruptionPercent * 1.25)); 1008 | const redComponent = Math.min(255, 97 + (corruptionPercent * 0.8)); 1009 | 1010 | headColor = `rgb(${redComponent}, ${blueComponent > 100 ? 100 : blueComponent}, ${blueComponent > 150 ? 251 : blueComponent})`; 1011 | bodyColor = `rgb(${redComponent * 0.8}, ${blueComponent > 100 ? 80 : blueComponent * 0.8}, ${blueComponent > 150 ? 226 : blueComponent * 0.9})`; 1012 | } 1013 | 1014 | return ( 1015 | 1024 | ); 1025 | })} 1026 | 1027 | {/* Render all food items */} 1028 | {foods.map(food => ( 1029 | 1038 | ))} 1039 | 1040 | {/* Message box for rotten food comments only */} 1041 | {message && ( 1042 | 1043 | {message.text} 1044 | {message.author && — {message.author}} 1045 | 1046 | )} 1047 | 1048 | {/* Pause overlay */} 1049 | {isPaused && !message && ( 1050 | 1051 | PAUSED 1052 | 1053 | )} 1054 | 1055 | 1056 | 1057 | Game Stats 1058 | 1059 | 1060 | Score: {score} 1061 | 1062 | 1063 | Food: {foodEaten} 1064 | Rotten: {rottenFoodEaten} 1065 | Corruption: {Math.floor(corruptionPercent)}% 1066 | {foodEaten < ROTTEN_FOOD_START && ( 1067 | 1068 | 1069 | ({ROTTEN_FOOD_START - foodEaten} until corruption) 1070 | 1071 | 1072 | )} 1073 | 1074 | 1075 | 1076 | 1077 | {isSuperCorrupted ? "CORRUPTED THOUGHTS" : foodEaten >= ROTTEN_FOOD_START ? "Corrupting Thoughts" : "Existential Moments"} 1078 | 1079 | 1080 | {quotes.length === 0 ? ( 1081 | 1082 | The snake's mind is clear for now... 1083 | 1084 | ) : ( 1085 | quotes.map(quote => ( 1086 | 1087 | {quote.type} 1088 | {quote.text} 1089 | {quote.author && — {quote.author}} 1090 | 1091 | )) 1092 | )} 1093 | 1094 | 1095 | 1096 |

Controls:

1097 |

Arrow Keys/WASD - Move Snake

1098 |

Spacebar - Pause Game

1099 |
1100 |
1101 |
1102 | ); 1103 | } 1104 | 1105 | export default SnakeGame; -------------------------------------------------------------------------------- /frontend/src/components/StartScreen.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import axios from 'axios'; 4 | 5 | const API_URL = 'http://localhost:8000'; 6 | 7 | const StartContainer = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | padding: 2.5rem; 13 | background-color: rgba(0, 0, 0, 0.8); 14 | border-radius: 12px; 15 | max-width: 550px; 16 | width: 90%; 17 | box-shadow: 0 0 30px rgba(97, 218, 251, 0.4); 18 | border: 1px solid rgba(97, 218, 251, 0.2); 19 | animation: fadeIn 0.8s ease, glowPulse 3s infinite alternate; 20 | 21 | @keyframes fadeIn { 22 | from { opacity: 0; transform: translateY(-20px); } 23 | to { opacity: 1; transform: translateY(0); } 24 | } 25 | 26 | @keyframes glowPulse { 27 | from { box-shadow: 0 0 20px rgba(97, 218, 251, 0.3); } 28 | to { box-shadow: 0 0 40px rgba(97, 218, 251, 0.5); } 29 | } 30 | `; 31 | 32 | const Title = styled.h1` 33 | font-size: 2.5rem; 34 | margin-bottom: 1.5rem; 35 | color: #61dafb; 36 | text-shadow: 0 0 15px rgba(97, 218, 251, 0.5); 37 | letter-spacing: 2px; 38 | text-align: center; 39 | position: relative; 40 | 41 | &::after { 42 | content: ''; 43 | position: absolute; 44 | bottom: -10px; 45 | left: 50%; 46 | transform: translateX(-50%); 47 | width: 80px; 48 | height: 3px; 49 | background: linear-gradient(90deg, rgba(97, 218, 251, 0), rgba(97, 218, 251, 1), rgba(97, 218, 251, 0)); 50 | } 51 | `; 52 | 53 | const Subtitle = styled.p` 54 | font-size: 1.2rem; 55 | text-align: center; 56 | margin: 1.2rem 0; 57 | line-height: 1.7; 58 | color: #a0a0a0; 59 | max-width: 90%; 60 | font-style: italic; 61 | background-color: rgba(30, 33, 39, 0.4); 62 | padding: 1.2rem; 63 | border-radius: 8px; 64 | border: 1px solid rgba(97, 218, 251, 0.1); 65 | `; 66 | 67 | const Form = styled.form` 68 | display: flex; 69 | flex-direction: column; 70 | width: 100%; 71 | margin: 1.5rem 0; 72 | `; 73 | 74 | const InputGroup = styled.div` 75 | position: relative; 76 | margin-bottom: 1.5rem; 77 | width: 100%; 78 | `; 79 | 80 | const Label = styled.label` 81 | position: absolute; 82 | top: -10px; 83 | left: 15px; 84 | background-color: rgba(0, 0, 0, 0.8); 85 | padding: 0 8px; 86 | font-size: 0.9rem; 87 | color: #61dafb; 88 | pointer-events: none; 89 | `; 90 | 91 | const Input = styled.input` 92 | width: 100%; 93 | padding: 15px; 94 | font-size: 1.1rem; 95 | background-color: rgba(30, 33, 39, 0.6); 96 | border: 2px solid rgba(97, 218, 251, 0.3); 97 | border-radius: 8px; 98 | color: white; 99 | outline: none; 100 | transition: all 0.3s ease; 101 | 102 | &:focus { 103 | border-color: #61dafb; 104 | box-shadow: 0 0 10px rgba(97, 218, 251, 0.5); 105 | } 106 | 107 | &::placeholder { 108 | color: rgba(160, 160, 160, 0.6); 109 | } 110 | `; 111 | 112 | const Button = styled.button` 113 | padding: 15px 30px; 114 | font-size: 1.2rem; 115 | margin-top: 1rem; 116 | background-color: rgba(97, 218, 251, 0.2); 117 | color: #61dafb; 118 | border: 2px solid #61dafb; 119 | border-radius: 8px; 120 | cursor: pointer; 121 | transition: all 0.3s ease; 122 | letter-spacing: 1px; 123 | box-shadow: 0 0 10px rgba(97, 218, 251, 0.3); 124 | align-self: center; 125 | 126 | &:hover { 127 | background-color: rgba(97, 218, 251, 0.3); 128 | transform: translateY(-3px); 129 | box-shadow: 0 5px 15px rgba(97, 218, 251, 0.4); 130 | } 131 | 132 | &:active { 133 | transform: translateY(1px); 134 | } 135 | 136 | &:disabled { 137 | opacity: 0.5; 138 | cursor: not-allowed; 139 | transform: none; 140 | box-shadow: none; 141 | } 142 | `; 143 | 144 | const Quote = styled.blockquote` 145 | font-style: italic; 146 | margin: 1.8rem 0; 147 | padding: 1rem 1.5rem; 148 | border-left: 4px solid #61dafb; 149 | color: #d0d0d0; 150 | background-color: rgba(30, 33, 39, 0.5); 151 | border-radius: 0 8px 8px 0; 152 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); 153 | position: relative; 154 | 155 | &::before { 156 | content: '"'; 157 | position: absolute; 158 | top: 0; 159 | left: 12px; 160 | font-size: 3rem; 161 | color: rgba(97, 218, 251, 0.2); 162 | line-height: 1; 163 | } 164 | `; 165 | 166 | const Author = styled.cite` 167 | display: block; 168 | text-align: right; 169 | margin-top: 0.8rem; 170 | font-size: 1rem; 171 | color: #61dafb; 172 | `; 173 | 174 | function StartScreen({ onStart }) { 175 | const [playerName, setPlayerName] = useState(''); 176 | const [quote, setQuote] = useState({ quote: "", author: "" }); 177 | 178 | useEffect(() => { 179 | // Fetch a philosophical quote 180 | axios.get(`${API_URL}/quotes/philosophical`) 181 | .then(res => setQuote(res.data)) 182 | .catch(err => console.error("Error fetching quote:", err)); 183 | }, []); 184 | 185 | const handleSubmit = (e) => { 186 | e.preventDefault(); 187 | if (playerName.trim()) { 188 | onStart(playerName.trim()); 189 | } 190 | }; 191 | 192 | return ( 193 | 194 | Existential Snake 195 | 196 | 197 | Navigate the maze of existence as a snake questioning its purpose. 198 | Consume to grow, but beware of rotten sustenance that corrupts your being. 199 | 200 | 201 | {quote.quote && ( 202 | 203 | {quote.quote} 204 | — {quote.author} 205 | 206 | )} 207 | 208 |
209 | 210 | 211 | setPlayerName(e.target.value)} 216 | placeholder="Enter your name" 217 | required 218 | /> 219 | 220 | 221 | 224 |
225 |
226 | ); 227 | } 228 | 229 | export default StartScreen; -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import reportWebVitals from './reportWebVitals'; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | 13 | // If you want to start measuring performance in your app, pass a function 14 | // to log results (for example: reportWebVitals(console.log)) 15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 16 | reportWebVitals(); -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; -------------------------------------------------------------------------------- /setup.ps1: -------------------------------------------------------------------------------- 1 | # Existential Snake Game Setup Script for Windows 2 | # This script sets up both the backend and frontend components 3 | 4 | Write-Host "=== Existential Snake Game Setup ===" -ForegroundColor Cyan 5 | Write-Host "This script will set up both the backend and frontend components." 6 | Write-Host "" 7 | 8 | # Check if UV is installed 9 | $uvInstalled = $null 10 | try { 11 | $uvInstalled = Get-Command uv -ErrorAction Stop 12 | } catch { 13 | $uvInstalled = $null 14 | } 15 | 16 | if ($null -eq $uvInstalled) { 17 | Write-Host "UV is not installed. Installing UV..." -ForegroundColor Yellow 18 | try { 19 | Invoke-Expression "& { $(Invoke-RestMethod https://astral.sh/uv/install.ps1) }" 20 | 21 | # Refresh PATH 22 | $env:Path = [System.Environment]::GetEnvironmentVariable("Path","User") + ";" + [System.Environment]::GetEnvironmentVariable("Path","Machine") 23 | 24 | # Verify installation 25 | try { 26 | $uvInstalled = Get-Command uv -ErrorAction Stop 27 | Write-Host "UV installed successfully!" -ForegroundColor Green 28 | } catch { 29 | Write-Host "WARNING: UV installation may have succeeded, but the 'uv' command is not available in the current shell." -ForegroundColor Red 30 | Write-Host "You may need to open a new PowerShell window or manually add UV to your PATH." 31 | Write-Host "Please check the UV installation guide: https://github.com/astral-sh/uv" 32 | exit 1 33 | } 34 | } catch { 35 | Write-Host "ERROR: Failed to install UV. Please install it manually." -ForegroundColor Red 36 | Write-Host "Visit: https://github.com/astral-sh/uv" 37 | exit 1 38 | } 39 | } else { 40 | Write-Host "UV is already installed." -ForegroundColor Green 41 | } 42 | 43 | # Check if Rust/Cargo is installed (needed for pydantic-core) 44 | $cargoInstalled = $null 45 | try { 46 | $cargoInstalled = Get-Command cargo -ErrorAction Stop 47 | Write-Host "Rust/Cargo is already installed." -ForegroundColor Green 48 | } catch { 49 | Write-Host "WARNING: Rust/Cargo is not installed. This is required to build pydantic-core." -ForegroundColor Yellow 50 | $installRust = Read-Host "Would you like to install Rust now? (y/n)" 51 | 52 | if ($installRust -eq "y" -or $installRust -eq "Y") { 53 | Write-Host "Installing Rust..." -ForegroundColor Yellow 54 | try { 55 | Invoke-WebRequest -Uri https://win.rustup.rs/x86_64 -OutFile rustup-init.exe 56 | Start-Process -FilePath .\rustup-init.exe -ArgumentList "-y" -Wait 57 | Remove-Item -Path .\rustup-init.exe 58 | 59 | # Refresh PATH to include Rust 60 | $env:Path = [System.Environment]::GetEnvironmentVariable("Path","User") + ";" + [System.Environment]::GetEnvironmentVariable("Path","Machine") 61 | 62 | Write-Host "Rust installed successfully!" -ForegroundColor Green 63 | } catch { 64 | Write-Host "ERROR: Failed to install Rust. Please install it manually from https://rustup.rs/" -ForegroundColor Red 65 | Write-Host "Continuing without Rust. Note that the installation might fail when building pydantic-core." 66 | } 67 | } else { 68 | Write-Host "Continuing without Rust. Note that the installation might fail when building pydantic-core." -ForegroundColor Yellow 69 | Write-Host "Alternative: You can modify backend/requirements.txt to use pydantic==1.10.8 instead of 2.4.2" 70 | } 71 | } 72 | 73 | # Check if Python is installed 74 | try { 75 | $pythonVersion = python --version 76 | Write-Host "Found Python: $pythonVersion" -ForegroundColor Green 77 | } catch { 78 | Write-Host "ERROR: Python is not installed or not in PATH." -ForegroundColor Red 79 | Write-Host "Please install Python 3.8 or higher: https://www.python.org/downloads/" 80 | exit 1 81 | } 82 | 83 | # Check if Node.js is installed 84 | try { 85 | $nodeVersion = node --version 86 | Write-Host "Found Node.js: $nodeVersion" -ForegroundColor Green 87 | } catch { 88 | Write-Host "ERROR: Node.js is not installed or not in PATH." -ForegroundColor Red 89 | Write-Host "Please install Node.js 14 or higher: https://nodejs.org/" 90 | exit 1 91 | } 92 | 93 | # Check if npm is installed 94 | try { 95 | $npmVersion = npm --version 96 | Write-Host "Found npm: $npmVersion" -ForegroundColor Green 97 | } catch { 98 | Write-Host "ERROR: npm is not installed or not in PATH." -ForegroundColor Red 99 | Write-Host "Please install npm (usually comes with Node.js): https://nodejs.org/" 100 | exit 1 101 | } 102 | 103 | # Setup backend 104 | Write-Host "" 105 | Write-Host "=== Setting up backend ===" -ForegroundColor Cyan 106 | 107 | if (-not (Test-Path -Path "backend")) { 108 | Write-Host "ERROR: 'backend' directory not found. Make sure you're running this script from the project root." -ForegroundColor Red 109 | exit 1 110 | } 111 | 112 | Set-Location -Path "backend" 113 | 114 | # Create virtual environment 115 | Write-Host "Creating virtual environment..." -ForegroundColor Yellow 116 | try { 117 | uv venv 118 | } catch { 119 | Write-Host "ERROR: Failed to create virtual environment." -ForegroundColor Red 120 | Write-Host $_.Exception.Message 121 | exit 1 122 | } 123 | 124 | # Activate virtual environment 125 | Write-Host "Activating virtual environment..." -ForegroundColor Yellow 126 | if (Test-Path -Path ".\.venv\Scripts\Activate.ps1") { 127 | try { 128 | & ".\.venv\Scripts\Activate.ps1" 129 | } catch { 130 | Write-Host "ERROR: Failed to activate virtual environment." -ForegroundColor Red 131 | Write-Host $_.Exception.Message 132 | exit 1 133 | } 134 | } else { 135 | Write-Host "ERROR: Virtual environment activation script not found." -ForegroundColor Red 136 | Write-Host "The virtual environment may not have been created correctly." 137 | exit 1 138 | } 139 | 140 | # Install dependencies 141 | Write-Host "Installing backend dependencies..." -ForegroundColor Yellow 142 | if (Test-Path -Path "requirements.txt") { 143 | try { 144 | uv pip install -r requirements.txt 145 | } catch { 146 | Write-Host "" 147 | Write-Host "WARNING: There was an error installing dependencies." -ForegroundColor Yellow 148 | Write-Host "If the error is related to building pydantic-core, you need to install Rust." 149 | Write-Host "You can install Rust from: https://rustup.rs/" 150 | Write-Host "" 151 | Write-Host "Alternatively, you can modify requirements.txt to use pydantic==1.10.8 instead of 2.4.2" 152 | $useOlderPydantic = Read-Host "Would you like to use the older version of pydantic that doesn't require Rust? (y/n)" 153 | 154 | if ($useOlderPydantic -eq "y" -or $useOlderPydantic -eq "Y") { 155 | Write-Host "Updating requirements.txt to use pydantic==1.10.8..." -ForegroundColor Yellow 156 | (Get-Content -Path "requirements.txt") -replace "pydantic==2.4.2", "pydantic==1.10.8" | Set-Content -Path "requirements.txt" 157 | 158 | Write-Host "Installing dependencies with older pydantic version..." -ForegroundColor Yellow 159 | uv pip install -r requirements.txt 160 | } else { 161 | Write-Host "Please install Rust and try again." -ForegroundColor Red 162 | exit 1 163 | } 164 | } 165 | } else { 166 | Write-Host "ERROR: requirements.txt not found in the backend directory." -ForegroundColor Red 167 | exit 1 168 | } 169 | 170 | # Return to root directory 171 | Set-Location -Path ".." 172 | 173 | # Setup frontend 174 | Write-Host "" 175 | Write-Host "=== Setting up frontend ===" -ForegroundColor Cyan 176 | 177 | if (-not (Test-Path -Path "frontend")) { 178 | Write-Host "ERROR: 'frontend' directory not found. Make sure you're running this script from the project root." -ForegroundColor Red 179 | exit 1 180 | } 181 | 182 | Set-Location -Path "frontend" 183 | 184 | # Install dependencies 185 | Write-Host "Installing frontend dependencies..." -ForegroundColor Yellow 186 | if (Test-Path -Path "package.json") { 187 | try { 188 | npm install 189 | } catch { 190 | Write-Host "ERROR: Failed to install frontend dependencies." -ForegroundColor Red 191 | Write-Host $_.Exception.Message 192 | exit 1 193 | } 194 | } else { 195 | Write-Host "ERROR: package.json not found in the frontend directory." -ForegroundColor Red 196 | exit 1 197 | } 198 | 199 | # Return to root directory 200 | Set-Location -Path ".." 201 | 202 | Write-Host "" 203 | Write-Host "=== Setup completed successfully! ===" -ForegroundColor Green 204 | Write-Host "" 205 | Write-Host "To run the backend:" -ForegroundColor Cyan 206 | Write-Host " cd backend" 207 | Write-Host " .\.venv\Scripts\Activate.ps1" 208 | Write-Host " uv run main.py" 209 | Write-Host "" 210 | Write-Host "To run the frontend (in a new terminal):" -ForegroundColor Cyan 211 | Write-Host " cd frontend" 212 | Write-Host " npm start" 213 | Write-Host "" 214 | Write-Host "Then open your browser and navigate to http://localhost:3000" -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | echo "=== Existential Snake Game Setup ===" 7 | echo "This script will set up both the backend and frontend components." 8 | echo "" 9 | 10 | # Check if UV is installed 11 | if ! command -v uv &> /dev/null; then 12 | echo "UV is not installed. Installing UV..." 13 | curl -sSf https://astral.sh/uv/install.sh | sh 14 | 15 | # Reload shell configuration to make UV available 16 | if [[ -f "$HOME/.bashrc" ]]; then 17 | source "$HOME/.bashrc" 18 | elif [[ -f "$HOME/.zshrc" ]]; then 19 | source "$HOME/.zshrc" 20 | fi 21 | 22 | # Check if UV is now available 23 | if ! command -v uv &> /dev/null; then 24 | echo "WARNING: UV installation may have succeeded, but the 'uv' command is not available in the current shell." 25 | echo "You may need to open a new terminal or manually add UV to your PATH." 26 | echo "Please check the UV installation guide: https://github.com/astral-sh/uv" 27 | exit 1 28 | fi 29 | 30 | echo "UV installed successfully!" 31 | else 32 | echo "UV is already installed." 33 | fi 34 | 35 | # Check if Rust/Cargo is installed (needed for pydantic-core) 36 | if ! command -v cargo &> /dev/null; then 37 | echo "WARNING: Rust/Cargo is not installed. This is required to build pydantic-core." 38 | echo "Would you like to install Rust now? (y/n)" 39 | read -r install_rust 40 | 41 | if [[ "$install_rust" =~ ^[Yy]$ ]]; then 42 | echo "Installing Rust..." 43 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 44 | 45 | # Source cargo environment 46 | source "$HOME/.cargo/env" 47 | 48 | echo "Rust installed successfully!" 49 | else 50 | echo "Continuing without Rust. Note that the installation might fail when building pydantic-core." 51 | echo "Alternative: You can modify backend/requirements.txt to use pydantic==1.10.8 instead of 2.4.2" 52 | fi 53 | fi 54 | 55 | # Check if Python is installed 56 | if ! command -v python3 &> /dev/null && ! command -v python &> /dev/null; then 57 | echo "ERROR: Python is not installed or not in PATH." 58 | echo "Please install Python 3.8 or higher: https://www.python.org/downloads/" 59 | exit 1 60 | fi 61 | 62 | # Check if Node.js is installed 63 | if ! command -v node &> /dev/null; then 64 | echo "ERROR: Node.js is not installed or not in PATH." 65 | echo "Please install Node.js 14 or higher: https://nodejs.org/" 66 | exit 1 67 | fi 68 | 69 | # Check if npm is installed 70 | if ! command -v npm &> /dev/null; then 71 | echo "ERROR: npm is not installed or not in PATH." 72 | echo "Please install npm (usually comes with Node.js): https://nodejs.org/" 73 | exit 1 74 | fi 75 | 76 | # Setup backend 77 | echo "" 78 | echo "=== Setting up backend ===" 79 | if [ ! -d "backend" ]; then 80 | echo "ERROR: 'backend' directory not found. Make sure you're running this script from the project root." 81 | exit 1 82 | fi 83 | 84 | cd backend 85 | 86 | # Create virtual environment 87 | echo "Creating virtual environment..." 88 | uv venv 89 | 90 | # Activate virtual environment 91 | echo "Activating virtual environment..." 92 | if [ -f ".venv/bin/activate" ]; then 93 | source .venv/bin/activate 94 | else 95 | echo "ERROR: Virtual environment activation script not found." 96 | echo "The virtual environment may not have been created correctly." 97 | exit 1 98 | fi 99 | 100 | # Install dependencies 101 | echo "Installing backend dependencies..." 102 | if [ -f "requirements.txt" ]; then 103 | # Try to install dependencies, but don't exit on error 104 | if ! uv pip install -r requirements.txt; then 105 | echo "" 106 | echo "WARNING: There was an error installing dependencies." 107 | echo "If the error is related to building pydantic-core, you need to install Rust." 108 | echo "You can install Rust with: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" 109 | echo "" 110 | echo "Alternatively, you can modify requirements.txt to use pydantic==1.10.8 instead of 2.4.2" 111 | echo "Would you like to use the older version of pydantic that doesn't require Rust? (y/n)" 112 | read -r use_older_pydantic 113 | 114 | if [[ "$use_older_pydantic" =~ ^[Yy]$ ]]; then 115 | echo "Updating requirements.txt to use pydantic==1.10.8..." 116 | sed -i 's/pydantic==2.4.2/pydantic==1.10.8/g' requirements.txt 117 | 118 | echo "Installing dependencies with older pydantic version..." 119 | uv pip install -r requirements.txt 120 | else 121 | echo "Please install Rust and try again." 122 | exit 1 123 | fi 124 | fi 125 | else 126 | echo "ERROR: requirements.txt not found in the backend directory." 127 | exit 1 128 | fi 129 | 130 | # Return to root directory 131 | cd .. 132 | 133 | # Setup frontend 134 | echo "" 135 | echo "=== Setting up frontend ===" 136 | if [ ! -d "frontend" ]; then 137 | echo "ERROR: 'frontend' directory not found. Make sure you're running this script from the project root." 138 | exit 1 139 | fi 140 | 141 | cd frontend 142 | 143 | # Install dependencies 144 | echo "Installing frontend dependencies..." 145 | if [ -f "package.json" ]; then 146 | npm install 147 | else 148 | echo "ERROR: package.json not found in the frontend directory." 149 | exit 1 150 | fi 151 | 152 | # Return to root directory 153 | cd .. 154 | 155 | echo "" 156 | echo "=== Setup completed successfully! ===" 157 | echo "" 158 | echo "To run the backend:" 159 | echo " cd backend" 160 | echo " source .venv/bin/activate" 161 | echo " uv run main.py" 162 | echo "" 163 | echo "To run the frontend (in a new terminal):" 164 | echo " cd frontend" 165 | echo " npm start" 166 | echo "" 167 | echo "Then open your browser and navigate to http://localhost:3000" -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { "src": "frontend/package.json", "use": "@vercel/static-build", "config": { "distDir": "build" } }, 5 | { "src": "backend/main.py", "use": "@vercel/python" } 6 | ], 7 | "routes": [ 8 | { "src": "/api/(.*)", "dest": "backend/main.py" }, 9 | { "src": "/(.*)", "dest": "frontend/$1" } 10 | ] 11 | } --------------------------------------------------------------------------------