fix: batch fix all 14 issues

- Fix memory test infinite loop (#1, #3) — add max iterations counter
- Fix os.system command injection (#2, #4) — use subprocess with list args
- Fix demon difficulty scaling (#5) — scale health/damage per cycle
- Fix relative import (#6) — absolute imports with sys.path fallback
- Fix normalize_text non-ASCII (#7) — use isalpha/isdigit instead of isalnum
- Fix calculate_similarity word order (#8) — set-based intersection
- Add CMD to Dockerfile (#9)
- Remove test files from prod image (#10) — .dockerignore + strip tests COPY
- Fix test class import (#11) — lazy import in test methods
- Fix pyproject.toml duplicate pytest (#12) — move to dev optional deps
- Add resource limits to compose (#13)
- Fix verses.json quality (#14) — fix garbled verse text
This commit is contained in:
Jarian 2026-07-05 22:22:41 +00:00
parent cc7aa2ba86
commit 7ec5ae1664
8 changed files with 55 additions and 59 deletions

View File

@ -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

View File

@ -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())

View File

@ -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"]

View File

@ -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
View File

@ -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]

View File

@ -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"]

View File

@ -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]

View File

@ -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"