island-battles-python 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.3
2
+ Name: island-battles-python
3
+ Version: 0.1.0
4
+ Summary: Async player client for Island Battles generation turns
5
+ Requires-Dist: centrifuge-python==0.6.0
6
+ Requires-Dist: httpx>=0.27,<1
7
+ Requires-Dist: pydantic>=2.12,<3
8
+ Requires-Python: >=3.14
@@ -0,0 +1,89 @@
1
+ """Async Island Battles runtime and its player-facing interfaces."""
2
+
3
+ from ._generated import (
4
+ BotTurn,
5
+ BotTurnResult,
6
+ GenerationLogLine,
7
+ PublicGenerationCommand,
8
+ )
9
+ from .bot import (
10
+ MEMORY_LIMIT_BYTES,
11
+ Play,
12
+ log_line,
13
+ logger,
14
+ next_upgrade_cost,
15
+ send_soldier_group,
16
+ turn_result,
17
+ upgrade_castle,
18
+ )
19
+ from .client import IslandBattlesClient
20
+ from .errors import (
21
+ BotMemoryTooLargeError,
22
+ CastleNotFoundError,
23
+ CastleNotOwnedError,
24
+ GameAuthenticationError,
25
+ GameClientClosedError,
26
+ GameCommandError,
27
+ GameEndedError,
28
+ GameError,
29
+ GameProtocolError,
30
+ GameTransportError,
31
+ InsufficientSoldiersError,
32
+ InvalidBotResultError,
33
+ UpgradeMaxLevelError,
34
+ )
35
+ from .models import (
36
+ NEUTRAL_TEAM_COLOR,
37
+ BoardEntity,
38
+ Castle,
39
+ Clash,
40
+ ClashParticipant,
41
+ GameOutcome,
42
+ Generation,
43
+ SoldierGroup,
44
+ TeamType,
45
+ Upgrade,
46
+ UpgradeLevel,
47
+ UpgradeType,
48
+ )
49
+
50
+ __all__ = [
51
+ "MEMORY_LIMIT_BYTES",
52
+ "NEUTRAL_TEAM_COLOR",
53
+ "BoardEntity",
54
+ "BotMemoryTooLargeError",
55
+ "BotTurn",
56
+ "BotTurnResult",
57
+ "Castle",
58
+ "CastleNotFoundError",
59
+ "CastleNotOwnedError",
60
+ "Clash",
61
+ "ClashParticipant",
62
+ "GameAuthenticationError",
63
+ "GameClientClosedError",
64
+ "GameCommandError",
65
+ "GameEndedError",
66
+ "GameError",
67
+ "GameOutcome",
68
+ "GameProtocolError",
69
+ "GameTransportError",
70
+ "Generation",
71
+ "GenerationLogLine",
72
+ "InsufficientSoldiersError",
73
+ "InvalidBotResultError",
74
+ "IslandBattlesClient",
75
+ "Play",
76
+ "PublicGenerationCommand",
77
+ "SoldierGroup",
78
+ "TeamType",
79
+ "Upgrade",
80
+ "UpgradeLevel",
81
+ "UpgradeMaxLevelError",
82
+ "UpgradeType",
83
+ "log_line",
84
+ "logger",
85
+ "next_upgrade_cost",
86
+ "send_soldier_group",
87
+ "turn_result",
88
+ "upgrade_castle",
89
+ ]
@@ -0,0 +1,107 @@
1
+ """Behavior for the generated models; their fields come from the OpenAPI document.
2
+
3
+ ``scripts/generate.py`` makes every generated model extend ``GameModel`` and maps
4
+ castles, soldier groups and generations onto the classes below. Keep this module
5
+ free of fields so the schema stays the only source of wire data.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Sequence
11
+ from enum import StrEnum
12
+ from math import hypot
13
+ from typing import TYPE_CHECKING, Any, Final, Self
14
+
15
+ from pydantic import (
16
+ BaseModel,
17
+ ConfigDict,
18
+ PrivateAttr,
19
+ field_validator,
20
+ model_validator,
21
+ )
22
+
23
+ from .errors import GameError
24
+
25
+ if TYPE_CHECKING:
26
+ from ._generated import Castle, SoldierGroup
27
+
28
+ NEUTRAL_TEAM_COLOR: Final[int] = 0
29
+ """Team color for castles and soldiers that belong to no player team."""
30
+
31
+
32
+ class TeamType(StrEnum):
33
+ """Another entity's allegiance relative to this entity."""
34
+
35
+ friendly = "friendly"
36
+ neutral = "neutral"
37
+ enemy = "enemy"
38
+
39
+
40
+ class GameModel(BaseModel):
41
+ """Immutable snapshot; nested sequences are stored as tuples."""
42
+
43
+ model_config = ConfigDict(frozen=True, allow_inf_nan=False)
44
+
45
+ @field_validator("*", mode="after")
46
+ @classmethod
47
+ def _freeze_sequences(cls, value: Any) -> Any:
48
+ return tuple(value) if isinstance(value, list) else value
49
+
50
+
51
+ class BoardEntity(GameModel):
52
+ """A positioned team entity, shared by castles and soldier groups."""
53
+
54
+ if TYPE_CHECKING:
55
+ team_color: int
56
+ x: float
57
+ y: float
58
+
59
+ def distance(self, other: BoardEntity) -> float:
60
+ """Return straight-line distance in map units."""
61
+ return hypot(other.x - self.x, other.y - self.y)
62
+
63
+ def compare_team(self, other: BoardEntity) -> TeamType:
64
+ """Classify other; a neutral target is neutral even when self is neutral."""
65
+ if other.team_color == NEUTRAL_TEAM_COLOR:
66
+ return TeamType.neutral
67
+ if self.team_color == other.team_color:
68
+ return TeamType.friendly
69
+ return TeamType.enemy
70
+
71
+
72
+ class SoldierGroupBase(BoardEntity):
73
+ """Links a group to its destination when its generation is validated."""
74
+
75
+ _destination_castle: Castle | None = PrivateAttr(default=None)
76
+
77
+ if TYPE_CHECKING:
78
+ destination_castle_id: int
79
+
80
+ @property
81
+ def destination_castle(self) -> Castle:
82
+ """The castle this group is marching to, from the same generation."""
83
+ if self._destination_castle is None:
84
+ raise GameError("soldier group was not loaded as part of a generation")
85
+ return self._destination_castle
86
+
87
+ def distance_left(self) -> float:
88
+ """Return map units to the destination, not travel time or generation count."""
89
+ return self.distance(self.destination_castle)
90
+
91
+
92
+ class GenerationBase(GameModel):
93
+ """Rejects groups whose destination castle is not in the same snapshot."""
94
+
95
+ if TYPE_CHECKING:
96
+ castles: Sequence[Castle]
97
+ soldier_groups: Sequence[SoldierGroup]
98
+
99
+ @model_validator(mode="after")
100
+ def _link_destinations(self) -> Self:
101
+ castles = {castle.id: castle for castle in self.castles}
102
+ for group in self.soldier_groups:
103
+ destination = castles.get(group.destination_castle_id)
104
+ if destination is None:
105
+ raise ValueError("soldier group destination castle is missing")
106
+ group._destination_castle = destination
107
+ return self