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.
- island_battles_python-0.1.0/PKG-INFO +8 -0
- island_battles_python-0.1.0/island_battles_client/__init__.py +89 -0
- island_battles_python-0.1.0/island_battles_client/_entities.py +107 -0
- island_battles_python-0.1.0/island_battles_client/_generated.py +870 -0
- island_battles_python-0.1.0/island_battles_client/bot.py +189 -0
- island_battles_python-0.1.0/island_battles_client/cli.py +539 -0
- island_battles_python-0.1.0/island_battles_client/client.py +763 -0
- island_battles_python-0.1.0/island_battles_client/errors.py +92 -0
- island_battles_python-0.1.0/island_battles_client/fixture.py +88 -0
- island_battles_python-0.1.0/island_battles_client/game_log.py +346 -0
- island_battles_python-0.1.0/island_battles_client/models.py +33 -0
- island_battles_python-0.1.0/pyproject.toml +47 -0
- island_battles_python-0.1.0/pyproject.toml.orig +49 -0
|
@@ -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
|