{"id":50405273,"url":"https://github.com/simonpainter/fighting-fantasy","last_synced_at":"2026-05-31T01:04:25.965Z","repository":{"id":349030073,"uuid":"1200610717","full_name":"simonpainter/fighting-fantasy","owner":"simonpainter","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-16T21:14:58.000Z","size":66,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-16T23:20:57.681Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/simonpainter.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-03T16:07:49.000Z","updated_at":"2026-04-16T21:15:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/simonpainter/fighting-fantasy","commit_stats":null,"previous_names":["simonpainter/fighting-fantasy"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/simonpainter/fighting-fantasy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simonpainter%2Ffighting-fantasy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simonpainter%2Ffighting-fantasy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simonpainter%2Ffighting-fantasy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simonpainter%2Ffighting-fantasy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/simonpainter","download_url":"https://codeload.github.com/simonpainter/fighting-fantasy/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simonpainter%2Ffighting-fantasy/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33715214,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-31T01:04:25.287Z","updated_at":"2026-05-31T01:04:25.957Z","avatar_url":"https://github.com/simonpainter.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fighting Fantasy\n\nA Python engine for running text-based adventure games in the style of the classic [Fighting Fantasy](https://en.wikipedia.org/wiki/Fighting_Fantasy) gamebook series, created by Steve Jackson and Ian Livingstone.\n\n## Overview\n\nFighting Fantasy gamebooks are single-player role-playing adventures first published in 1982. Each book is divided into numbered sections; the reader makes choices that jump between sections, resolving combat and challenges with two six-sided dice and three character attributes: **SKILL**, **STAMINA**, and **LUCK**.\n\nThis project provides a Python game engine that faithfully recreates that experience in the terminal — branching narrative, dice-based combat, luck tests, and all.\n\n## Features\n\n- **Branching narrative engine** — section-based story graph where player choices drive progression\n- **Character creation** — roll SKILL, STAMINA, and LUCK at the start of each adventure\n- **Turn-based combat** — faithful implementation of the Fighting Fantasy combat rules\n- **Luck tests** — Test your Luck to modify combat outcomes, at the cost of your LUCK score\n- **Multiple mechanics** — luck tests, stamina tests, dice-roll challenges, item/gold checks, random encounters, and more\n- **Inventory system** — gold, provisions, items, and numbered keys; eat to restore STAMINA mid-adventure\n- **Adventure data format** — define adventures in JSON; no code changes needed\n\n## Getting Started\n\n### Prerequisites\n\n- Python 3.10+\n\n### Installation\n\n```bash\ngit clone https://github.com/simonpainter/fighting-fantasy.git\ncd fighting-fantasy\n```\n\nNo additional dependencies — the engine uses the Python standard library only.\n\n### Running\n\n```bash\npython3 main.py adventures/your_adventure.json\n```\n\n### Running the test suite\n\nThe repository ships with a comprehensive unittest suite covering static\nvalidation of adventure files, every mechanic, the inventory system, and\nknown winning paths through `adventures/warlock.json`:\n\n```bash\npython3 -m unittest discover tests -v\n```\n\nAll tests use the standard library only and run in well under a second.\n\n## How to Play\n\n### Character Attributes\n\nAt the start of an adventure your character's attributes are rolled randomly:\n\n| Attribute | Roll | Range | Description |\n|-----------|------|-------|-------------|\n| **SKILL** | 1d6 + 6 | 7–12 | Combat prowess and dexterity |\n| **STAMINA** | 2d6 + 12 | 14–24 | Health — reach 0 and you die |\n| **LUCK** | 1d6 + 6 | 7–12 | Fortune — decreases each time you test it |\n\n### Navigation\n\nAt each location you are shown the available exits and prompted to choose a direction. Exits can be cardinal directions (`N`, `S`, `E`, `W`) or descriptive words (`charge`, `back`, `search`, etc.). Input is case-insensitive.\n\nAt any exit prompt you can also type:\n\n- `inv` (or `i`, `inventory`) — show your gold, provisions, items, and keys\n- `eat` (or `e`) — eat a Provision to restore STAMINA (see [Inventory \u0026 Provisions](#inventory--provisions))\n\n### Combat\n\nWhen a monster is present you must fight before you can move on. Each combat round:\n\n1. Both you and the monster roll **2d6** and add your respective **SKILL** scores to get an **Attack Strength**\n2. The higher Attack Strength wins the round and deals **2 STAMINA** damage to the loser\n3. A tie means both attacks are deflected — no damage is dealt\n4. After each hit you may **Test your Luck** to modify the damage (see below)\n5. Combat continues until either side reaches 0 STAMINA\n\n**Testing your Luck in combat:**\n- If *you* land a hit: Lucky → deal **4 damage** instead of 2; Unlucky → deal only **1 damage**\n- If *you* are hit: Lucky → take only **1 damage** instead of 2; Unlucky → take **3 damage**\n\n### Testing Your Luck\n\nRoll **2d6**. If the result is ≤ your current LUCK score you are **Lucky**. Your LUCK decreases by 1 regardless of the outcome — the more you rely on luck, the riskier it becomes.\n\n### Other Mechanics\n\nSome locations trigger automatic checks rather than a player choice:\n\n| Mechanic | How it works |\n|----------|-------------|\n| `test_luck` | Roll 2d6 against your LUCK score. LUCK decreases by 1. |\n| `test_stamina` | Roll 2d6 against your STAMINA score. STAMINA is unaffected. |\n| `dice_roll` | Roll a specified number of dice; certain values cause failure. |\n| `pre_combat_luck` | Test Luck before a fight; lucky/unlucky effects are applied to your stats. |\n| `test_luck_choice` | Lucky takes a fixed exit; unlucky forces you to pick from the remaining exits. |\n| `dice_table` | Roll dice and look up the destination (and any effects) in a table. |\n| `random_encounter` | Roll a die against a monster table to see what (if anything) appears. |\n| `fight` / `return` | Detour into a sub-section to fight, then return to where you came from. |\n| `key_sum` | Sum the numbers on three keys you've collected to determine the next section. |\n| `item_check` | Branch based on whether you hold a named item (optionally consuming it). |\n| `gold_check` | Branch based on whether you have enough Gold Pieces (optionally spending them). |\n\nSee [Mechanic exits](#mechanic-exits) for the JSON schema for each mechanic.\n\n---\n\n## Inventory \u0026 Provisions\n\nBeyond SKILL/STAMINA/LUCK, your character carries:\n\n- **Gold Pieces** — accumulated via `grants_gold` locations or starting gold; spent via `gold_check` mechanics\n- **Provisions** — meals you can eat at any exit prompt to restore STAMINA\n- **Items** — named objects (e.g. `silver_key`, `lantern`) granted by locations and checked by `item_check` mechanics\n- **Keys** — numbered keys used by the `key_sum` mechanic\n\n### Eating Provisions\n\nAt any exit prompt, type `eat` to consume one Provision. This restores up to **4 STAMINA**, capped at your initial STAMINA score. You cannot eat if you are already at full STAMINA or have no Provisions left.\n\n### Starting equipment\n\nSet starting values in the adventure's `config` block (see below).\n\n---\n\n## Adventure Data Format\n\nAdventures are defined in JSON files. Copy `adventures/game_data.json.example` as a starting point.\n\n### Top-level structure\n\n```json\n{\n    \"config\": { ... },\n    \"1\": { ... },\n    \"2\": { ... }\n}\n```\n\n### `config` block\n\n```json\n\"config\": {\n    \"title\": \"The Cave of Shadows\",\n    \"author\": \"Your Name\",\n    \"description\": \"Short blurb shown at startup.\",\n    \"background\": \"Longer backstory shown before character creation.\",\n    \"starting_gold\": 10,\n    \"starting_provisions\": 5,\n    \"inventory\": {\n        \"lantern\": 1,\n        \"rope\": 1\n    }\n}\n```\n\n| Field | Type | Purpose |\n|-------|------|---------|\n| `title` | string | Adventure name shown at startup |\n| `author` | string | Author credit |\n| `description` | string | Short blurb shown at startup |\n| `background` | string | Longer backstory shown before character creation |\n| `starting_gold` | int | Gold Pieces the player begins with (default `0`) |\n| `starting_provisions` | int | Provisions the player begins with (default `0`) |\n| `inventory` | object | `{ \"item_name\": quantity }` map of starting items (default `{}`) |\n\n### Location block\n\n```json\n\"42\": {\n    \"description\": \"Narrative text shown to the player.\",\n    \"exits\": {\n        \"N\": \"17\",\n        \"open\": \"85\",\n        \"back\": \"3\"\n    },\n    \"monsters\": [\n        { \"name\": \"Goblin\", \"skill\": 5, \"stamina\": 6 }\n    ]\n}\n```\n\n- **Keys** are section numbers as strings\n- **`exits`** maps direction/action names to destination section numbers (as strings)\n- **`monsters`** is an array of `{ name, skill, stamina }` objects; use `[]` for empty rooms\n- Multiple monsters are fought in sequence\n\n#### Optional location fields\n\nAny location may include any of these:\n\n| Field | Type | Effect |\n|-------|------|--------|\n| `on_entry_effects` | object | `{ \"skill\": -1, \"stamina\": -2, \"luck\": +1 }` — applied automatically on entry. Stats are clamped at 0. |\n| `grants_gold` | int | Adds the given number of Gold Pieces to the player. |\n| `grants_provisions` | int | Adds Provisions. |\n| `grants_item` | string or array | Offers the named item (or each item in the list) — the player chooses to take it. |\n| `consumes_item` | string or array | Removes the named item(s) from the inventory if held. |\n| `grants_key` | int | Offers a numbered key. Used together with the `key_sum` mechanic. |\n\nExample:\n\n```json\n\"45\": {\n    \"description\": \"A merchant offers you a healing draught for 2 Gold Pieces.\",\n    \"on_entry_effects\": { \"stamina\": -1 },\n    \"grants_item\": \"healing_draught\",\n    \"exits\": { \"continue\": \"46\" },\n    \"monsters\": []\n}\n```\n\n### Mechanic exits\n\nWhen a location requires an automatic check rather than a player choice, add `lucky` and `unlucky` keys to `exits` and specify a `mechanic`. Most lucky/unlucky mechanics also accept optional `lucky_effects` / `unlucky_effects` inside `mechanic_data` to apply stat changes on each branch.\n\n```json\n\"71\": {\n    \"description\": \"Test your Luck to sneak past the sleeping guard...\",\n    \"mechanic\": \"test_luck\",\n    \"mechanic_data\": {\n        \"unlucky_effects\": { \"stamina\": -2 }\n    },\n    \"exits\": {\n        \"lucky\": \"301\",\n        \"unlucky\": \"248\"\n    },\n    \"monsters\": []\n}\n```\n\n```json\n\"298\": {\n    \"description\": \"Roll one die — a 6 means you slip off the bridge...\",\n    \"mechanic\": \"dice_roll\",\n    \"mechanic_data\": {\n        \"dice\": 1,\n        \"fail_values\": [6]\n    },\n    \"exits\": {\n        \"lucky\": \"7\",\n        \"unlucky\": \"86\"\n    },\n    \"monsters\": []\n}\n```\n\n```json\n\"316\": {\n    \"description\": \"Roll two dice against your STAMINA to swim across...\",\n    \"mechanic\": \"test_stamina\",\n    \"mechanic_data\": {\n        \"dice\": 2\n    },\n    \"exits\": {\n        \"lucky\": \"151\",\n        \"unlucky\": \"218\"\n    },\n    \"monsters\": []\n}\n```\n\n#### `pre_combat_luck`\n\nTests Luck immediately before combat starts. `lucky_effects` / `unlucky_effects` are applied to your stats; the fight then proceeds against the monsters defined on the location.\n\n```json\n\"120\": {\n    \"description\": \"The orc swings at you before you're ready. Test your Luck.\",\n    \"mechanic\": \"pre_combat_luck\",\n    \"mechanic_data\": {\n        \"unlucky_effects\": { \"stamina\": -2 }\n    },\n    \"exits\": { \"continue\": \"121\" },\n    \"monsters\": [{ \"name\": \"Orc\", \"skill\": 6, \"stamina\": 5 }]\n}\n```\n\n#### `test_luck_choice`\n\nLucky goes straight to the `lucky` exit; unlucky forces the player to pick from the remaining exits.\n\n```json\n\"77\": {\n    \"description\": \"Test your Luck. If lucky you spot the trapdoor; otherwise you must pick a direction.\",\n    \"mechanic\": \"test_luck_choice\",\n    \"exits\": {\n        \"lucky\": \"200\",\n        \"N\": \"78\",\n        \"S\": \"79\"\n    },\n    \"monsters\": []\n}\n```\n\n#### `dice_table`\n\nRoll dice and look up the destination (and optional effects) in a table. The location's `exits` is unused (typically `{}`).\n\n```json\n\"55\": {\n    \"description\": \"Roll two dice on the wandering monster table.\",\n    \"mechanic\": \"dice_table\",\n    \"mechanic_data\": {\n        \"dice\": 2,\n        \"table\": [\n            { \"rolls\": [2, 3, 4],   \"destination\": \"60\" },\n            { \"rolls\": [5, 6, 7, 8], \"destination\": \"61\", \"effects\": { \"stamina\": -1 } },\n            { \"rolls\": [9, 10, 11, 12], \"destination\": \"62\" }\n        ]\n    },\n    \"exits\": {},\n    \"monsters\": []\n}\n```\n\n#### `random_encounter`\n\nRoll one die against a monster table; if a monster appears, fight it, then return to the section that sent you here (set by a `fight` mechanic).\n\n```json\n\"180\": {\n    \"description\": \"A noise behind you — something approaches.\",\n    \"mechanic\": \"random_encounter\",\n    \"mechanic_data\": {\n        \"table\": {\n            \"1\": { \"name\": \"Goblin\",  \"skill\": 5, \"stamina\": 5 },\n            \"2\": { \"name\": \"Orc\",     \"skill\": 6, \"stamina\": 6 },\n            \"3\": { \"name\": \"Skeleton\",\"skill\": 6, \"stamina\": 5 }\n        }\n    },\n    \"exits\": {},\n    \"monsters\": []\n}\n```\n\n#### `fight` / `return`\n\n`fight` detours the player into a sub-section to resolve a side encounter, then `return` bounces back to where they came from.\n\n```json\n\"50\": {\n    \"description\": \"A guard challenges you. Fight!\",\n    \"mechanic\": \"fight\",\n    \"exits\": { \"fight\": \"51\" },\n    \"monsters\": []\n},\n\"51\": {\n    \"description\": \"You defeat the guard and continue on your way.\",\n    \"mechanic\": \"return\",\n    \"exits\": {},\n    \"monsters\": [{ \"name\": \"Guard\", \"skill\": 7, \"stamina\": 8 }]\n}\n```\n\n#### `key_sum`\n\nRequires the player to be holding three numbered keys (granted via `grants_key`). Sums them and uses the total as the next section number.\n\n```json\n\"400\": {\n    \"description\": \"Three keyholes glow on the door. Insert your keys.\",\n    \"mechanic\": \"key_sum\",\n    \"exits\": {},\n    \"monsters\": []\n}\n```\n\n#### `item_check`\n\nBranches based on whether the player holds a named item. Set `consume: true` to remove the item on a successful check.\n\n```json\n\"88\": {\n    \"description\": \"Do you have the silver key?\",\n    \"mechanic\": \"item_check\",\n    \"mechanic_data\": {\n        \"item\": \"silver_key\",\n        \"consume\": true\n    },\n    \"exits\": {\n        \"have\": \"89\",\n        \"missing\": \"90\"\n    },\n    \"monsters\": []\n}\n```\n\n#### `gold_check`\n\nBranches based on whether the player has at least `amount` Gold Pieces. Set `consume: true` to deduct the gold on success.\n\n```json\n\"112\": {\n    \"description\": \"The ferryman demands 5 Gold Pieces.\",\n    \"mechanic\": \"gold_check\",\n    \"mechanic_data\": {\n        \"amount\": 5,\n        \"consume\": true\n    },\n    \"exits\": {\n        \"have\": \"113\",\n        \"missing\": \"114\"\n    },\n    \"monsters\": []\n}\n```\n\n### Endings\n\nA location with an empty `exits` object (`{}`) ends the game — use this for victory or death scenes.\n\n### Adventure file location\n\nYour own adventure JSON files are **gitignored** — only the `game_data.json.example` template is committed to the repository (see `.gitignore`). Place your adventure files in the `adventures/` directory and reference them at runtime:\n\n```bash\npython3 main.py adventures/my_adventure.json\n```\n\n---\n\n## Project Structure\n\n```\nfighting-fantasy/\n├── main.py                        # Engine entry point (Player, Location, Monster classes)\n├── adventures/\n│   └── game_data.json.example     # Example adventure to use as a template\n├── tests/\n│   ├── harness.py                 # Shared scripted-IO test harness\n│   ├── test_static.py             # Static validation of adventure files\n│   ├── test_mechanics.py          # Per-mechanic engine tests\n│   ├── test_inventory.py          # Inventory, gold, and provisions tests\n│   └── test_paths.py              # Known-path playthroughs\n├── .github/                       # GitHub workflows / config\n├── .gitignore\n├── LICENSE\n└── README.md\n```\n\n## Contributing\n\nPull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and\nworkflow details.\n\nPlease also review:\n\n- [Code of Conduct](CODE_OF_CONDUCT.md)\n- [Security Policy](SECURITY.md)\n\n## Licence\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimonpainter%2Ffighting-fantasy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsimonpainter%2Ffighting-fantasy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimonpainter%2Ffighting-fantasy/lists"}