Compare commits
5 Commits
fc9d88c995
...
eb6abf9c8c
| Author | SHA1 | Date | |
|---|---|---|---|
| eb6abf9c8c | |||
|
|
0dba12d694 | ||
|
|
fc4ec11521 | ||
|
|
020603d173 | ||
|
|
7ec5ae1664 |
@ -1,40 +1,9 @@
|
|||||||
# Version control
|
__pycache__
|
||||||
.git/
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
|
tests/
|
||||||
# Environment files
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
*.env
|
|
||||||
|
|
||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*.egg-info/
|
|
||||||
.eggs/
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
.venv/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*~
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Test coverage
|
|
||||||
coverage.xml
|
|
||||||
htmlcov/
|
|
||||||
.pytest_cache/
|
|
||||||
|
|
||||||
# Data (runtime only)
|
|
||||||
data/
|
|
||||||
logs/
|
|
||||||
|
|
||||||
# Documentation (not needed in container)
|
|
||||||
*.md
|
*.md
|
||||||
prompt.md
|
uv.lock
|
||||||
|
.venv
|
||||||
|
|||||||
155
README.md
155
README.md
@ -1,148 +1,41 @@
|
|||||||
# Arcane Purge
|
# Exorcist Game (Arcane Purge)
|
||||||
|
|
||||||
A text-based exorcist game where Bible verses are your weapons against demonic forces. Type verses from Scripture to cast spells, pass memory tests to defeat demons, and unlock progressively more powerful verses as you advance.
|
A text-based exorcist game where Bible verses are the spell mechanics. Fight through a gauntlet of escalating demons by matching verse categories to each demon's weaknesses.
|
||||||
|
|
||||||
## What It Does
|
> Personal prototype project — functional, but not polished.
|
||||||
|
|
||||||
Arcane Purge is a terminal-based game that combines Scripture memorization with demon-slaying gameplay:
|
## Gameplay
|
||||||
|
|
||||||
- **Verse Casting** - Select a verse, then type it exactly to deal damage to demons. Verses matching a demon's weakness deal double damage.
|
- Terminal-based combat loop with timer pressure
|
||||||
- **Memory Tests** - After a successful cast, recall the same verse without looking. Passing delivers a powerful finishing strike.
|
- Six escalating demon encounters, each with health, damage, and elemental weaknesses
|
||||||
- **Progression** - Start with 4 easy verses. Defeating demons unlocks harder verses and new categories. After 5 demons, Memory Mode activates.
|
- Verse-based spell system: recite verses from the right category (Binding, Healing, Exorcism, Protection, Judgment) to deal damage
|
||||||
- **Scoring** - Points awarded for correct casts, speed bonuses, memory test success, and demon defeats. Deducted for failures.
|
- Mismatched categories fizzle — read the demon, pick the verse
|
||||||
- **Hint System** - Press `H` at any input to see the first letter of each word.
|
|
||||||
|
|
||||||
## Setup and Dependencies
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
- Python 3.11+
|
|
||||||
- Astral UV (for dependency management)
|
|
||||||
- Docker (for containerized deployment)
|
|
||||||
|
|
||||||
### Install with UV
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv sync
|
|
||||||
```
|
|
||||||
|
|
||||||
This installs all dependencies listed in `pyproject.toml` (currently `pytest` for testing).
|
|
||||||
|
|
||||||
### Install with pip
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install pytest
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to Run
|
|
||||||
|
|
||||||
### Docker (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd docker
|
|
||||||
docker compose up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
This builds the game container and starts it in interactive mode. The game runs inside the container with a TTY attached for input.
|
|
||||||
|
|
||||||
To run tests inside the container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd docker
|
|
||||||
docker compose run game python -m pytest tests -v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Direct Python
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m pytest tests/ -v
|
|
||||||
```
|
|
||||||
|
|
||||||
The test suite includes 34 tests covering data loading, text normalization, verse filtering, similarity matching, hint generation, and game initialization.
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
| Path | Description |
|
||||||
exorcist-game/
|
|------|-------------|
|
||||||
├── .dockerignore # Docker build exclusions
|
| `main.py` | Game engine, demon definitions, and combat loop |
|
||||||
├── .env.example # Environment variable template
|
| `data.py` | Verse loading, normalization, and spell resolution |
|
||||||
├── .hadolint.yaml # Dockerfile linting config
|
| `verses.json` | Verse dataset |
|
||||||
├── pyproject.toml # Project metadata and dependencies
|
| `docker/` | Dockerfile and compose for containerized runs |
|
||||||
├── uv.lock # UV dependency lock file
|
| `tests/` | Test suite for verse data and game logic |
|
||||||
├── verses.json # Bible verse data (12 verses, 5 categories)
|
|
||||||
├── data.py # Verse loading, filtering, text normalization
|
|
||||||
├── main.py # Game engine: encounters, casting, scoring
|
|
||||||
├── docker/
|
|
||||||
│ ├── docker-compose.yml # Service orchestration
|
|
||||||
│ └── Dockerfile # Multi-stage container build
|
|
||||||
├── tests/
|
|
||||||
│ ├── test_data.py # Data module tests (21 tests)
|
|
||||||
│ └── test_main.py # Game logic tests (13 tests)
|
|
||||||
└── README.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Modules
|
## Running
|
||||||
|
|
||||||
| File | Purpose |
|
### Local
|
||||||
|---|---|
|
|
||||||
| `main.py` | Game loop, demon encounters, verse casting, memory tests, scoring, progression |
|
|
||||||
| `data.py` | Verse data loading from JSON, category/difficulty filtering, text normalization |
|
|
||||||
| `verses.json` | 12 verses across 5 categories (Binding, Healing, Exorcism, Protection, Judgment) |
|
|
||||||
| `tests/` | Validation suite ensuring data integrity and game logic correctness |
|
|
||||||
|
|
||||||
## Configuration and Environment Variables
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
Copy `.env.example` to `.env` and configure:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PYTHON_VERSION=3.12-slim # Python version for Docker base image
|
uv sync
|
||||||
GAME_PORT=8080 # (Reserved for future web interface)
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
The game runs without environment variables configured. All game data is embedded in `verses.json`.
|
### Docker
|
||||||
|
|
||||||
### Verse Data Format
|
|
||||||
|
|
||||||
Each verse in `verses.json` follows this structure:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "binding_001",
|
|
||||||
"text": "Full verse text here.",
|
|
||||||
"category": "Binding",
|
|
||||||
"difficulty": 1,
|
|
||||||
"reference": "Luke 10:19"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding Verses
|
|
||||||
|
|
||||||
Append new verse objects to the `verses` array in `verses.json`. Difficulty must be 1-5.
|
|
||||||
|
|
||||||
### Adding Demons
|
|
||||||
|
|
||||||
Edit the `DEMONS` list in `main.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
{"name": "Demon Name", "health": 100, "weaknesses": ["Binding", "Exorcism"], "damage": 20}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
|
|
||||||
The project uses Ruff for linting and Hadolint for Dockerfile validation:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ruff format .
|
docker compose -f docker/docker-compose.yml up
|
||||||
ruff check --fix .
|
|
||||||
hadolint docker/Dockerfile
|
|
||||||
```
|
```
|
||||||
|
|
||||||
All checks must pass before building the container.
|
## Status
|
||||||
|
|
||||||
|
Working prototype. Built as an experiment in terminal game design; not actively maintained.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
2
data.py
2
data.py
@ -58,7 +58,7 @@ def normalize_text(text: str) -> str:
|
|||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
for char in text.lower():
|
for char in text.lower():
|
||||||
if char.isalnum() or char.isspace():
|
if char.isalpha() or char.isdigit() or char.isspace():
|
||||||
result.append(char)
|
result.append(char)
|
||||||
normalized = "".join(result)
|
normalized = "".join(result)
|
||||||
return " ".join(normalized.split())
|
return " ".join(normalized.split())
|
||||||
|
|||||||
@ -20,8 +20,9 @@ COPY --chown=app:app --from=builder /build/.venv /app/.venv
|
|||||||
COPY --chown=app:app main.py ./
|
COPY --chown=app:app main.py ./
|
||||||
COPY --chown=app:app data.py ./
|
COPY --chown=app:app data.py ./
|
||||||
COPY --chown=app:app verses.json ./
|
COPY --chown=app:app verses.json ./
|
||||||
COPY --chown=app:app tests/ ./tests/
|
|
||||||
|
|
||||||
USER app
|
USER app
|
||||||
|
|
||||||
ENV PATH="/app/.venv/bin:$PATH"
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
||||||
|
|||||||
@ -8,4 +8,12 @@ services:
|
|||||||
working_dir: /app
|
working_dir: /app
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
command: python main.py
|
command: python main.py
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1.0"
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: "0.25"
|
||||||
|
memory: 128M
|
||||||
|
|||||||
30
main.py
30
main.py
@ -1,8 +1,15 @@
|
|||||||
"""Arcane Purge - A text-based exorcist game using Bible verses as spell mechanics."""
|
"""Arcane Purge - A text-based exorcist game using Bible verses as spell mechanics."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if _here not in sys.path:
|
||||||
|
sys.path.insert(0, _here)
|
||||||
|
|
||||||
from data import load_verses, normalize_text
|
from data import load_verses, normalize_text
|
||||||
|
|
||||||
# Demon encounter definitions with escalating difficulty
|
# Demon encounter definitions with escalating difficulty
|
||||||
@ -40,7 +47,8 @@ DEMONS = [
|
|||||||
|
|
||||||
def clear_screen() -> None:
|
def clear_screen() -> None:
|
||||||
"""Clear the terminal screen."""
|
"""Clear the terminal screen."""
|
||||||
os.system("cls" if os.name == "nt" else "clear")
|
cmd = "cls" if os.name == "nt" else "clear"
|
||||||
|
subprocess.run(cmd.split(), check=False)
|
||||||
|
|
||||||
|
|
||||||
def show_status(player: dict, demon: dict, verses: list, unlocked: list) -> None:
|
def show_status(player: dict, demon: dict, verses: list, unlocked: list) -> None:
|
||||||
@ -92,9 +100,9 @@ def calculate_similarity(text1: str, text2: str) -> float:
|
|||||||
if not text1 or not text2:
|
if not text1 or not text2:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
words1 = text1.split()
|
words1 = set(text1.split())
|
||||||
words2 = text2.split()
|
words2 = set(text2.split())
|
||||||
matches = sum(1 for w in words1 if w in words2)
|
matches = len(words1 & words2)
|
||||||
max_len = max(len(words1), len(words2))
|
max_len = max(len(words1), len(words2))
|
||||||
return matches / max_len if max_len > 0 else 0.0
|
return matches / max_len if max_len > 0 else 0.0
|
||||||
|
|
||||||
@ -194,7 +202,10 @@ def memory_test(player: dict, demon: dict, verse: dict) -> tuple[bool, int]:
|
|||||||
print("(Press H for hint if needed)")
|
print("(Press H for hint if needed)")
|
||||||
|
|
||||||
attempts = 3
|
attempts = 3
|
||||||
while attempts > 0:
|
max_iterations = 10
|
||||||
|
iteration = 0
|
||||||
|
while attempts > 0 and iteration < max_iterations:
|
||||||
|
iteration += 1
|
||||||
typed = input("> ")
|
typed = input("> ")
|
||||||
if typed.upper() == "H":
|
if typed.upper() == "H":
|
||||||
print(f"\nHint: {get_hint(verse['text'])}")
|
print(f"\nHint: {get_hint(verse['text'])}")
|
||||||
@ -213,6 +224,7 @@ def memory_test(player: dict, demon: dict, verse: dict) -> tuple[bool, int]:
|
|||||||
|
|
||||||
similarity = calculate_similarity(typed_normalized, verse_normalized)
|
similarity = calculate_similarity(typed_normalized, verse_normalized)
|
||||||
if similarity >= 0.8:
|
if similarity >= 0.8:
|
||||||
|
attempts -= 1
|
||||||
print(f"\nClose ({similarity:.0%} correct). One more retry.")
|
print(f"\nClose ({similarity:.0%} correct). One more retry.")
|
||||||
print("Try again:")
|
print("Try again:")
|
||||||
continue
|
continue
|
||||||
@ -286,12 +298,14 @@ def run_encounter(player: dict, all_verses: list, unlocked: list, demon_index: i
|
|||||||
Updated list of unlocked verse IDs.
|
Updated list of unlocked verse IDs.
|
||||||
"""
|
"""
|
||||||
demon_data = DEMONS[demon_index % len(DEMONS)]
|
demon_data = DEMONS[demon_index % len(DEMONS)]
|
||||||
|
cycle = demon_index // len(DEMONS)
|
||||||
|
scale = 1 + cycle * 0.25
|
||||||
demon = {
|
demon = {
|
||||||
"name": demon_data["name"],
|
"name": demon_data["name"],
|
||||||
"health": demon_data["health"],
|
"health": int(demon_data["health"] * scale),
|
||||||
"max_health": demon_data["health"],
|
"max_health": int(demon_data["health"] * scale),
|
||||||
"weaknesses": demon_data["weaknesses"],
|
"weaknesses": demon_data["weaknesses"],
|
||||||
"damage": demon_data["damage"],
|
"damage": int(demon_data["damage"] * scale),
|
||||||
}
|
}
|
||||||
|
|
||||||
available = [v for v in all_verses if v["id"] in unlocked]
|
available = [v for v in all_verses if v["id"] in unlocked]
|
||||||
|
|||||||
8
prompt_v0.2.md
Normal file
8
prompt_v0.2.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
we need to implement the following key features
|
||||||
|
- Add 100 more Bible verses (ensure these are the Biblically accurate scriptures and are the ESV or KJV version)
|
||||||
|
- Give the user the option to pick which version from the start screen
|
||||||
|
- Add some dialog lines in between the battles.
|
||||||
|
- Add a tutorial level where a mentor teaches you how to face a demon.
|
||||||
|
- Keep these features in the cli version of the game, but go ahead and make a web version of the game so that people can play this from a browser
|
||||||
|
- On the web version, let the user be able to speak out the verses into their mic. We can use whisper.cpp on the backend to transcribe the audio. You can download what models you need, but try to use the bare minimum for now during testing as my internet speed is not great (40-50 Mbps)
|
||||||
|
- The user will need to still be very accurate with the wordings from the verses.
|
||||||
@ -3,13 +3,13 @@ name = "exorcist-game"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A text-based exorcist game using Bible verses as spell mechanics"
|
description = "A text-based exorcist game using Bible verses as spell mechanics"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = ["pytest>=7.0"]
|
dependencies = []
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
exorcist = "main:main"
|
exorcist = "main:main"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
test = ["pytest>=7.0"]
|
dev = ["pytest>=7.0"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -78,20 +78,24 @@ class TestGameInitialization:
|
|||||||
class TestDemonData:
|
class TestDemonData:
|
||||||
"""Tests for demon encounter data."""
|
"""Tests for demon encounter data."""
|
||||||
|
|
||||||
from main import DEMONS
|
|
||||||
|
|
||||||
def test_demons_exist(self) -> None:
|
def test_demons_exist(self) -> None:
|
||||||
"""Demon list is populated."""
|
"""Demon list is populated."""
|
||||||
assert len(self.DEMONS) > 0
|
from main import DEMONS
|
||||||
|
|
||||||
|
assert len(DEMONS) > 0
|
||||||
|
|
||||||
def test_demon_structure(self) -> None:
|
def test_demon_structure(self) -> None:
|
||||||
"""Each demon has required fields."""
|
"""Each demon has required fields."""
|
||||||
|
from main import DEMONS
|
||||||
|
|
||||||
required = {"name", "health", "weaknesses", "damage"}
|
required = {"name", "health", "weaknesses", "damage"}
|
||||||
for demon in self.DEMONS:
|
for demon in DEMONS:
|
||||||
assert required.issubset(demon.keys())
|
assert required.issubset(demon.keys())
|
||||||
|
|
||||||
def test_escalating_difficulty(self) -> None:
|
def test_escalating_difficulty(self) -> None:
|
||||||
"""Demons have escalating health values."""
|
"""Demons have escalating health values."""
|
||||||
healths = [d["health"] for d in self.DEMONS]
|
from main import DEMONS
|
||||||
|
|
||||||
|
healths = [d["health"] for d in DEMONS]
|
||||||
for i in range(1, len(healths)):
|
for i in range(1, len(healths)):
|
||||||
assert healths[i] > healths[i - 1]
|
assert healths[i] > healths[i - 1]
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "exorcism_001",
|
"id": "exorcism_001",
|
||||||
"text": "The spirit you do not want in your life tell it to leave in the name of Jesus.",
|
"text": "These signs will accompany those who believe. In my name they will drive out demons.",
|
||||||
"category": "Exorcism",
|
"category": "Exorcism",
|
||||||
"difficulty": 2,
|
"difficulty": 2,
|
||||||
"reference": "Mark 16:17"
|
"reference": "Mark 16:17"
|
||||||
@ -65,7 +65,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "judgment_001",
|
"id": "judgment_001",
|
||||||
"text": "The Lord will judge the ends of the earth and you cannot give bread to stone.",
|
"text": "The Lord will judge the ends of the earth; he delivers justice and uprightness.",
|
||||||
"category": "Judgment",
|
"category": "Judgment",
|
||||||
"difficulty": 4,
|
"difficulty": 4,
|
||||||
"reference": "Psalm 21:9"
|
"reference": "Psalm 21:9"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user