feat: Create a module for Mythic RPG’s Fate Chart
This commit is contained in:
parent
bf69d7463e
commit
4d92935fda
192
gm_assistant/fate_chart.py
Normal file
192
gm_assistant/fate_chart.py
Normal file
@ -0,0 +1,192 @@
|
||||
# SPDX-FileCopyrightText: 2025 2025
|
||||
# SPDX-FileContributor: Gergely Polonkai
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Calculate the odds using Mythic RPG’s Fate Chart"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from .dice import Die
|
||||
|
||||
|
||||
class FateOutcome(Enum):
|
||||
"""Possible outcomes"""
|
||||
|
||||
EXC_YES = "eyes"
|
||||
YES = "yes"
|
||||
NO = "no"
|
||||
EXC_NO = "eno"
|
||||
|
||||
|
||||
class FateOdds(Enum):
|
||||
"""Odds of a “yes” answer"""
|
||||
|
||||
IMPOSSIBLE = 0
|
||||
NO_WAY = 1
|
||||
VERY_UNLIKELY = 2
|
||||
UNLIKELY = 3
|
||||
FIFTY_FIFTY = 4
|
||||
SOMEWHAT_LIKELY = 5
|
||||
LIKELY = 6
|
||||
VERY_LIKELY = 7
|
||||
NEAR_SURE_THING = 8
|
||||
SURE_THING = 9
|
||||
HAS_TO_BE = 10
|
||||
HAS_TO_BE2 = 11
|
||||
|
||||
|
||||
TABLE: dict[FateOdds, list[tuple[int, int, int]]] = {
|
||||
FateOdds.IMPOSSIBLE: [
|
||||
(0, -20, 77),
|
||||
(0, 0, 81),
|
||||
(0, 0, 81),
|
||||
(1, 5, 82),
|
||||
(1, 5, 82),
|
||||
(2, 10, 83),
|
||||
(3, 15, 84),
|
||||
(5, 25, 86),
|
||||
(10, 50, 91),
|
||||
],
|
||||
FateOdds.NO_WAY: [
|
||||
(0, 0, 81),
|
||||
(1, 5, 82),
|
||||
(1, 5, 82),
|
||||
(2, 10, 83),
|
||||
(3, 15, 84),
|
||||
(5, 25, 86),
|
||||
(7, 35, 88),
|
||||
(10, 50, 91),
|
||||
(15, 75, 96),
|
||||
],
|
||||
FateOdds.VERY_UNLIKELY: [
|
||||
(1, 5, 82),
|
||||
(1, 5, 82),
|
||||
(2, 10, 83),
|
||||
(3, 15, 84),
|
||||
(5, 25, 86),
|
||||
(9, 45, 90),
|
||||
(10, 50, 91),
|
||||
(13, 65, 94),
|
||||
(16, 85, 97),
|
||||
],
|
||||
FateOdds.UNLIKELY: [
|
||||
(1, 5, 82),
|
||||
(2, 10, 83),
|
||||
(3, 15, 84),
|
||||
(4, 20, 85),
|
||||
(7, 35, 88),
|
||||
(10, 50, 91),
|
||||
(11, 55, 92),
|
||||
(15, 75, 96),
|
||||
(18, 90, 99),
|
||||
],
|
||||
FateOdds.FIFTY_FIFTY: [
|
||||
(2, 10, 83),
|
||||
(3, 15, 84),
|
||||
(5, 25, 86),
|
||||
(7, 35, 88),
|
||||
(10, 50, 91),
|
||||
(13, 65, 94),
|
||||
(15, 75, 96),
|
||||
(16, 85, 97),
|
||||
(19, 95, 100),
|
||||
],
|
||||
FateOdds.SOMEWHAT_LIKELY: [
|
||||
(4, 20, 85),
|
||||
(5, 25, 86),
|
||||
(9, 45, 90),
|
||||
(10, 50, 91),
|
||||
(13, 65, 94),
|
||||
(16, 80, 97),
|
||||
(16, 85, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
],
|
||||
FateOdds.LIKELY: [
|
||||
(5, 25, 86),
|
||||
(7, 35, 88),
|
||||
(10, 50, 91),
|
||||
(11, 55, 92),
|
||||
(15, 75, 96),
|
||||
(16, 85, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(20, 100, 0),
|
||||
],
|
||||
FateOdds.VERY_LIKELY: [
|
||||
(9, 45, 90),
|
||||
(10, 50, 91),
|
||||
(13, 65, 94),
|
||||
(15, 75, 96),
|
||||
(16, 85, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(19, 95, 100),
|
||||
(21, 105, 0),
|
||||
],
|
||||
FateOdds.NEAR_SURE_THING: [
|
||||
(10, 50, 91),
|
||||
(11, 55, 92),
|
||||
(15, 75, 96),
|
||||
(16, 80, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(19, 95, 100),
|
||||
(20, 100, 0),
|
||||
(23, 115, 0),
|
||||
],
|
||||
FateOdds.SURE_THING: [
|
||||
(11, 55, 92),
|
||||
(13, 65, 94),
|
||||
(16, 80, 97),
|
||||
(16, 85, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(19, 95, 100),
|
||||
(22, 110, 0),
|
||||
(25, 125, 0),
|
||||
],
|
||||
FateOdds.HAS_TO_BE: [
|
||||
(16, 80, 97),
|
||||
(16, 85, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(19, 95, 100),
|
||||
(20, 100, 0),
|
||||
(20, 100, 0),
|
||||
(26, 130, 0),
|
||||
(26, 145, 0),
|
||||
],
|
||||
FateOdds.HAS_TO_BE2: [
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(19, 95, 100),
|
||||
(20, 100, 0),
|
||||
(22, 110, 0),
|
||||
(24, 120, 0),
|
||||
(24, 120, 0),
|
||||
(30, 150, 0),
|
||||
(29, 165, 0),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def decide(odds: FateOdds, chaos_level: int = 5) -> FateOutcome:
|
||||
"""Decide the outcome of a situation using the Fate Chart"""
|
||||
|
||||
# Chaos levels range from 1 and 9 (both included), but list indices start at 0
|
||||
chaos_level -= 1
|
||||
|
||||
eyes, yes, eno = TABLE[odds][chaos_level]
|
||||
value = Die("d100").roll()
|
||||
|
||||
if value <= eyes:
|
||||
return FateOutcome.EXC_YES
|
||||
|
||||
if value <= yes:
|
||||
return FateOutcome.YES
|
||||
|
||||
if value >= eno:
|
||||
return FateOutcome.EXC_NO
|
||||
|
||||
return FateOutcome.NO
|
51
tests/test_fate_chart.py
Normal file
51
tests/test_fate_chart.py
Normal file
@ -0,0 +1,51 @@
|
||||
# SPDX-FileCopyrightText: 2025 2025
|
||||
# SPDX-FileContributor: Gergely Polonkai
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Tests for the Fate Chart module"""
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from gm_assistant.fate_chart import FateOdds, FateOutcome, decide
|
||||
|
||||
|
||||
@pytest.mark.parametrize("odds", FateOdds)
|
||||
@pytest.mark.parametrize("chaos_level", range(9))
|
||||
def test_all_valid_values(odds: FateOdds, chaos_level: int) -> None:
|
||||
"""Test if every valid value yields some result"""
|
||||
|
||||
assert isinstance(decide(odds, chaos_level=chaos_level), FateOutcome)
|
||||
|
||||
|
||||
# WARNING
|
||||
#
|
||||
# Tests below this line rely on exact values in the fate_chart module
|
||||
|
||||
|
||||
def test_exceptional_yes(mocker: MockerFixture) -> None:
|
||||
"""Test if exceptional yes result is returned"""
|
||||
|
||||
mocker.patch("gm_assistant.fate_chart.Die.roll", return_value=10)
|
||||
assert decide(FateOdds.FIFTY_FIFTY, chaos_level=5) == FateOutcome.EXC_YES
|
||||
|
||||
|
||||
def test_yes(mocker: MockerFixture) -> None:
|
||||
"""Test if yes result is returned"""
|
||||
|
||||
mocker.patch("gm_assistant.fate_chart.Die.roll", return_value=50)
|
||||
assert decide(FateOdds.FIFTY_FIFTY, chaos_level=5) == FateOutcome.YES
|
||||
|
||||
|
||||
def test_exceptional_no(mocker: MockerFixture) -> None:
|
||||
"""Test if exceptional no result is returned"""
|
||||
|
||||
mocker.patch("gm_assistant.fate_chart.Die.roll", return_value=91)
|
||||
assert decide(FateOdds.FIFTY_FIFTY, chaos_level=5) == FateOutcome.EXC_NO
|
||||
|
||||
|
||||
def test_no(mocker: MockerFixture) -> None:
|
||||
"""Test if exceptional no result is returned"""
|
||||
|
||||
mocker.patch("gm_assistant.fate_chart.Die.roll", return_value=51)
|
||||
assert decide(FateOdds.FIFTY_FIFTY, chaos_level=5) == FateOutcome.NO
|
Loading…
x
Reference in New Issue
Block a user