misc: drop typing as T, use builtin generics

Python 3.10 is the minimum, so annotations use `X | None`, `list[X]`
and friends, enforced by ruff's pyupgrade rules. The remaining typing
names are imported directly, with abstract types coming from
collections.abc. first() is now generic over the item and default.
This commit is contained in:
Dash committed 2026-10-17 15:55:55 +02:00
1 parent ada8d5b6ed
commit 0a4358e2b8
40 files changed
+248 -236

No files matched your search

+3 -3
View File
@@ -1,7 +1,7 @@
import argparse import argparse
import sys import sys
import typing as T
from pathlib import Path from pathlib import Path
from typing import Any
from xdg_base_dirs import xdg_config_home from xdg_base_dirs import xdg_config_home
@@ -84,7 +84,7 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args() return parser.parse_args()
def list_players(roster: Roster, file: T.Optional[T.Any] = None) -> None: def list_players(roster: Roster, file: Any | None = None) -> None:
for i, player in enumerate(roster.players, start=1): for i, player in enumerate(roster.players, start=1):
print(f"{i}. {player.name}", file=file) print(f"{i}. {player.name}", file=file)
@@ -130,7 +130,7 @@ def main() -> None:
list_players(roster) list_players(roster)
exit(0) exit(0)
player: T.Optional[Player] = None player: Player | None = None
if args.load_save: if args.load_save:
try: try:
player = list(roster.players)[args.load_save - 1] player = list(roster.players)[args.load_save - 1]
+3 -4
View File
@@ -1,5 +1,4 @@
import enum import enum
import typing as T
from dataclasses import dataclass from dataclasses import dataclass
@@ -60,16 +59,16 @@ class EquipmentPreset:
class Monster: class Monster:
name: str name: str
level: int level: int
item: T.Optional[str] item: str | None
@dataclass @dataclass
class Race: class Race:
name: str name: str
attr: T.List[StatType] attr: list[StatType]
@dataclass @dataclass
class Class: class Class:
name: str name: str
attr: T.List[StatType] attr: list[StatType]
+43 -42
View File
@@ -1,9 +1,10 @@
import json import json
import sys import sys
import typing as T from collections.abc import Callable, Sequence
from dataclasses import dataclass, fields from dataclasses import dataclass, fields
from importlib import resources from importlib import resources
from pathlib import Path from pathlib import Path
from typing import Any, cast
from pqcli.config import ( from pqcli.config import (
Class, Class,
@@ -23,24 +24,24 @@ class DataError(Exception):
@dataclass(frozen=True) @dataclass(frozen=True)
class GameData: class GameData:
spells: T.Tuple[str, ...] spells: tuple[str, ...]
offense_attrib: T.Tuple[Modifier, ...] offense_attrib: tuple[Modifier, ...]
defense_attrib: T.Tuple[Modifier, ...] defense_attrib: tuple[Modifier, ...]
offense_bad: T.Tuple[Modifier, ...] offense_bad: tuple[Modifier, ...]
defense_bad: T.Tuple[Modifier, ...] defense_bad: tuple[Modifier, ...]
shields: T.Tuple[EquipmentPreset, ...] shields: tuple[EquipmentPreset, ...]
armors: T.Tuple[EquipmentPreset, ...] armors: tuple[EquipmentPreset, ...]
weapons: T.Tuple[EquipmentPreset, ...] weapons: tuple[EquipmentPreset, ...]
specials: T.Tuple[str, ...] specials: tuple[str, ...]
item_attrib: T.Tuple[str, ...] item_attrib: tuple[str, ...]
item_ofs: T.Tuple[str, ...] item_ofs: tuple[str, ...]
boring_items: T.Tuple[str, ...] boring_items: tuple[str, ...]
monsters: T.Tuple[Monster, ...] monsters: tuple[Monster, ...]
races: T.Tuple[Race, ...] races: tuple[Race, ...]
classes: T.Tuple[Class, ...] classes: tuple[Class, ...]
titles: T.Tuple[str, ...] titles: tuple[str, ...]
impressive_titles: T.Tuple[str, ...] impressive_titles: tuple[str, ...]
combat_verbs: T.Tuple[str, ...] combat_verbs: tuple[str, ...]
DATA_FILES = tuple(field.name for field in fields(GameData)) DATA_FILES = tuple(field.name for field in fields(GameData))
@@ -55,8 +56,8 @@ class _Context:
def _check_keys( def _check_keys(
ctx: _Context, where: str, obj: T.Any, keys: T.Sequence[str] ctx: _Context, where: str, obj: Any, keys: Sequence[str]
) -> T.Dict[str, T.Any]: ) -> dict[str, Any]:
if not isinstance(obj, dict): if not isinstance(obj, dict):
raise ctx.error(f"{where} must be an object") raise ctx.error(f"{where} must be an object")
missing = [key for key in keys if key not in obj] missing = [key for key in keys if key not in obj]
@@ -68,19 +69,19 @@ def _check_keys(
return obj return obj
def _str(ctx: _Context, where: str, value: T.Any) -> str: def _str(ctx: _Context, where: str, value: Any) -> str:
if not isinstance(value, str): if not isinstance(value, str):
raise ctx.error(f"{where} must be a string") raise ctx.error(f"{where} must be a string")
return value return value
def _int(ctx: _Context, where: str, value: T.Any) -> int: def _int(ctx: _Context, where: str, value: Any) -> int:
if isinstance(value, bool) or not isinstance(value, int): if isinstance(value, bool) or not isinstance(value, int):
raise ctx.error(f"{where} must be an integer") raise ctx.error(f"{where} must be an integer")
return value return value
def _stat(ctx: _Context, where: str, value: T.Any) -> StatType: def _stat(ctx: _Context, where: str, value: Any) -> StatType:
_str(ctx, where, value) _str(ctx, where, value)
for stat in StatType: for stat in StatType:
if value.lower() in (stat.value.lower(), stat.name): if value.lower() in (stat.value.lower(), stat.name):
@@ -89,18 +90,18 @@ def _stat(ctx: _Context, where: str, value: T.Any) -> StatType:
raise ctx.error(f"{where}: unknown stat {value!r} (use one of {allowed})") raise ctx.error(f"{where}: unknown stat {value!r} (use one of {allowed})")
def _parse_str(ctx: _Context, where: str, item: T.Any) -> str: def _parse_str(ctx: _Context, where: str, item: Any) -> str:
return _str(ctx, where, item) return _str(ctx, where, item)
def _parse_verb(ctx: _Context, where: str, item: T.Any) -> str: def _parse_verb(ctx: _Context, where: str, item: Any) -> str:
verb = _str(ctx, where, item) verb = _str(ctx, where, item)
if TARGET_PLACEHOLDER not in verb: if TARGET_PLACEHOLDER not in verb:
raise ctx.error(f"{where} must contain {TARGET_PLACEHOLDER}") raise ctx.error(f"{where} must contain {TARGET_PLACEHOLDER}")
return verb return verb
def _parse_modifier(ctx: _Context, where: str, item: T.Any) -> Modifier: def _parse_modifier(ctx: _Context, where: str, item: Any) -> Modifier:
obj = _check_keys(ctx, where, item, ["name", "quality"]) obj = _check_keys(ctx, where, item, ["name", "quality"])
return Modifier( return Modifier(
_str(ctx, f"{where}.name", obj["name"]), _str(ctx, f"{where}.name", obj["name"]),
@@ -108,7 +109,7 @@ def _parse_modifier(ctx: _Context, where: str, item: T.Any) -> Modifier:
) )
def _parse_preset(ctx: _Context, where: str, item: T.Any) -> EquipmentPreset: def _parse_preset(ctx: _Context, where: str, item: Any) -> EquipmentPreset:
obj = _check_keys(ctx, where, item, ["name", "quality"]) obj = _check_keys(ctx, where, item, ["name", "quality"])
return EquipmentPreset( return EquipmentPreset(
_str(ctx, f"{where}.name", obj["name"]), _str(ctx, f"{where}.name", obj["name"]),
@@ -116,7 +117,7 @@ def _parse_preset(ctx: _Context, where: str, item: T.Any) -> EquipmentPreset:
) )
def _parse_monster(ctx: _Context, where: str, item: T.Any) -> Monster: def _parse_monster(ctx: _Context, where: str, item: Any) -> Monster:
obj = _check_keys(ctx, where, item, ["name", "level", "item"]) obj = _check_keys(ctx, where, item, ["name", "level", "item"])
loot = obj["item"] loot = obj["item"]
return Monster( return Monster(
@@ -126,13 +127,13 @@ def _parse_monster(ctx: _Context, where: str, item: T.Any) -> Monster:
) )
def _parse_attrs(ctx: _Context, where: str, value: T.Any) -> T.List[StatType]: def _parse_attrs(ctx: _Context, where: str, value: Any) -> list[StatType]:
if not isinstance(value, list): if not isinstance(value, list):
raise ctx.error(f"{where} must be a list of stats") raise ctx.error(f"{where} must be a list of stats")
return [_stat(ctx, f"{where}[{i}]", stat) for i, stat in enumerate(value)] return [_stat(ctx, f"{where}[{i}]", stat) for i, stat in enumerate(value)]
def _parse_race(ctx: _Context, where: str, item: T.Any) -> Race: def _parse_race(ctx: _Context, where: str, item: Any) -> Race:
obj = _check_keys(ctx, where, item, ["name", "attr"]) obj = _check_keys(ctx, where, item, ["name", "attr"])
return Race( return Race(
_str(ctx, f"{where}.name", obj["name"]), _str(ctx, f"{where}.name", obj["name"]),
@@ -140,7 +141,7 @@ def _parse_race(ctx: _Context, where: str, item: T.Any) -> Race:
) )
def _parse_class(ctx: _Context, where: str, item: T.Any) -> Class: def _parse_class(ctx: _Context, where: str, item: Any) -> Class:
obj = _check_keys(ctx, where, item, ["name", "attr"]) obj = _check_keys(ctx, where, item, ["name", "attr"])
return Class( return Class(
_str(ctx, f"{where}.name", obj["name"]), _str(ctx, f"{where}.name", obj["name"]),
@@ -148,7 +149,7 @@ def _parse_class(ctx: _Context, where: str, item: T.Any) -> Class:
) )
_PARSERS: T.Dict[str, T.Callable[[_Context, str, T.Any], T.Any]] = { _PARSERS: dict[str, Callable[[_Context, str, Any], Any]] = {
"spells": _parse_str, "spells": _parse_str,
"offense_attrib": _parse_modifier, "offense_attrib": _parse_modifier,
"defense_attrib": _parse_modifier, "defense_attrib": _parse_modifier,
@@ -171,7 +172,7 @@ _PARSERS: T.Dict[str, T.Callable[[_Context, str, T.Any], T.Any]] = {
assert set(_PARSERS) == set(DATA_FILES) assert set(_PARSERS) == set(DATA_FILES)
def _parse(name: str, path: str, text: str) -> T.Tuple[T.Any, ...]: def _parse(name: str, path: str, text: str) -> tuple[Any, ...]:
ctx = _Context(path) ctx = _Context(path)
try: try:
value = json.loads(text) value = json.loads(text)
@@ -185,26 +186,26 @@ def _parse(name: str, path: str, text: str) -> T.Tuple[T.Any, ...]:
return tuple(parser(ctx, f"[{i}]", item) for i, item in enumerate(value)) return tuple(parser(ctx, f"[{i}]", item) for i, item in enumerate(value))
def _builtin_dir() -> T.Any: def _builtin_dir() -> Any:
return resources.files("pqcli") / "data" return resources.files("pqcli") / "data"
def _read_text(path: T.Any, display: str) -> str: def _read_text(path: Any, display: str) -> str:
try: try:
return T.cast(str, path.read_text(encoding="utf-8")) return cast(str, path.read_text(encoding="utf-8"))
except UnicodeDecodeError as ex: except UnicodeDecodeError as ex:
raise DataError(f"{display}: not valid UTF-8: {ex}") from ex raise DataError(f"{display}: not valid UTF-8: {ex}") from ex
except OSError as ex: except OSError as ex:
raise DataError(f"{display}: cannot read: {ex}") from ex raise DataError(f"{display}: cannot read: {ex}") from ex
def load_game_data(override_dir: T.Optional[Path] = None) -> GameData: def load_game_data(override_dir: Path | None = None) -> GameData:
"""Load the built-in word lists, replacing any that override_dir has. """Load the built-in word lists, replacing any that override_dir has.
A missing override_dir is fine. Each <name>.json in it replaces the A missing override_dir is fine. Each <name>.json in it replaces the
whole built-in list of that name. whole built-in list of that name.
""" """
overrides: T.Set[str] = set() overrides: set[str] = set()
if override_dir is not None and override_dir.exists(): if override_dir is not None and override_dir.exists():
if not override_dir.is_dir(): if not override_dir.is_dir():
raise DataError(f"{override_dir}: not a directory") raise DataError(f"{override_dir}: not a directory")
@@ -218,11 +219,11 @@ def load_game_data(override_dir: T.Optional[Path] = None) -> GameData:
file=sys.stderr, file=sys.stderr,
) )
values: T.Dict[str, T.Tuple[T.Any, ...]] = {} values: dict[str, tuple[Any, ...]] = {}
for name in DATA_FILES: for name in DATA_FILES:
if name in overrides: if name in overrides:
assert override_dir is not None assert override_dir is not None
source: T.Any = override_dir / f"{name}.json" source: Any = override_dir / f"{name}.json"
display = str(source) display = str(source)
else: else:
source = _builtin_dir() / f"{name}.json" source = _builtin_dir() / f"{name}.json"
@@ -231,7 +232,7 @@ def load_game_data(override_dir: T.Optional[Path] = None) -> GameData:
return GameData(**values) return GameData(**values)
def dump_builtin_data(target_dir: Path) -> T.List[Path]: def dump_builtin_data(target_dir: Path) -> list[Path]:
"""Copy the built-in data files to target_dir, never overwriting.""" """Copy the built-in data files to target_dir, never overwriting."""
targets = [target_dir / f"{name}.json" for name in DATA_FILES] targets = [target_dir / f"{name}.json" for name in DATA_FILES]
existing = [path for path in targets if path.exists()] existing = [path for path in targets if path.exists()]
+1 -2
View File
@@ -1,5 +1,4 @@
import datetime import datetime
import typing as T
from pqcli import random from pqcli import random
@@ -114,7 +113,7 @@ def definite(subject: str, qty: int) -> str:
return "the " + subject return "the " + subject
def prefix(a: T.List[str], m: int, subject: str, sep: str = " ") -> str: def prefix(a: list[str], m: int, subject: str, sep: str = " ") -> str:
m = abs(m) m = abs(m)
if m < 1 or m > len(a): if m < 1 or m > len(a):
return subject return subject
+53 -54
View File
@@ -3,9 +3,10 @@ import functools
import itertools import itertools
import logging import logging
import math import math
import typing as T
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable, Iterator, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, cast
from pqcli import random from pqcli import random
from pqcli.config import ( from pqcli.config import (
@@ -35,23 +36,21 @@ from pqcli.random import Alea, RngState
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_SIGNALS: T.Dict[ _SIGNALS: dict[tuple["SignalMixin", str], list[Callable[..., Any]]] = (
T.Tuple["SignalMixin", str], T.List[T.Callable[..., T.Any]] defaultdict(list)
] = defaultdict(list) )
class SignalMixin: class SignalMixin:
def emit(self, signal_name: str, *user_data: T.Any) -> None: def emit(self, signal_name: str, *user_data: Any) -> None:
for callback in _SIGNALS[self, signal_name]: for callback in _SIGNALS[self, signal_name]:
callback(*user_data) callback(*user_data)
def connect( def connect(self, signal_name: str, callback: Callable[..., Any]) -> None:
self, signal_name: str, callback: T.Callable[..., T.Any]
) -> None:
_SIGNALS[self, signal_name].append(callback) _SIGNALS[self, signal_name].append(callback)
def disconnect( def disconnect(
self, signal_name: str, callback: T.Callable[..., T.Any] self, signal_name: str, callback: Callable[..., Any]
) -> None: ) -> None:
try: try:
idx = _SIGNALS[self, signal_name].index(callback) idx = _SIGNALS[self, signal_name].index(callback)
@@ -108,10 +107,10 @@ class Bar(SignalMixin):
class Stats(SignalMixin): class Stats(SignalMixin):
signals = ["change"] signals = ["change"]
def __init__(self, values: T.Dict[StatType, int]) -> None: def __init__(self, values: dict[StatType, int]) -> None:
self._values = values self._values = values
def __iter__(self) -> T.Iterator[T.Tuple[StatType, int]]: def __iter__(self) -> Iterator[tuple[StatType, int]]:
return iter(self._values.items()) return iter(self._values.items())
def __getitem__(self, stat: StatType) -> int: def __getitem__(self, stat: StatType) -> int:
@@ -137,22 +136,22 @@ class QuestBook(SignalMixin):
signals = ["start_act", "start_quest"] signals = ["start_act", "start_quest"]
def __init__(self) -> None: def __init__(self) -> None:
self._quests: T.List[str] = [] self._quests: list[str] = []
self._act = 0 self._act = 0
self.plot_bar = Bar(max_=1) self.plot_bar = Bar(max_=1)
self.quest_bar = Bar(max_=1) self.quest_bar = Bar(max_=1)
self.monster: T.Optional[Monster] = None self.monster: Monster | None = None
@property @property
def act(self) -> int: def act(self) -> int:
return self._act return self._act
@property @property
def quests(self) -> T.List[str]: def quests(self) -> list[str]:
return self._quests return self._quests
@property @property
def current_quest(self) -> T.Optional[str]: def current_quest(self) -> str | None:
if self._quests: if self._quests:
return self._quests[-1] return self._quests[-1]
return None return None
@@ -178,7 +177,7 @@ class Inventory(SignalMixin):
def __init__(self, capacity: int = 0) -> None: def __init__(self, capacity: int = 0) -> None:
self._gold = 0 self._gold = 0
self._items: T.List[InventoryItem] = [] self._items: list[InventoryItem] = []
self.encum_bar = Bar(max_=capacity) self.encum_bar = Bar(max_=capacity)
@property @property
@@ -188,7 +187,7 @@ class Inventory(SignalMixin):
def __len__(self) -> int: def __len__(self) -> int:
return len(self._items) return len(self._items)
def __iter__(self) -> T.Iterator[InventoryItem]: def __iter__(self) -> Iterator[InventoryItem]:
return iter(self._items) return iter(self._items)
def __getitem__(self, idx: int) -> InventoryItem: def __getitem__(self, idx: int) -> InventoryItem:
@@ -234,16 +233,16 @@ class Equipment(SignalMixin):
signals = ["change"] signals = ["change"]
def __init__(self) -> None: def __init__(self) -> None:
self._items: T.Dict[EquipmentType, str] = { self._items: dict[EquipmentType, str] = {
EquipmentType.weapon: "Sharp Rock", EquipmentType.weapon: "Sharp Rock",
EquipmentType.hauberk: "-3 Burlap", EquipmentType.hauberk: "-3 Burlap",
} }
self.best = self[EquipmentType.weapon] self.best = self[EquipmentType.weapon]
def __iter__(self) -> T.Iterator[T.Tuple[EquipmentType, str]]: def __iter__(self) -> Iterator[tuple[EquipmentType, str]]:
return iter(self._items.items()) return iter(self._items.items())
def __getitem__(self, equipment_type: EquipmentType) -> T.Optional[str]: def __getitem__(self, equipment_type: EquipmentType) -> str | None:
return self._items.get(equipment_type, None) return self._items.get(equipment_type, None)
def put(self, equipment_type: EquipmentType, item_name: str) -> None: def put(self, equipment_type: EquipmentType, item_name: str) -> None:
@@ -267,9 +266,9 @@ class SpellBook(SignalMixin):
signals = ["add", "change"] signals = ["add", "change"]
def __init__(self) -> None: def __init__(self) -> None:
self._spells: T.List[Spell] = [] self._spells: list[Spell] = []
def __iter__(self) -> T.Iterator[Spell]: def __iter__(self) -> Iterator[Spell]:
return iter(self._spells) return iter(self._spells)
def __bool__(self) -> bool: def __bool__(self) -> bool:
@@ -292,7 +291,7 @@ class SpellBook(SignalMixin):
self.emit("add", spell) self.emit("add", spell)
@property @property
def best(self) -> T.Optional[Spell]: def best(self) -> Spell | None:
if not self._spells: if not self._spells:
return None return None
return max(self._spells, key=lambda spell: spell.level) return max(self._spells, key=lambda spell: spell.level)
@@ -306,7 +305,7 @@ class BaseTask:
@dataclass @dataclass
class KillTask(BaseTask): class KillTask(BaseTask):
monster: T.Optional[Monster] = None monster: Monster | None = None
class BuyTask(BaseTask): class BuyTask(BaseTask):
@@ -347,7 +346,7 @@ class Player(SignalMixin):
race: Race, race: Race,
class_: Class, class_: Class,
stats: Stats, stats: Stats,
rng: T.Optional[Alea] = None, rng: Alea | None = None,
) -> None: ) -> None:
self.name: str = name self.name: str = name
self.birthday: datetime.datetime = birthday self.birthday: datetime.datetime = birthday
@@ -366,10 +365,10 @@ class Player(SignalMixin):
self.spell_book = SpellBook() self.spell_book = SpellBook()
self.task_bar = Bar(max_=1) self.task_bar = Bar(max_=1)
self.task: T.Optional[BaseTask] = None self.task: BaseTask | None = None
self.queue: T.List[BaseTask] = [] self.queue: list[BaseTask] = []
def __setstate__(self, obj: T.Any) -> None: def __setstate__(self, obj: Any) -> None:
self.__dict__.update(obj) self.__dict__.update(obj)
self.elapsed = obj.get("elapsed", 0.0) self.elapsed = obj.get("elapsed", 0.0)
if "rng" not in obj: if "rng" not in obj:
@@ -402,7 +401,7 @@ class Player(SignalMixin):
self.emit("level_up") self.emit("level_up")
def win_stat(self) -> bool: def win_stat(self) -> bool:
chosen_stat: T.Optional[StatType] = None chosen_stat: StatType | None = None
if random.odds(1, 2): if random.odds(1, 2):
chosen_stat = random.choice(list(StatType)) chosen_stat = random.choice(list(StatType))
@@ -437,9 +436,9 @@ class Player(SignalMixin):
def win_equipment(self, data: GameData) -> None: def win_equipment(self, data: GameData) -> None:
choice = random.choice(list(EquipmentType)) choice = random.choice(list(EquipmentType))
stuff: T.Sequence[EquipmentPreset] stuff: Sequence[EquipmentPreset]
better: T.Sequence[Modifier] better: Sequence[Modifier]
worse: T.Sequence[Modifier] worse: Sequence[Modifier]
if choice == EquipmentType.weapon: if choice == EquipmentType.weapon:
stuff = data.weapons stuff = data.weapons
@@ -566,7 +565,7 @@ class Simulation:
self.player.task_bar.max_ / 1000 self.player.task_bar.max_ / 1000
) )
elif self.player.quest_book.act == 0 and not isinstance( elif self.player.quest_book.act == 0 and not isinstance(
self.player.task, (LoadingTask, PlotTask) self.player.task, LoadingTask | PlotTask
): ):
# the Prologue story tasks fill the plot bar # the Prologue story tasks fill the plot bar
self.player.quest_book.plot_bar.increment( self.player.quest_book.plot_bar.increment(
@@ -599,7 +598,7 @@ class Simulation:
self.player.inventory.add_gold(-self.player.equip_price()) self.player.inventory.add_gold(-self.player.equip_price())
self.player.win_equipment(self.data) self.player.win_equipment(self.data)
elif isinstance(self.player.task, (HeadingToMarketTask, SellTask)): elif isinstance(self.player.task, HeadingToMarketTask | SellTask):
if isinstance(self.player.task, SellTask): if isinstance(self.player.task, SellTask):
item = self.player.inventory[0] item = self.player.inventory[0]
amount = item.quantity * self.player.level amount = item.quantity * self.player.level
@@ -629,7 +628,7 @@ class Simulation:
self.player.set_task( self.player.set_task(
HeadingToMarketTask("Heading to market to sell loot", 4000) HeadingToMarketTask("Heading to market to sell loot", 4000)
) )
elif not isinstance(old, (KillTask, HeadingToKillingFieldsTask)): elif not isinstance(old, KillTask | HeadingToKillingFieldsTask):
if self.player.inventory.gold > self.player.equip_price(): if self.player.inventory.gold > self.player.equip_price():
self.player.set_task( self.player.set_task(
BuyTask( BuyTask(
@@ -838,35 +837,35 @@ def special_item(data: GameData) -> str:
return ( return (
interesting_item(data) interesting_item(data)
+ " of " + " of "
+ T.cast(str, random.choice(data.item_ofs)) + cast(str, random.choice(data.item_ofs))
) )
def interesting_item(data: GameData) -> str: def interesting_item(data: GameData) -> str:
return ( return (
T.cast(str, random.choice(data.item_attrib)) cast(str, random.choice(data.item_attrib))
+ " " + " "
+ T.cast(str, random.choice(data.specials)) + cast(str, random.choice(data.specials))
) )
def boring_item(data: GameData) -> str: def boring_item(data: GameData) -> str:
return T.cast(str, random.choice(data.boring_items)) return cast(str, random.choice(data.boring_items))
def impressive_guy(data: GameData) -> str: def impressive_guy(data: GameData) -> str:
if random.below(2): if random.below(2):
title = T.cast(str, random.choice(data.impressive_titles)) title = cast(str, random.choice(data.impressive_titles))
race = T.cast(Race, random.choice(data.races)) race = cast(Race, random.choice(data.races))
return f"the {title} of the {plural(race.name)}" return f"the {title} of the {plural(race.name)}"
title = T.cast(str, random.choice(data.impressive_titles)) title = cast(str, random.choice(data.impressive_titles))
return f"{title} {generate_name()} of {generate_name()}" return f"{title} {generate_name()} of {generate_name()}"
def unnamed_monster(data: GameData, level: int, iterations: int) -> Monster: def unnamed_monster(data: GameData, level: int, iterations: int) -> Monster:
result = T.cast(Monster, random.choice(data.monsters)) result = cast(Monster, random.choice(data.monsters))
for _ in range(iterations): for _ in range(iterations):
alternative = T.cast(Monster, random.choice(data.monsters)) alternative = cast(Monster, random.choice(data.monsters))
if abs(level - alternative.level) < abs(level - result.level): if abs(level - alternative.level) < abs(level - result.level):
result = alternative result = alternative
return result return result
@@ -878,18 +877,18 @@ def named_monster(data: GameData, level: int) -> str:
def pick_equipment( def pick_equipment(
source: T.Sequence[EquipmentPreset], goal: int source: Sequence[EquipmentPreset], goal: int
) -> EquipmentPreset: ) -> EquipmentPreset:
result = T.cast(EquipmentPreset, random.choice(source)) result = cast(EquipmentPreset, random.choice(source))
for _ in range(5): for _ in range(5):
alternative = T.cast(EquipmentPreset, random.choice(source)) alternative = cast(EquipmentPreset, random.choice(source))
if abs(goal - alternative.quality) < abs(goal - result.quality): if abs(goal - alternative.quality) < abs(goal - result.quality):
result = alternative result = alternative
return result return result
def monster_task( def monster_task(
data: GameData, player_level: int, quest_monster: T.Optional[Monster] data: GameData, player_level: int, quest_monster: Monster | None
) -> KillTask: ) -> KillTask:
level = player_level level = player_level
for _ in range(level): for _ in range(level):
@@ -899,7 +898,7 @@ def monster_task(
level = 1 level = 1
is_definite = False is_definite = False
monster: T.Optional[Monster] = None monster: Monster | None = None
if random.odds(1, 25): if random.odds(1, 25):
# use an NPC every once in a while # use an NPC every once in a while
race = random.choice(data.races) race = random.choice(data.races)
@@ -968,7 +967,7 @@ def monster_task(
verb = ( verb = (
data.combat_verbs[0] data.combat_verbs[0]
if len(data.combat_verbs) == 1 if len(data.combat_verbs) == 1
else T.cast(str, random.choice(data.combat_verbs)) else cast(str, random.choice(data.combat_verbs))
) )
description = verb.replace(TARGET_PLACEHOLDER, result) description = verb.replace(TARGET_PLACEHOLDER, result)
return KillTask(description, duration, monster=monster) return KillTask(description, duration, monster=monster)
@@ -982,9 +981,9 @@ class _Roll:
class StatsBuilder: class StatsBuilder:
def __init__(self, rng: T.Optional[Alea] = None) -> None: def __init__(self, rng: Alea | None = None) -> None:
self._rng = rng or Alea() self._rng = rng or Alea()
self.history: T.List[_Roll] = [] self.history: list[_Roll] = []
@property @property
def seed(self) -> RngState: def seed(self) -> RngState:
@@ -998,7 +997,7 @@ class StatsBuilder:
def roll(self) -> Stats: def roll(self) -> Stats:
seed = self._rng.state() seed = self._rng.state()
with random.using(self._rng): with random.using(self._rng):
values: T.Dict[StatType, int] = { values: dict[StatType, int] = {
stat: 3 + random.below(6) + random.below(6) + random.below(6) stat: 3 + random.below(6) + random.below(6) + random.below(6)
for stat in PRIME_STATS for stat in PRIME_STATS
} }
@@ -1024,7 +1023,7 @@ def create_player(
race: Race, race: Race,
class_: Class, class_: Class,
stats: Stats, stats: Stats,
seed: T.Optional[RngState] = None, seed: RngState | None = None,
) -> Player: ) -> Player:
player = Player( player = Player(
birthday=datetime.datetime.now(), birthday=datetime.datetime.now(),
+7 -6
View File
@@ -1,8 +1,9 @@
import contextlib import contextlib
import time import time
import typing as T from collections.abc import Callable, Iterator, Sequence
from typing import Any
RngState = T.Tuple[float, float, float, int] RngState = tuple[float, float, float, int]
_2_POW_32 = 0x100000000 _2_POW_32 = 0x100000000
_2_POW_MINUS_32 = 2.3283064365386963e-10 _2_POW_MINUS_32 = 2.3283064365386963e-10
@@ -13,7 +14,7 @@ def _to_uint32(value: float) -> float:
return float(int(value) % _2_POW_32) return float(int(value) % _2_POW_32)
def _mash() -> T.Callable[[str], float]: def _mash() -> Callable[[str], float]:
n = float(0xEFC8249D) n = float(0xEFC8249D)
def mash(data: str) -> float: def mash(data: str) -> float:
@@ -90,7 +91,7 @@ def seed(source: str) -> None:
@contextlib.contextmanager @contextlib.contextmanager
def using(rng: Alea) -> T.Iterator[None]: def using(rng: Alea) -> Iterator[None]:
"""Draw from rng instead of the shared generator inside the block.""" """Draw from rng instead of the shared generator inside the block."""
global _current global _current
previous = _current previous = _current
@@ -101,11 +102,11 @@ def using(rng: Alea) -> T.Iterator[None]:
_current = previous _current = previous
def choice(source: T.Sequence[T.Any]) -> T.Any: def choice(source: Sequence[Any]) -> Any:
return source[below(len(source))] return source[below(len(source))]
def choice_low(source: T.Sequence[T.Any]) -> T.Any: def choice_low(source: Sequence[Any]) -> Any:
return source[below_low(len(source))] return source[below_low(len(source))]
+2 -3
View File
@@ -1,6 +1,5 @@
import logging import logging
import pickle import pickle
import typing as T
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -8,13 +7,13 @@ from pqcli.mechanic import Player
class Roster: class Roster:
def __init__(self, path: Path, players: T.List[Player]) -> None: def __init__(self, path: Path, players: list[Player]) -> None:
self.path = path self.path = path
self.players = players self.players = players
self._last_save = datetime.now() self._last_save = datetime.now()
@staticmethod @staticmethod
def load(path: T.Union[str, Path]) -> "Roster": def load(path: str | Path) -> "Roster":
real_path = Path(path) real_path = Path(path)
if real_path.exists(): if real_path.exists():
return Roster( return Roster(
+1 -2
View File
@@ -1,6 +1,5 @@
import abc import abc
import argparse import argparse
import typing as T
from pqcli.game_data import GameData from pqcli.game_data import GameData
from pqcli.mechanic import Player from pqcli.mechanic import Player
@@ -11,7 +10,7 @@ class BaseUserInterface(abc.ABC):
def __init__( def __init__(
self, self,
roster: Roster, roster: Roster,
player: T.Optional[Player], player: Player | None,
args: argparse.Namespace, args: argparse.Namespace,
data: GameData, data: GameData,
) -> None: ) -> None:
+9 -8
View File
@@ -4,8 +4,9 @@ import logging
import signal import signal
import sys import sys
import time import time
import typing as T from collections.abc import Sequence
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any
from pqcli import lingo from pqcli import lingo
from pqcli.config import PRIME_STATS from pqcli.config import PRIME_STATS
@@ -36,13 +37,13 @@ class BasicUserInterface(BaseUserInterface):
def __init__( def __init__(
self, self,
roster: Roster, roster: Roster,
player: T.Optional[Player], player: Player | None,
args: argparse.Namespace, args: argparse.Namespace,
data: GameData, data: GameData,
) -> None: ) -> None:
super().__init__(roster, player, args, data) super().__init__(roster, player, args, data)
def signal_handler(sig: T.Any, frame: T.Any) -> None: def signal_handler(sig: Any, frame: Any) -> None:
logging.info("Quitting") logging.info("Quitting")
if self.args.use_saves: if self.args.use_saves:
self.roster.save() self.roster.save()
@@ -103,7 +104,7 @@ class BasicUserInterface(BaseUserInterface):
self.quit() self.quit()
break break
def create_player(self, *, auto_play: bool = True) -> T.Optional[Player]: def create_player(self, *, auto_play: bool = True) -> Player | None:
name = input("Name your new character: ") name = input("Name your new character: ")
if not name: if not name:
print("Cancelled.") print("Cancelled.")
@@ -196,7 +197,7 @@ class BasicUserInterface(BaseUserInterface):
if self.args.use_saves: if self.args.use_saves:
self.roster.save() self.roster.save()
def choose_player(self) -> T.Optional[Player]: def choose_player(self) -> Player | None:
if not self.roster.players: if not self.roster.players:
print("No characters to choose from!") print("No characters to choose from!")
return None return None
@@ -216,9 +217,9 @@ class BasicUserInterface(BaseUserInterface):
def menu( def menu(
self, self,
options: T.Sequence[T.Tuple[T.Any, str]], options: Sequence[tuple[Any, str]],
title: T.Optional[str] = None, title: str | None = None,
) -> T.Any: ) -> Any:
print() print()
if title: if title:
print(f"{title}:") print(f"{title}:")
+6 -6
View File
@@ -3,7 +3,7 @@ import curses
import os import os
import signal import signal
import time import time
import typing as T from typing import Any, cast
from pqcli import lingo from pqcli import lingo
from pqcli.config import Class, Race from pqcli.config import Class, Race
@@ -68,7 +68,7 @@ class CursesUserInterface(BaseUserInterface):
curses.init_pair(COLOR_PROGRESSBAR, 0, 9) curses.init_pair(COLOR_PROGRESSBAR, 0, 9)
curses.init_pair(COLOR_HIGHLIGHT, 3, -1) curses.init_pair(COLOR_HIGHLIGHT, 3, -1)
def signal_handler(sig: T.Any, frame: T.Any) -> None: def signal_handler(sig: Any, frame: Any) -> None:
if self.args.use_saves: if self.args.use_saves:
self.roster.save() self.roster.save()
exit(0) exit(0)
@@ -91,7 +91,7 @@ class CursesUserInterface(BaseUserInterface):
self._switch_to_roster_view() self._switch_to_roster_view()
while True: while True:
key = T.cast(int, self._screen.getch()) key = cast(int, self._screen.getch())
if key == curses.ERR: if key == curses.ERR:
time.sleep(0.1) time.sleep(0.1)
@@ -115,7 +115,7 @@ class CursesUserInterface(BaseUserInterface):
self._switch_view(self._roster_view) self._switch_view(self._roster_view)
def _switch_to_create_char_name_view( def _switch_to_create_char_name_view(
self, player_name: T.Optional[str] = None self, player_name: str | None = None
) -> None: ) -> None:
view = ChooseCharacterNameView(self._screen, player_name) view = ChooseCharacterNameView(self._screen, player_name)
view.on_cancel += self._switch_to_roster_view view.on_cancel += self._switch_to_roster_view
@@ -123,7 +123,7 @@ class CursesUserInterface(BaseUserInterface):
self._switch_view(view) self._switch_view(view)
def _switch_to_create_char_race_view( def _switch_to_create_char_race_view(
self, player_name: str, race: T.Optional[Race] = None self, player_name: str, race: Race | None = None
) -> None: ) -> None:
view = ChooseCharacterRaceView(self._screen, self.data.races, race) view = ChooseCharacterRaceView(self._screen, self.data.races, race)
view.on_cancel += lambda: self._switch_to_create_char_name_view( view.on_cancel += lambda: self._switch_to_create_char_name_view(
@@ -135,7 +135,7 @@ class CursesUserInterface(BaseUserInterface):
self._switch_view(view) self._switch_view(view)
def _switch_to_create_char_class_view( def _switch_to_create_char_class_view(
self, player_name: str, race: Race, class_: T.Optional[Class] = None self, player_name: str, race: Race, class_: Class | None = None
) -> None: ) -> None:
view = ChooseCharacterClassView( view = ChooseCharacterClassView(
self._screen, self.data.classes, class_ self._screen, self.data.classes, class_
+5 -4
View File
@@ -1,14 +1,15 @@
import typing as T from collections.abc import Callable
from typing import Any
class EventHandler: class EventHandler:
def __init__(self) -> None: def __init__(self) -> None:
self.callbacks: T.List[T.Callable[..., T.Any]] = [] self.callbacks: list[Callable[..., Any]] = []
def __call__(self, *args: T.Any, **kwargs: T.Any) -> None: def __call__(self, *args: Any, **kwargs: Any) -> None:
for callback in self.callbacks: for callback in self.callbacks:
callback(*args, **kwargs) callback(*args, **kwargs)
def __iadd__(self, callback: T.Callable[..., T.Any]) -> "EventHandler": def __iadd__(self, callback: Callable[..., Any]) -> "EventHandler":
self.callbacks.append(callback) self.callbacks.append(callback)
return self return self
+17 -4
View File
@@ -1,7 +1,8 @@
import curses import curses
import curses.ascii import curses.ascii
import typing as T from collections.abc import Callable, Iterable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, TypeVar, overload
KEYS_CYCLE = {curses.ascii.TAB} KEYS_CYCLE = {curses.ascii.TAB}
KEYS_DOWN = set(map(ord, "jJ")) | {curses.KEY_DOWN} KEYS_DOWN = set(map(ord, "jJ")) | {curses.KEY_DOWN}
@@ -13,10 +14,22 @@ KEYS_CANCEL = set(map(ord, "qQ")) | {curses.ascii.ESC}
@dataclass @dataclass
class Choice: class Choice:
keys: T.List[int] keys: list[int]
desc: str desc: str
callback: T.Callable[..., T.Any] callback: Callable[..., Any]
def first(source: T.Iterable[T.Any], default: T.Any = None) -> T.Any: ItemT = TypeVar("ItemT")
DefaultT = TypeVar("DefaultT")
@overload
def first(source: Iterable[ItemT]) -> ItemT | None: ...
@overload
def first(source: Iterable[ItemT], default: DefaultT) -> ItemT | DefaultT: ...
def first(source: Iterable[Any], default: Any = None) -> Any:
return next(iter(source), default) return next(iter(source), default)
+2 -2
View File
@@ -1,8 +1,8 @@
import typing as T from typing import Any
class BaseView: class BaseView:
def __init__(self, screen: T.Any) -> None: def __init__(self, screen: Any) -> None:
self.screen = screen self.screen = screen
def start(self) -> None: def start(self) -> None:
@@ -1,5 +1,5 @@
import functools import functools
import typing as T from typing import Any
from pqcli.lingo import act_name, to_roman from pqcli.lingo import act_name, to_roman
from pqcli.roster import Roster from pqcli.roster import Roster
@@ -8,11 +8,11 @@ from pqcli.ui.curses.views.menu_view import MenuView
class ChooseCharacterView(MenuView): class ChooseCharacterView(MenuView):
def __init__(self, screen: T.Any, roster: Roster, title: str) -> None: def __init__(self, screen: Any, roster: Roster, title: str) -> None:
super().__init__(screen, title) super().__init__(screen, title)
for y, player in enumerate(roster.players, 1): for y, player in enumerate(roster.players, 1):
key: T.Optional[str] = str(y % 10) if y <= 10 else None key: str | None = str(y % 10) if y <= 10 else None
best_equip = player.equipment.best best_equip = player.equipment.best
+3 -3
View File
@@ -1,4 +1,4 @@
import typing as T from typing import Any
from pqcli.ui.curses.event_handler import EventHandler from pqcli.ui.curses.event_handler import EventHandler
from pqcli.ui.curses.util import Choice from pqcli.ui.curses.util import Choice
@@ -8,14 +8,14 @@ from .base_view import BaseView
class ConfirmView(BaseView): class ConfirmView(BaseView):
def __init__(self, screen: T.Any, title: str) -> None: def __init__(self, screen: Any, title: str) -> None:
super().__init__(screen) super().__init__(screen)
self.title = f"{title}:" self.title = f"{title}:"
self.on_cancel = EventHandler() self.on_cancel = EventHandler()
self.on_confirm = EventHandler() self.on_confirm = EventHandler()
self.choices: T.List[Choice] = [ self.choices: list[Choice] = [
Choice( Choice(
keys=list(map(ord, "yY")), keys=list(map(ord, "yY")),
desc="[Y] Yes", desc="[Y] Yes",
@@ -1,5 +1,6 @@
import functools import functools
import typing as T from collections.abc import Sequence
from typing import Any
from pqcli.config import Class from pqcli.config import Class
from pqcli.ui.curses.util import KEYS_CANCEL, Choice from pqcli.ui.curses.util import KEYS_CANCEL, Choice
@@ -9,9 +10,9 @@ from pqcli.ui.curses.views.menu_view import MenuView
class ChooseCharacterClassView(MenuView): class ChooseCharacterClassView(MenuView):
def __init__( def __init__(
self, self,
screen: T.Any, screen: Any,
classes: T.Sequence[Class], classes: Sequence[Class],
class_: T.Optional[Class] = None, class_: Class | None = None,
) -> None: ) -> None:
super().__init__( super().__init__(
screen, screen,
@@ -20,7 +21,7 @@ class ChooseCharacterClassView(MenuView):
) )
for y, class_ in enumerate(classes, 1): for y, class_ in enumerate(classes, 1):
key: T.Optional[str] = str(y % 10) if y <= 10 else None key: str | None = str(y % 10) if y <= 10 else None
self._choices.append( self._choices.append(
Choice( Choice(
keys=[ord(key)] if key is not None else [], keys=[ord(key)] if key is not None else [],
@@ -1,6 +1,6 @@
import curses import curses
import curses.ascii import curses.ascii
import typing as T from typing import Any
from pqcli.mechanic import generate_name from pqcli.mechanic import generate_name
from pqcli.ui.curses.event_handler import EventHandler from pqcli.ui.curses.event_handler import EventHandler
@@ -10,17 +10,15 @@ from pqcli.ui.curses.widgets.focusable import focus_standout
class ChooseCharacterNameView(BaseView): class ChooseCharacterNameView(BaseView):
def __init__( def __init__(self, screen: Any, character_name: str | None = None) -> None:
self, screen: T.Any, character_name: T.Optional[str] = None
) -> None:
super().__init__(screen) super().__init__(screen)
self.on_cancel = EventHandler() self.on_cancel = EventHandler()
self.on_confirm = EventHandler() self.on_confirm = EventHandler()
self._active_widget = 0 self._active_widget = 0
self._win: T.Optional[T.Any] = None self._win: Any | None = None
self._text_win: T.Optional[T.Any] = None self._text_win: Any | None = None
self._text = character_name or generate_name() self._text = character_name or generate_name()
def start(self) -> None: def start(self) -> None:
@@ -1,5 +1,6 @@
import functools import functools
import typing as T from collections.abc import Sequence
from typing import Any
from pqcli.config import Race from pqcli.config import Race
from pqcli.ui.curses.util import KEYS_CANCEL, Choice from pqcli.ui.curses.util import KEYS_CANCEL, Choice
@@ -9,9 +10,9 @@ from pqcli.ui.curses.views.menu_view import MenuView
class ChooseCharacterRaceView(MenuView): class ChooseCharacterRaceView(MenuView):
def __init__( def __init__(
self, self,
screen: T.Any, screen: Any,
races: T.Sequence[Race], races: Sequence[Race],
race: T.Optional[Race] = None, race: Race | None = None,
) -> None: ) -> None:
super().__init__( super().__init__(
screen, screen,
@@ -20,7 +21,7 @@ class ChooseCharacterRaceView(MenuView):
) )
for y, race in enumerate(races, 1): for y, race in enumerate(races, 1):
key: T.Optional[str] = str(y % 10) if y <= 10 else None key: str | None = str(y % 10) if y <= 10 else None
self._choices.append( self._choices.append(
Choice( Choice(
keys=[ord(key)] if key is not None else [], keys=[ord(key)] if key is not None else [],
@@ -1,5 +1,5 @@
import curses.ascii import curses.ascii
import typing as T from typing import Any
from pqcli.config import PRIME_STATS from pqcli.config import PRIME_STATS
from pqcli.mechanic import StatsBuilder from pqcli.mechanic import StatsBuilder
@@ -10,15 +10,15 @@ from pqcli.ui.curses.widgets.focusable import focus_standout
class ChooseCharacterStatsView(BaseView): class ChooseCharacterStatsView(BaseView):
def __init__(self, screen: T.Any) -> None: def __init__(self, screen: Any) -> None:
super().__init__(screen) super().__init__(screen)
self.on_cancel = EventHandler() self.on_cancel = EventHandler()
self.on_confirm = EventHandler() self.on_confirm = EventHandler()
self._active_widget = 0 self._active_widget = 0
self._win: T.Optional[T.Any] = None self._win: Any | None = None
self._stats_win: T.Optional[T.Any] = None self._stats_win: Any | None = None
self._stats_builder = StatsBuilder() self._stats_builder = StatsBuilder()
self._stats = self._stats_builder.roll() self._stats = self._stats_builder.roll()
+4 -4
View File
@@ -1,7 +1,7 @@
import argparse import argparse
import curses import curses
import datetime import datetime
import typing as T from typing import Any
from pqcli.game_data import GameData from pqcli.game_data import GameData
from pqcli.mechanic import Player, SignalMixin, Simulation from pqcli.mechanic import Player, SignalMixin, Simulation
@@ -30,7 +30,7 @@ from .task_progress_window import TaskProgressWindow
class GameView(BaseView): class GameView(BaseView):
def __init__( def __init__(
self, self,
screen: T.Any, screen: Any,
roster: Roster, roster: Roster,
player: Player, player: Player,
args: argparse.Namespace, args: argparse.Namespace,
@@ -225,7 +225,7 @@ class GameView(BaseView):
x=0, x=0,
) )
self._focusable_children: T.List[Focusable] = [ self._focusable_children: list[Focusable] = [
self._char_sheet_win, self._char_sheet_win,
self._spell_book_win, self._spell_book_win,
self._equipment_win, self._equipment_win,
@@ -233,7 +233,7 @@ class GameView(BaseView):
self._plot_win, self._plot_win,
self._quest_book_win, self._quest_book_win,
] ]
self._children: T.List[Widget] = self._focusable_children + [ self._children: list[Widget] = self._focusable_children + [
self._task_win self._task_win
] ]
@@ -1,4 +1,4 @@
import typing as T from typing import Any
from pqcli.mechanic import Player, StatType from pqcli.mechanic import Player, StatType
from pqcli.ui.curses.widgets import Focusable from pqcli.ui.curses.widgets import Focusable
@@ -8,7 +8,7 @@ from .progress_bar_window import DataTableProgressBarWindow
class CharacterSheetWindow(Focusable, DataTableProgressBarWindow): class CharacterSheetWindow(Focusable, DataTableProgressBarWindow):
def __init__( def __init__(
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int self, player: Player, parent: Any, h: int, w: int, y: int, x: int
) -> None: ) -> None:
super().__init__( super().__init__(
parent, parent,
@@ -1,4 +1,4 @@
import typing as T from typing import Any
from pqcli.mechanic import EquipmentType, Player from pqcli.mechanic import EquipmentType, Player
from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper
@@ -6,7 +6,7 @@ from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper
class EquipmentWindow(Focusable, WindowWrapper): class EquipmentWindow(Focusable, WindowWrapper):
def __init__( def __init__(
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int self, player: Player, parent: Any, h: int, w: int, y: int, x: int
) -> None: ) -> None:
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
self._on_focus_change += self._render self._on_focus_change += self._render
@@ -39,7 +39,7 @@ class EquipmentWindow(Focusable, WindowWrapper):
self._render() self._render()
def _sync_equipment_change( def _sync_equipment_change(
self, equipment_type: EquipmentType, item_name: T.Optional[str] self, equipment_type: EquipmentType, item_name: str | None
) -> None: ) -> None:
self._data_table.set(equipment_type.value, item_name or "") self._data_table.set(equipment_type.value, item_name or "")
self._data_table.select(equipment_type.value) self._data_table.select(equipment_type.value)
@@ -1,4 +1,4 @@
import typing as T from typing import Any
from pqcli.mechanic import InventoryItem, Player from pqcli.mechanic import InventoryItem, Player
from pqcli.ui.curses.widgets import Focusable from pqcli.ui.curses.widgets import Focusable
@@ -8,7 +8,7 @@ from .progress_bar_window import DataTableProgressBarWindow
class InventoryWindow(Focusable, DataTableProgressBarWindow): class InventoryWindow(Focusable, DataTableProgressBarWindow):
def __init__( def __init__(
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int self, player: Player, parent: Any, h: int, w: int, y: int, x: int
) -> None: ) -> None:
super().__init__( super().__init__(
parent, parent,
@@ -1,4 +1,4 @@
import typing as T from typing import Any
from pqcli.lingo import act_name from pqcli.lingo import act_name
from pqcli.mechanic import Player from pqcli.mechanic import Player
@@ -11,7 +11,7 @@ class PlotWindow(Focusable, ListBoxProgressBarWindow):
cutoff = 100 cutoff = 100
def __init__( def __init__(
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int self, player: Player, parent: Any, h: int, w: int, y: int, x: int
) -> None: ) -> None:
super().__init__( super().__init__(
parent, h, w, y, x, " Plot Development ", show_time=True parent, h, w, y, x, " Plot Development ", show_time=True
@@ -1,5 +1,5 @@
import curses import curses
import typing as T from typing import Any
from pqcli.ui.curses.widgets import ( from pqcli.ui.curses.widgets import (
DataTable, DataTable,
@@ -13,7 +13,7 @@ from pqcli.ui.curses.widgets import (
class ProgressBarWindow(WindowWrapper): class ProgressBarWindow(WindowWrapper):
def __init__( def __init__(
self, self,
parent: T.Any, parent: Any,
h: int, h: int,
w: int, w: int,
y: int, y: int,
@@ -82,7 +82,7 @@ class ProgressBarWindow(WindowWrapper):
class DataTableProgressBarWindow(ProgressBarWindow): class DataTableProgressBarWindow(ProgressBarWindow):
def __init__( def __init__(
self, self,
parent: T.Any, parent: Any,
h: int, h: int,
w: int, w: int,
y: int, y: int,
@@ -123,7 +123,7 @@ class DataTableProgressBarWindow(ProgressBarWindow):
class ListBoxProgressBarWindow(ProgressBarWindow): class ListBoxProgressBarWindow(ProgressBarWindow):
def __init__( def __init__(
self, self,
parent: T.Any, parent: Any,
h: int, h: int,
w: int, w: int,
y: int, y: int,
@@ -1,4 +1,4 @@
import typing as T from typing import Any
from pqcli.mechanic import Player from pqcli.mechanic import Player
from pqcli.ui.curses.widgets import Focusable from pqcli.ui.curses.widgets import Focusable
@@ -10,7 +10,7 @@ class QuestBookWindow(Focusable, ListBoxProgressBarWindow):
cutoff = 100 cutoff = 100
def __init__( def __init__(
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int self, player: Player, parent: Any, h: int, w: int, y: int, x: int
) -> None: ) -> None:
super().__init__(parent, h, w, y, x, " Quests ", show_time=True) super().__init__(parent, h, w, y, x, " Quests ", show_time=True)
self._on_focus_change += self._render self._on_focus_change += self._render
@@ -1,4 +1,4 @@
import typing as T from typing import Any
from pqcli.lingo import to_roman from pqcli.lingo import to_roman
from pqcli.mechanic import Player, Spell from pqcli.mechanic import Player, Spell
@@ -7,7 +7,7 @@ from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper
class SpellBookWindow(Focusable, WindowWrapper): class SpellBookWindow(Focusable, WindowWrapper):
def __init__( def __init__(
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int self, player: Player, parent: Any, h: int, w: int, y: int, x: int
) -> None: ) -> None:
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
self._on_focus_change += self._render self._on_focus_change += self._render
@@ -1,4 +1,4 @@
import typing as T from typing import Any
from pqcli.mechanic import Player from pqcli.mechanic import Player
from pqcli.ui.curses.widgets import ( from pqcli.ui.curses.widgets import (
@@ -11,7 +11,7 @@ from pqcli.ui.curses.widgets import (
class TaskProgressWindow(Focusable, WindowWrapper): class TaskProgressWindow(Focusable, WindowWrapper):
def __init__( def __init__(
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int self, player: Player, parent: Any, h: int, w: int, y: int, x: int
) -> None: ) -> None:
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
+3 -3
View File
@@ -1,4 +1,4 @@
import typing as T from typing import Any
from pqcli.ui.curses.event_handler import EventHandler from pqcli.ui.curses.event_handler import EventHandler
from pqcli.ui.curses.util import Choice from pqcli.ui.curses.util import Choice
@@ -8,7 +8,7 @@ from pqcli.ui.curses.widgets import Menu
class MenuView(BaseView): class MenuView(BaseView):
def __init__( def __init__(
self, screen: T.Any, title: str, active_choice: int = 0 self, screen: Any, title: str, active_choice: int = 0
) -> None: ) -> None:
super().__init__(screen) super().__init__(screen)
@@ -16,7 +16,7 @@ class MenuView(BaseView):
self.on_confirm = EventHandler() self.on_confirm = EventHandler()
self._title = f"{title}:" self._title = f"{title}:"
self._choices: T.List[Choice] = [] self._choices: list[Choice] = []
self._active_choice = active_choice self._active_choice = active_choice
def start(self) -> None: def start(self) -> None:
+4 -4
View File
@@ -1,5 +1,5 @@
import curses import curses
import typing as T from typing import Any
from pqcli.ui.curses.colors import COLOR_LOGO, COLOR_LOGO_ALT, has_colors from pqcli.ui.curses.colors import COLOR_LOGO, COLOR_LOGO_ALT, has_colors
from pqcli.ui.curses.event_handler import EventHandler from pqcli.ui.curses.event_handler import EventHandler
@@ -29,7 +29,7 @@ LOGO = """
class MainMenu(Menu): class MainMenu(Menu):
def __init__( def __init__(
self, choices: T.List[Choice], scr_height: int, scr_width: int self, choices: list[Choice], scr_height: int, scr_width: int
) -> None: ) -> None:
super().__init__( super().__init__(
header=LOGO, header=LOGO,
@@ -50,7 +50,7 @@ class MainMenu(Menu):
class RosterView(BaseView): class RosterView(BaseView):
def __init__(self, screen: T.Any) -> None: def __init__(self, screen: Any) -> None:
super().__init__(screen) super().__init__(screen)
self.on_create = EventHandler() self.on_create = EventHandler()
@@ -58,7 +58,7 @@ class RosterView(BaseView):
self.on_delete = EventHandler() self.on_delete = EventHandler()
self.on_quit = EventHandler() self.on_quit = EventHandler()
self.choices: T.List[Choice] = [ self.choices: list[Choice] = [
Choice( Choice(
keys=list(map(ord, "cC")), keys=list(map(ord, "cC")),
desc="[C] Create new character", desc="[C] Create new character",
+5 -5
View File
@@ -1,5 +1,5 @@
import curses import curses
import typing as T from typing import Any
class Widget: class Widget:
@@ -12,10 +12,10 @@ class Widget:
class WindowWrapper(Widget): class WindowWrapper(Widget):
def __init__( def __init__(
self, parent: T.Optional[T.Any], h: int, w: int, y: int, x: int self, parent: Any | None, h: int, w: int, y: int, x: int
) -> None: ) -> None:
self._parent = parent self._parent = parent
self._win: T.Optional[T.Any] self._win: Any | None
if not parent: if not parent:
self._win = None self._win = None
@@ -30,12 +30,12 @@ class WindowWrapper(Widget):
del self._win del self._win
self._win = None self._win = None
def getbegyx(self) -> T.Tuple[int, int]: def getbegyx(self) -> tuple[int, int]:
if not self._win: if not self._win:
return (0, 0) return (0, 0)
return self._win.getbegyx() return self._win.getbegyx()
def getmaxyx(self) -> T.Tuple[int, int]: def getmaxyx(self) -> tuple[int, int]:
if not self._win: if not self._win:
return (0, 0) return (0, 0)
return self._win.getmaxyx() return self._win.getmaxyx()
+6 -6
View File
@@ -1,5 +1,5 @@
import curses import curses
import typing as T from typing import Any
from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors
@@ -9,7 +9,7 @@ from .scrollable import Scrollable
class DataTable(Scrollable): class DataTable(Scrollable):
def __init__( def __init__(
self, self,
parent: T.Any, parent: Any,
h: int, h: int,
w: int, w: int,
y: int, y: int,
@@ -19,8 +19,8 @@ class DataTable(Scrollable):
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
self._align_right = align_right self._align_right = align_right
self._items: T.List[T.Tuple[str, str]] self._items: list[tuple[str, str]]
self._selected: T.Optional[int] = None self._selected: int | None = None
def add(self, text: str, value: str) -> None: def add(self, text: str, value: str) -> None:
self._items.append((text, value)) self._items.append((text, value))
@@ -40,10 +40,10 @@ class DataTable(Scrollable):
del self._items[idx] del self._items[idx]
self.scroll_to_item(idx) self.scroll_to_item(idx)
def select(self, text: T.Optional[str]) -> None: def select(self, text: str | None) -> None:
self._selected = None if text is None else self.get_idx(text) self._selected = None if text is None else self.get_idx(text)
def get_idx(self, text: str) -> T.Optional[int]: def get_idx(self, text: str) -> int | None:
for idx, row in enumerate(self._items): for idx, row in enumerate(self._items):
if row[0].strip() == text.strip(): if row[0].strip() == text.strip():
return idx return idx
+5 -4
View File
@@ -1,13 +1,14 @@
import contextlib import contextlib
import curses import curses
import typing as T from collections.abc import Generator
from typing import Any
from pqcli.ui.curses.colors import COLOR_FOCUSED, has_colors from pqcli.ui.curses.colors import COLOR_FOCUSED, has_colors
from pqcli.ui.curses.event_handler import EventHandler from pqcli.ui.curses.event_handler import EventHandler
@contextlib.contextmanager @contextlib.contextmanager
def focus_standout(focused: bool, win: T.Any) -> T.Generator: def focus_standout(focused: bool, win: Any) -> Generator:
if focused: if focused:
if has_colors(): if has_colors():
win.attron(curses.color_pair(COLOR_FOCUSED)) win.attron(curses.color_pair(COLOR_FOCUSED))
@@ -22,7 +23,7 @@ def focus_standout(focused: bool, win: T.Any) -> T.Generator:
class Focusable: class Focusable:
def __init__(self, *args: T.Any, **kwargs: T.Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
self._focused = False self._focused = False
self._on_focus_change = EventHandler() self._on_focus_change = EventHandler()
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -37,6 +38,6 @@ class Focusable:
self._on_focus_change() self._on_focus_change()
@contextlib.contextmanager @contextlib.contextmanager
def focus_standout(self, win: T.Any) -> T.Generator: def focus_standout(self, win: Any) -> Generator:
with focus_standout(self._focused, win): with focus_standout(self._focused, win):
yield yield
+6 -6
View File
@@ -1,5 +1,5 @@
import curses import curses
import typing as T from typing import Any
from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors
@@ -7,11 +7,11 @@ from .scrollable import Scrollable
class ListBox(Scrollable): class ListBox(Scrollable):
_items: T.List[str] _items: list[str]
def __init__(self, parent: T.Any, h: int, w: int, y: int, x: int) -> None: def __init__(self, parent: Any, h: int, w: int, y: int, x: int) -> None:
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
self._selected: T.Optional[int] = None self._selected: int | None = None
def add(self, text: str) -> None: def add(self, text: str) -> None:
self._items.append(text) self._items.append(text)
@@ -22,7 +22,7 @@ class ListBox(Scrollable):
idx %= len(self._items) idx %= len(self._items)
self._items[idx] = value self._items[idx] = value
def get(self, idx: int) -> T.Optional[str]: def get(self, idx: int) -> str | None:
if not self._items: if not self._items:
return None return None
if idx < 0: if idx < 0:
@@ -35,7 +35,7 @@ class ListBox(Scrollable):
def delete(self, idx: int, count: int = 1) -> None: def delete(self, idx: int, count: int = 1) -> None:
del self._items[idx : idx + count] del self._items[idx : idx + count]
def select(self, idx: T.Optional[int]) -> None: def select(self, idx: int | None) -> None:
if idx < 0 and len(self._items): if idx < 0 and len(self._items):
idx %= len(self._items) idx %= len(self._items)
self._selected = idx self._selected = idx
+4 -4
View File
@@ -1,6 +1,6 @@
import curses import curses
import curses.ascii import curses.ascii
import typing as T from typing import Any
from pqcli.ui.curses.util import KEYS_CYCLE, KEYS_DOWN, KEYS_UP, Choice from pqcli.ui.curses.util import KEYS_CYCLE, KEYS_DOWN, KEYS_UP, Choice
from pqcli.ui.curses.widgets.focusable import focus_standout from pqcli.ui.curses.widgets.focusable import focus_standout
@@ -12,7 +12,7 @@ class Menu(Widget):
def __init__( def __init__(
self, self,
header: str, header: str,
choices: T.List[Choice], choices: list[Choice],
active_choice: int, active_choice: int,
scr_height: int, scr_height: int,
scr_width: int, scr_width: int,
@@ -28,7 +28,7 @@ class Menu(Widget):
w = max(map(len, all_lines)) + 1 w = max(map(len, all_lines)) + 1
h = len(all_lines) + 1 h = len(all_lines) + 1
self._pad: T.Optional[T.Any] = curses.newpad(h, w) self._pad: Any | None = curses.newpad(h, w)
for y, line in enumerate(self._header_lines): for y, line in enumerate(self._header_lines):
self._pad.move(y, 0) self._pad.move(y, 0)
@@ -40,7 +40,7 @@ class Menu(Widget):
del self._pad del self._pad
self._pad = None self._pad = None
def getmaxyx(self) -> T.Tuple[int, int]: def getmaxyx(self) -> tuple[int, int]:
if not self._pad: if not self._pad:
return (0, 0) return (0, 0)
return self._pad.getmaxyx() return self._pad.getmaxyx()
+4 -4
View File
@@ -1,6 +1,6 @@
import curses import curses
import datetime import datetime
import typing as T from typing import Any
from pqcli.lingo import format_timespan from pqcli.lingo import format_timespan
from pqcli.ui.curses.colors import COLOR_PROGRESSBAR, has_colors from pqcli.ui.curses.colors import COLOR_PROGRESSBAR, has_colors
@@ -9,7 +9,7 @@ from pqcli.ui.curses.widgets.base import WindowWrapper
class ProgressBar(WindowWrapper): class ProgressBar(WindowWrapper):
def __init__( def __init__(
self, parent: T.Any, h: int, w: int, y: int, x: int, show_time: bool self, parent: Any, h: int, w: int, y: int, x: int, show_time: bool
) -> None: ) -> None:
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
@@ -17,10 +17,10 @@ class ProgressBar(WindowWrapper):
self._max_pos = 1.0 self._max_pos = 1.0
self._show_time = show_time self._show_time = show_time
self._last_tick: T.Optional[T.Tuple[datetime.datetime, float]] = None self._last_tick: tuple[datetime.datetime, float] | None = None
@property @property
def time_left(self) -> T.Optional[datetime.timedelta]: def time_left(self) -> datetime.timedelta | None:
if self._last_tick is None: if self._last_tick is None:
return None return None
time_then, pos_then = self._last_tick time_then, pos_then = self._last_tick
+4 -4
View File
@@ -1,5 +1,5 @@
import curses import curses
import typing as T from typing import Any
from pqcli.ui.curses.colors import ( from pqcli.ui.curses.colors import (
COLOR_SCROLLBAR_THUMB, COLOR_SCROLLBAR_THUMB,
@@ -11,11 +11,11 @@ from .base import WindowWrapper
class Scrollable(WindowWrapper): class Scrollable(WindowWrapper):
def __init__(self, parent: T.Any, h: int, w: int, y: int, x: int) -> None: def __init__(self, parent: Any, h: int, w: int, y: int, x: int) -> None:
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
self._scroll_pos = 0 self._scroll_pos = 0
self._pad: T.Optional[T.Any] = curses.newpad(1, 1) self._pad: Any | None = curses.newpad(1, 1)
self._items: T.List[T.Any] = [] self._items: list[Any] = []
def clear(self) -> None: def clear(self) -> None:
self._items = [] self._items = []
+1 -1
View File
@@ -46,7 +46,7 @@ dev = ["prek>=0.2", "pytest>=8.0"]
line-length = 79 line-length = 79
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "W", "I"] select = ["E", "F", "W", "I", "UP"]
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
known-first-party = ["pqcli"] known-first-party = ["pqcli"]
+2 -3
View File
@@ -1,5 +1,4 @@
import datetime import datetime
import typing as T
from pathlib import Path from pathlib import Path
from pqcli import random from pqcli import random
@@ -20,7 +19,7 @@ STATS = {
} }
def run_trace(seed: str, ticks: int = 3000) -> T.List[str]: def run_trace(seed: str, ticks: int = 3000) -> list[str]:
"""Play a deterministic game and return everything that happened.""" """Play a deterministic game and return everything that happened."""
data = load_game_data() data = load_game_data()
player = Player( player = Player(
@@ -34,7 +33,7 @@ def run_trace(seed: str, ticks: int = 3000) -> T.List[str]:
random.seed(seed) random.seed(seed)
simulation = Simulation(player, data) simulation = Simulation(player, data)
lines: T.List[str] = [] lines: list[str] = []
player.connect( player.connect(
"new_task", "new_task",
lambda: lines.append(f"task: {player.task.description}"), lambda: lines.append(f"task: {player.task.description}"),
+3 -3
View File
@@ -1,8 +1,8 @@
import datetime import datetime
import json import json
import pickle import pickle
import typing as T
from pathlib import Path from pathlib import Path
from typing import Any
import pytest import pytest
@@ -18,7 +18,7 @@ from pqcli.game_data import (
from pqcli.mechanic import Player, Stats, monster_task from pqcli.mechanic import Player, Stats, monster_task
def write(directory: Path, name: str, value: T.Any) -> Path: def write(directory: Path, name: str, value: Any) -> Path:
path = directory / f"{name}.json" path = directory / f"{name}.json"
path.write_text(json.dumps(value), encoding="utf-8") path.write_text(json.dumps(value), encoding="utf-8")
return path return path
@@ -98,7 +98,7 @@ def test_unknown_file_warns(
], ],
) )
def test_invalid_override_raises( def test_invalid_override_raises(
tmp_path: Path, name: str, value: T.Any, message: str tmp_path: Path, name: str, value: Any, message: str
) -> None: ) -> None:
write(tmp_path, name, value) write(tmp_path, name, value)
with pytest.raises(DataError) as excinfo: with pytest.raises(DataError) as excinfo: