├── .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 |
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 | }
--------------------------------------------------------------------------------