fcode 2.2.0__cp312-cp312-macosx_11_0_arm64.whl
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.
- fcode/__init__.py +3 -0
- fcode/_types.py +655 -0
- fcode/api.py +146 -0
- fcode/auth.py +49 -0
- fcode/cli.py +127 -0
- fcode/commands/__init__.py +0 -0
- fcode/commands/init.py +29 -0
- fcode/commands/ladder.py +74 -0
- fcode/commands/login.py +135 -0
- fcode/commands/logout.py +21 -0
- fcode/commands/map_editor.py +91 -0
- fcode/commands/match_detail.py +173 -0
- fcode/commands/match_group.py +163 -0
- fcode/commands/matches.py +127 -0
- fcode/commands/run.py +186 -0
- fcode/commands/starter.py +119 -0
- fcode/commands/status.py +112 -0
- fcode/commands/submission.py +260 -0
- fcode/commands/team_group.py +73 -0
- fcode/commands/teams.py +73 -0
- fcode/commands/test.py +51 -0
- fcode/commands/test_matches.py +59 -0
- fcode/commands/test_run.py +125 -0
- fcode/commands/watch.py +123 -0
- fcode/compat.py +36 -0
- fcode/config.py +47 -0
- fcode/data/__init__.py +0 -0
- fcode/data/docs/spec.md +290 -0
- fcode/data/maps/__init__.py +0 -0
- fcode/data/maps/atoll.map26 +0 -0
- fcode/data/maps/aurora.map26 +0 -0
- fcode/data/maps/crossfire.map26 +0 -0
- fcode/data/maps/duel.map26 +0 -0
- fcode/data/maps/fjord.map26 +0 -0
- fcode/data/maps/hive.map26 +0 -0
- fcode/data/maps/longship.map26 +0 -0
- fcode/data/maps/pinch.map26 +0 -0
- fcode/data/maps/quarry.map26 +0 -0
- fcode/data/maps/runestone.map26 +0 -0
- fcode/data/maps/skerry.map26 +0 -0
- fcode/data/maps/sprint.map26 +0 -0
- fcode/data/maps/strait.map26 +0 -0
- fcode/data/maps/twins.map26 +0 -0
- fcode/data/maps/vault.map26 +0 -0
- fcode/data/starter_bot.py +452 -0
- fcode/data/visualiser/.directory +9 -0
- fcode/data/visualiser/Builderbot_med.png +0 -0
- fcode/data/visualiser/armoured_conveyor_gold.png +0 -0
- fcode/data/visualiser/armoured_conveyor_silver.png +0 -0
- fcode/data/visualiser/assets/app-mehzAGkK.css +1 -0
- fcode/data/visualiser/assets/app-ngkJrQ_K.js +86 -0
- fcode/data/visualiser/assets/main-Og2XCMt5.js +1 -0
- fcode/data/visualiser/assets/map-editor-B2YdQGnN.js +1 -0
- fcode/data/visualiser/axionite_ore.png +0 -0
- fcode/data/visualiser/axionite_processed.png +0 -0
- fcode/data/visualiser/axionite_raw.png +0 -0
- fcode/data/visualiser/barrier_gold.png +0 -0
- fcode/data/visualiser/barrier_silver.png +0 -0
- fcode/data/visualiser/base_gold.png +0 -0
- fcode/data/visualiser/base_silver.png +0 -0
- fcode/data/visualiser/battlecodetitle-orange.webp +0 -0
- fcode/data/visualiser/bg-light.png +0 -0
- fcode/data/visualiser/bg.png +0 -0
- fcode/data/visualiser/breach_e_gold.png +0 -0
- fcode/data/visualiser/breach_e_silver.png +0 -0
- fcode/data/visualiser/breach_n_gold.png +0 -0
- fcode/data/visualiser/breach_n_silver.png +0 -0
- fcode/data/visualiser/breach_ne_gold.png +0 -0
- fcode/data/visualiser/breach_ne_silver.png +0 -0
- fcode/data/visualiser/breach_nw_gold.png +0 -0
- fcode/data/visualiser/breach_nw_silver.png +0 -0
- fcode/data/visualiser/breach_s_gold.png +0 -0
- fcode/data/visualiser/breach_s_silver.png +0 -0
- fcode/data/visualiser/breach_se_gold.png +0 -0
- fcode/data/visualiser/breach_se_silver.png +0 -0
- fcode/data/visualiser/breach_sw_gold.png +0 -0
- fcode/data/visualiser/breach_sw_silver.png +0 -0
- fcode/data/visualiser/bridge_gold.png +0 -0
- fcode/data/visualiser/bridge_silver.png +0 -0
- fcode/data/visualiser/bridge_stand_gold.png +0 -0
- fcode/data/visualiser/bridge_stand_silver.png +0 -0
- fcode/data/visualiser/builderbot_back_gold.png +0 -0
- fcode/data/visualiser/builderbot_back_silver.png +0 -0
- fcode/data/visualiser/builderbot_front_gold.png +0 -0
- fcode/data/visualiser/builderbot_front_silver.png +0 -0
- fcode/data/visualiser/builderbot_side_gold.png +0 -0
- fcode/data/visualiser/builderbot_side_silver.png +0 -0
- fcode/data/visualiser/conveyor_gold.png +0 -0
- fcode/data/visualiser/conveyor_silver.png +0 -0
- fcode/data/visualiser/dimetric/Dimetric_Tile_Cube.png +0 -0
- fcode/data/visualiser/dimetric/Dimetric_Tile_Floor_Footprint.png +0 -0
- fcode/data/visualiser/dimetric/barrier_team1.png +0 -0
- fcode/data/visualiser/dimetric/barrier_team2.png +0 -0
- fcode/data/visualiser/dimetric/base_team1.png +0 -0
- fcode/data/visualiser/dimetric/base_team2.png +0 -0
- fcode/data/visualiser/dimetric/bg.png +0 -0
- fcode/data/visualiser/dimetric/bridge_stand_team1.png +0 -0
- fcode/data/visualiser/dimetric/bridge_stand_team2.png +0 -0
- fcode/data/visualiser/dimetric/bridge_team1.png +0 -0
- fcode/data/visualiser/dimetric/bridge_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_e_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_e_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_n_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_n_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_ne_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_ne_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_nw_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_nw_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_s_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_s_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_se_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_se_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_sw_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_sw_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_w_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_w_team2.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_ne_team1.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_ne_team2.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_nw_team1.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_nw_team2.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_se_team1.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_se_team2.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_sw_team1.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_sw_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_e_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_e_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_n_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_n_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_ne_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_ne_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_nw_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_nw_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_s_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_s_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_se_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_se_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_sw_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_sw_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_w_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_w_team2.png +0 -0
- fcode/data/visualiser/dimetric/harvester_team1.png +0 -0
- fcode/data/visualiser/dimetric/harvester_team2.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team1_ne.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team1_nw.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team1_se.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team1_sw.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team2_ne.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team2_nw.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team2_se.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team2_sw.png +0 -0
- fcode/data/visualiser/dimetric/natural_wall.png +0 -0
- fcode/data/visualiser/dimetric/natural_wall_ne.png +0 -0
- fcode/data/visualiser/dimetric/natural_wall_nw.png +0 -0
- fcode/data/visualiser/dimetric/natural_wall_se.png +0 -0
- fcode/data/visualiser/dimetric/natural_wall_sw.png +0 -0
- fcode/data/visualiser/dimetric/road_team1.png +0 -0
- fcode/data/visualiser/dimetric/road_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_e_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_e_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_n_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_n_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_ne_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_ne_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_nw_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_nw_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_s_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_s_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_se_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_se_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_sw_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_sw_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_w_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_w_team2.png +0 -0
- fcode/data/visualiser/dimetric/splitter_ne_team1.png +0 -0
- fcode/data/visualiser/dimetric/splitter_ne_team2.png +0 -0
- fcode/data/visualiser/dimetric/splitter_nw_team1.png +0 -0
- fcode/data/visualiser/dimetric/splitter_nw_team2.png +0 -0
- fcode/data/visualiser/dimetric/splitter_se_team1.png +0 -0
- fcode/data/visualiser/dimetric/splitter_se_team2.png +0 -0
- fcode/data/visualiser/dimetric/splitter_sw_team1.png +0 -0
- fcode/data/visualiser/dimetric/splitter_sw_team2.png +0 -0
- fcode/data/visualiser/dimetric/titanium.png +0 -0
- fcode/data/visualiser/dimetric/titanium_ore.png +0 -0
- fcode/data/visualiser/fonts/HKGrotesk-Regular.otf +0 -0
- fcode/data/visualiser/foundry_gold.png +0 -0
- fcode/data/visualiser/foundry_silver.png +0 -0
- fcode/data/visualiser/gunner_n_gold.png +0 -0
- fcode/data/visualiser/gunner_n_silver.png +0 -0
- fcode/data/visualiser/gunner_ne_gold.png +0 -0
- fcode/data/visualiser/gunner_ne_silver.png +0 -0
- fcode/data/visualiser/gunner_nw_gold.png +0 -0
- fcode/data/visualiser/gunner_nw_silver.png +0 -0
- fcode/data/visualiser/gunner_s_gold.png +0 -0
- fcode/data/visualiser/gunner_s_silver.png +0 -0
- fcode/data/visualiser/gunner_se_gold.png +0 -0
- fcode/data/visualiser/gunner_se_silver.png +0 -0
- fcode/data/visualiser/gunner_sw_gold.png +0 -0
- fcode/data/visualiser/gunner_sw_silver.png +0 -0
- fcode/data/visualiser/gunner_w_gold.png +0 -0
- fcode/data/visualiser/gunner_w_silver.png +0 -0
- fcode/data/visualiser/harvester_gold.png +0 -0
- fcode/data/visualiser/harvester_silver.png +0 -0
- fcode/data/visualiser/index.html +20 -0
- fcode/data/visualiser/launcher_gold.png +0 -0
- fcode/data/visualiser/launcher_silver.png +0 -0
- fcode/data/visualiser/map-editor.html +20 -0
- fcode/data/visualiser/marker_gold.png +0 -0
- fcode/data/visualiser/marker_silver.png +0 -0
- fcode/data/visualiser/natural_wall.jpg +0 -0
- fcode/data/visualiser/road_gold.png +0 -0
- fcode/data/visualiser/road_silver.png +0 -0
- fcode/data/visualiser/sentinel_e_gold.png +0 -0
- fcode/data/visualiser/sentinel_e_silver.png +0 -0
- fcode/data/visualiser/sentinel_n_gold.png +0 -0
- fcode/data/visualiser/sentinel_n_silver.png +0 -0
- fcode/data/visualiser/sentinel_ne_gold.png +0 -0
- fcode/data/visualiser/sentinel_ne_silver.png +0 -0
- fcode/data/visualiser/sentinel_nw_gold.png +0 -0
- fcode/data/visualiser/sentinel_nw_silver.png +0 -0
- fcode/data/visualiser/sentinel_s_gold.png +0 -0
- fcode/data/visualiser/sentinel_s_silver.png +0 -0
- fcode/data/visualiser/sentinel_se_gold.png +0 -0
- fcode/data/visualiser/sentinel_se_silver.png +0 -0
- fcode/data/visualiser/sentinel_sw_gold.png +0 -0
- fcode/data/visualiser/sentinel_sw_silver.png +0 -0
- fcode/data/visualiser/splitter.png +0 -0
- fcode/data/visualiser/splitter_e_gold.png +0 -0
- fcode/data/visualiser/splitter_e_silver.png +0 -0
- fcode/data/visualiser/splitter_n_gold.png +0 -0
- fcode/data/visualiser/splitter_n_silver.png +0 -0
- fcode/data/visualiser/splitter_s_gold.png +0 -0
- fcode/data/visualiser/splitter_s_silver.png +0 -0
- fcode/data/visualiser/splitter_w_gold.png +0 -0
- fcode/data/visualiser/splitter_w_silver.png +0 -0
- fcode/data/visualiser/template02.png +0 -0
- fcode/data/visualiser/titanium.png +0 -0
- fcode/data/visualiser/titanium_ore.png +0 -0
- fcode/fcode_engine.cpython-312-darwin.so +0 -0
- fcode/py.typed +0 -0
- fcode/version_check.py +72 -0
- fcode-2.2.0.dist-info/METADATA +7 -0
- fcode-2.2.0.dist-info/RECORD +245 -0
- fcode-2.2.0.dist-info/WHEEL +4 -0
- fcode-2.2.0.dist-info/entry_points.txt +2 -0
- fcode-2.2.0.dist-info/sboms/fcode_engine.cyclonedx.json +2019 -0
fcode/__init__.py
ADDED
fcode/_types.py
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
"""Common types for the Titan game."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GameError(Exception):
|
|
10
|
+
"""Raised when a player issues an invalid action."""
|
|
11
|
+
|
|
12
|
+
__slots__ = ()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Team(Enum):
|
|
16
|
+
__slots__ = ()
|
|
17
|
+
|
|
18
|
+
A = "a"
|
|
19
|
+
B = "b"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ResourceType(Enum):
|
|
23
|
+
__slots__ = ()
|
|
24
|
+
|
|
25
|
+
TITANIUM = "titanium"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EntityType(Enum):
|
|
29
|
+
__slots__ = ()
|
|
30
|
+
|
|
31
|
+
BUILDER_BOT = "builder_bot"
|
|
32
|
+
CORE = "core"
|
|
33
|
+
GUNNER = "gunner"
|
|
34
|
+
SENTINEL = "sentinel"
|
|
35
|
+
LAUNCHER = "launcher"
|
|
36
|
+
CONVEYOR = "conveyor"
|
|
37
|
+
SPLITTER = "splitter"
|
|
38
|
+
HARVESTER = "harvester"
|
|
39
|
+
BARRIER = "barrier"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GameConstants:
|
|
43
|
+
__slots__ = ()
|
|
44
|
+
|
|
45
|
+
MAX_TURNS = 1000
|
|
46
|
+
STACK_SIZE = 10
|
|
47
|
+
STARTING_TITANIUM = 500
|
|
48
|
+
MAX_TEAM_UNITS = 50
|
|
49
|
+
PASSIVE_TITANIUM_AMOUNT = 10
|
|
50
|
+
PASSIVE_TITANIUM_INTERVAL = 4
|
|
51
|
+
|
|
52
|
+
ACTION_RADIUS_SQ = 2
|
|
53
|
+
CORE_SPAWNING_RADIUS_SQ = 2
|
|
54
|
+
CORE_ACTION_RADIUS_SQ = 8
|
|
55
|
+
|
|
56
|
+
CORE_VISION_RADIUS_SQ = 36
|
|
57
|
+
BUILDER_BOT_VISION_RADIUS_SQ = 20
|
|
58
|
+
GUNNER_VISION_RADIUS_SQ = 13
|
|
59
|
+
SENTINEL_VISION_RADIUS_SQ = 32
|
|
60
|
+
LAUNCHER_VISION_RADIUS_SQ = 26
|
|
61
|
+
|
|
62
|
+
CONVEYOR_BASE_COST = 3
|
|
63
|
+
SPLITTER_BASE_COST = 6
|
|
64
|
+
HARVESTER_BASE_COST = 20
|
|
65
|
+
BARRIER_BASE_COST = 3
|
|
66
|
+
GUNNER_BASE_COST = 10
|
|
67
|
+
SENTINEL_BASE_COST = 30
|
|
68
|
+
LAUNCHER_BASE_COST = 20
|
|
69
|
+
BUILDER_BOT_BASE_COST = 30
|
|
70
|
+
GUNNER_ROTATE_COST = 10
|
|
71
|
+
GUNNER_ROTATE_COOLDOWN = 1
|
|
72
|
+
|
|
73
|
+
CONVEYOR_MAX_HP = 20
|
|
74
|
+
SPLITTER_MAX_HP = 20
|
|
75
|
+
HARVESTER_MAX_HP = 30
|
|
76
|
+
BARRIER_MAX_HP = 30
|
|
77
|
+
|
|
78
|
+
STORE_SIZE = 16
|
|
79
|
+
|
|
80
|
+
BUILDER_BOT_MAX_HP = 40
|
|
81
|
+
CORE_MAX_HP = 500
|
|
82
|
+
GUNNER_MAX_HP = 40
|
|
83
|
+
SENTINEL_MAX_HP = 30
|
|
84
|
+
LAUNCHER_MAX_HP = 30
|
|
85
|
+
|
|
86
|
+
BUILDER_BOT_SELF_DESTRUCT_DAMAGE = 0
|
|
87
|
+
BUILDER_BOT_ATTACK_DAMAGE = 2
|
|
88
|
+
BUILDER_BOT_ATTACK_COST = 2
|
|
89
|
+
BUILDER_BOT_HEAL_COST = 1
|
|
90
|
+
HEAL_AMOUNT = 4
|
|
91
|
+
|
|
92
|
+
GUNNER_DAMAGE = 10
|
|
93
|
+
GUNNER_FIRE_COOLDOWN = 1
|
|
94
|
+
GUNNER_AMMO_COST = 2
|
|
95
|
+
|
|
96
|
+
SENTINEL_DAMAGE = 18
|
|
97
|
+
SENTINEL_FIRE_COOLDOWN = 3
|
|
98
|
+
SENTINEL_AMMO_COST = 10
|
|
99
|
+
|
|
100
|
+
LAUNCHER_FIRE_COOLDOWN = 1
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Environment(Enum):
|
|
104
|
+
__slots__ = ()
|
|
105
|
+
|
|
106
|
+
EMPTY = "empty"
|
|
107
|
+
WALL = "wall"
|
|
108
|
+
ORE_TITANIUM = "ore_titanium"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Direction(Enum):
|
|
112
|
+
__slots__ = ()
|
|
113
|
+
|
|
114
|
+
NORTH = "north"
|
|
115
|
+
NORTHEAST = "northeast"
|
|
116
|
+
EAST = "east"
|
|
117
|
+
SOUTHEAST = "southeast"
|
|
118
|
+
SOUTH = "south"
|
|
119
|
+
SOUTHWEST = "southwest"
|
|
120
|
+
WEST = "west"
|
|
121
|
+
NORTHWEST = "northwest"
|
|
122
|
+
CENTRE = "centre"
|
|
123
|
+
|
|
124
|
+
def delta(self) -> tuple[int, int]:
|
|
125
|
+
"""Return the (dx, dy) step for this direction."""
|
|
126
|
+
return {
|
|
127
|
+
Direction.NORTH: (0, -1),
|
|
128
|
+
Direction.NORTHEAST: (1, -1),
|
|
129
|
+
Direction.EAST: (1, 0),
|
|
130
|
+
Direction.SOUTHEAST: (1, 1),
|
|
131
|
+
Direction.SOUTH: (0, 1),
|
|
132
|
+
Direction.SOUTHWEST: (-1, 1),
|
|
133
|
+
Direction.WEST: (-1, 0),
|
|
134
|
+
Direction.NORTHWEST: (-1, -1),
|
|
135
|
+
Direction.CENTRE: (0, 0),
|
|
136
|
+
}[self]
|
|
137
|
+
|
|
138
|
+
def rotate_left(self) -> Direction:
|
|
139
|
+
"""Return the direction rotated 45 degrees counterclockwise."""
|
|
140
|
+
return {
|
|
141
|
+
Direction.NORTH: Direction.NORTHWEST,
|
|
142
|
+
Direction.NORTHEAST: Direction.NORTH,
|
|
143
|
+
Direction.EAST: Direction.NORTHEAST,
|
|
144
|
+
Direction.SOUTHEAST: Direction.EAST,
|
|
145
|
+
Direction.SOUTH: Direction.SOUTHEAST,
|
|
146
|
+
Direction.SOUTHWEST: Direction.SOUTH,
|
|
147
|
+
Direction.WEST: Direction.SOUTHWEST,
|
|
148
|
+
Direction.NORTHWEST: Direction.WEST,
|
|
149
|
+
Direction.CENTRE: Direction.CENTRE,
|
|
150
|
+
}[self]
|
|
151
|
+
|
|
152
|
+
def rotate_right(self) -> Direction:
|
|
153
|
+
"""Return the direction rotated 45 degrees clockwise."""
|
|
154
|
+
return {
|
|
155
|
+
Direction.NORTH: Direction.NORTHEAST,
|
|
156
|
+
Direction.NORTHEAST: Direction.EAST,
|
|
157
|
+
Direction.EAST: Direction.SOUTHEAST,
|
|
158
|
+
Direction.SOUTHEAST: Direction.SOUTH,
|
|
159
|
+
Direction.SOUTH: Direction.SOUTHWEST,
|
|
160
|
+
Direction.SOUTHWEST: Direction.WEST,
|
|
161
|
+
Direction.WEST: Direction.NORTHWEST,
|
|
162
|
+
Direction.NORTHWEST: Direction.NORTH,
|
|
163
|
+
Direction.CENTRE: Direction.CENTRE,
|
|
164
|
+
}[self]
|
|
165
|
+
|
|
166
|
+
def opposite(self) -> Direction:
|
|
167
|
+
"""Return the opposite direction (180 degrees)."""
|
|
168
|
+
return {
|
|
169
|
+
Direction.NORTH: Direction.SOUTH,
|
|
170
|
+
Direction.NORTHEAST: Direction.SOUTHWEST,
|
|
171
|
+
Direction.EAST: Direction.WEST,
|
|
172
|
+
Direction.SOUTHEAST: Direction.NORTHWEST,
|
|
173
|
+
Direction.SOUTH: Direction.NORTH,
|
|
174
|
+
Direction.SOUTHWEST: Direction.NORTHEAST,
|
|
175
|
+
Direction.WEST: Direction.EAST,
|
|
176
|
+
Direction.NORTHWEST: Direction.SOUTHEAST,
|
|
177
|
+
Direction.CENTRE: Direction.CENTRE,
|
|
178
|
+
}[self]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Position(NamedTuple):
|
|
182
|
+
x: int
|
|
183
|
+
y: int
|
|
184
|
+
|
|
185
|
+
def add(self, d: Direction) -> Position:
|
|
186
|
+
"""Return a new position offset by the direction delta."""
|
|
187
|
+
dx, dy = d.delta()
|
|
188
|
+
return Position(self.x + dx, self.y + dy)
|
|
189
|
+
|
|
190
|
+
def distance_squared(self, other: Position) -> int:
|
|
191
|
+
"""Return squared distance to another position."""
|
|
192
|
+
dx = self.x - other.x
|
|
193
|
+
dy = self.y - other.y
|
|
194
|
+
return dx * dx + dy * dy
|
|
195
|
+
|
|
196
|
+
def direction_to(self, other: Position) -> Direction:
|
|
197
|
+
"""Return the closest 45-degree Direction approximation toward other."""
|
|
198
|
+
dx = other.x - self.x
|
|
199
|
+
dy = other.y - self.y
|
|
200
|
+
if dx == 0 and dy == 0:
|
|
201
|
+
return Direction.CENTRE
|
|
202
|
+
# atan2 gives angle in radians; map to one of 8 compass directions.
|
|
203
|
+
import math
|
|
204
|
+
|
|
205
|
+
# Use y-up convention for direction mapping: north is decreasing y.
|
|
206
|
+
angle = math.atan2(-dy, dx) # radians, x-east / y-north convention
|
|
207
|
+
# Snap to nearest 45-degree sector (each sector is pi/4 wide).
|
|
208
|
+
# Sectors: E=0, NE=1, N=2, NW=3, W=4, SW=5, S=6, SE=7
|
|
209
|
+
sector = int((angle + 2 * math.pi + math.pi / 8) / (math.pi / 4)) % 8
|
|
210
|
+
return [
|
|
211
|
+
Direction.EAST,
|
|
212
|
+
Direction.NORTHEAST,
|
|
213
|
+
Direction.NORTH,
|
|
214
|
+
Direction.NORTHWEST,
|
|
215
|
+
Direction.WEST,
|
|
216
|
+
Direction.SOUTHWEST,
|
|
217
|
+
Direction.SOUTH,
|
|
218
|
+
Direction.SOUTHEAST,
|
|
219
|
+
][sector]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# The real Controller is injected from Rust at runtime, overwriting this placeholder.
|
|
223
|
+
# Keep the TYPE_CHECKING stub in sync with engine/rust/src/bindings/controller.rs.
|
|
224
|
+
#
|
|
225
|
+
# IMPORTANT: The runtime placeholder must have NO methods. With SHARED_GIL
|
|
226
|
+
# subinterpreters, Python methods on a Controller class pollute the Rust
|
|
227
|
+
# Controller's method resolution, causing bot-defined methods with the same
|
|
228
|
+
# name (e.g. `move`) to be incorrectly bound to Controller instances in other
|
|
229
|
+
# subinterpreters. The full stub is only for type checkers.
|
|
230
|
+
#
|
|
231
|
+
# NOTE: We use `if not TYPE_CHECKING: ... else: ...` (not a top-level class
|
|
232
|
+
# followed by a TYPE_CHECKING override) because mypy treats a top-level
|
|
233
|
+
# redefinition as an error and sticks with the first (empty) class.
|
|
234
|
+
# The else branch must contain the stubs so they come last — PyCharm uses
|
|
235
|
+
# "last definition wins" rather than properly evaluating TYPE_CHECKING.
|
|
236
|
+
if not TYPE_CHECKING:
|
|
237
|
+
|
|
238
|
+
class Controller: # Placeholder — overwritten by Rust class before bot code runs.
|
|
239
|
+
pass
|
|
240
|
+
else:
|
|
241
|
+
|
|
242
|
+
class Controller:
|
|
243
|
+
# --- Info ---
|
|
244
|
+
|
|
245
|
+
def get_team(self, id: int | None = None) -> Team:
|
|
246
|
+
"""Return the team of the entity with the given id, or this unit if omitted."""
|
|
247
|
+
...
|
|
248
|
+
|
|
249
|
+
def get_position(self, id: int | None = None) -> Position:
|
|
250
|
+
"""Return the position of the entity with the given id, or this unit if omitted."""
|
|
251
|
+
...
|
|
252
|
+
|
|
253
|
+
def get_id(self) -> int:
|
|
254
|
+
"""Return this unit's entity id."""
|
|
255
|
+
...
|
|
256
|
+
|
|
257
|
+
def get_action_cooldown(self) -> int:
|
|
258
|
+
"""Return this unit's current action cooldown. Actions require cooldown == 0."""
|
|
259
|
+
...
|
|
260
|
+
|
|
261
|
+
def get_move_cooldown(self) -> int:
|
|
262
|
+
"""Return this unit's current move cooldown. Movement requires cooldown == 0."""
|
|
263
|
+
...
|
|
264
|
+
|
|
265
|
+
def get_ammo_amount(self) -> int:
|
|
266
|
+
"""Return the amount of ammo this turret currently holds."""
|
|
267
|
+
...
|
|
268
|
+
|
|
269
|
+
def get_ammo_type(self) -> ResourceType | None:
|
|
270
|
+
"""Return the resource type loaded as ammo, or None if empty."""
|
|
271
|
+
...
|
|
272
|
+
|
|
273
|
+
def get_vision_radius_sq(self, id: int | None = None) -> int:
|
|
274
|
+
"""Return the vision radius squared of the given unit, or this unit if omitted."""
|
|
275
|
+
...
|
|
276
|
+
|
|
277
|
+
def get_hp(self, id: int | None = None) -> int:
|
|
278
|
+
"""Return the current HP of the entity with the given id, or this unit if omitted."""
|
|
279
|
+
...
|
|
280
|
+
|
|
281
|
+
def get_max_hp(self, id: int | None = None) -> int:
|
|
282
|
+
"""Return the max HP of the entity with the given id, or this unit if omitted."""
|
|
283
|
+
...
|
|
284
|
+
|
|
285
|
+
def get_entity_type(self, id: int | None = None) -> EntityType:
|
|
286
|
+
"""Return the EntityType of the entity with the given id, or this unit if omitted."""
|
|
287
|
+
...
|
|
288
|
+
|
|
289
|
+
def get_direction(self, id: int | None = None) -> Direction:
|
|
290
|
+
"""Return the facing direction of a conveyor, splitter, or turret.
|
|
291
|
+
Raises GameError if the entity has no direction."""
|
|
292
|
+
...
|
|
293
|
+
|
|
294
|
+
def get_stored_resource(self, id: int | None = None) -> ResourceType | None:
|
|
295
|
+
"""Return the resource stored in a conveyor or splitter.
|
|
296
|
+
Returns None if empty. Raises GameError if the entity has no storage."""
|
|
297
|
+
...
|
|
298
|
+
|
|
299
|
+
def get_stored_resource_id(self, id: int | None = None) -> int | None:
|
|
300
|
+
"""Return the id of the resource stored in a conveyor or splitter.
|
|
301
|
+
Returns None if empty. Raises GameError if the entity has no storage."""
|
|
302
|
+
...
|
|
303
|
+
|
|
304
|
+
def get_tile_env(self, pos: Position) -> Environment:
|
|
305
|
+
"""Return the environment type (empty, wall, ore) of the tile at pos."""
|
|
306
|
+
...
|
|
307
|
+
|
|
308
|
+
def get_tile_building_id(self, pos: Position) -> int | None:
|
|
309
|
+
"""Return the id of the building on the tile at pos, or None if there is none."""
|
|
310
|
+
...
|
|
311
|
+
|
|
312
|
+
def get_tile_builder_bot_id(self, pos: Position) -> int | None:
|
|
313
|
+
"""Return the id of the builder bot on the tile at pos, or None if there is none."""
|
|
314
|
+
...
|
|
315
|
+
|
|
316
|
+
def is_tile_empty(self, pos: Position) -> bool:
|
|
317
|
+
"""Return True if the tile has no building and is not a wall."""
|
|
318
|
+
...
|
|
319
|
+
|
|
320
|
+
def is_tile_passable(self, pos: Position) -> bool:
|
|
321
|
+
"""Return True if a builder bot belonging to this team could stand on the tile
|
|
322
|
+
(i.e. no other builder bot is there, and the tile is not a wall and either has
|
|
323
|
+
no building on it or has a conveyor, splitter, or the allied core)."""
|
|
324
|
+
...
|
|
325
|
+
|
|
326
|
+
def is_in_vision(self, pos: Position) -> bool:
|
|
327
|
+
"""Return True if pos is within this unit's vision radius."""
|
|
328
|
+
...
|
|
329
|
+
|
|
330
|
+
def get_nearby_tiles(self, dist_sq: int | None = None) -> list[Position]:
|
|
331
|
+
"""Return all in-bounds tile positions within dist_sq of this unit (defaults to vision radius).
|
|
332
|
+
dist_sq must not exceed the vision radius."""
|
|
333
|
+
...
|
|
334
|
+
|
|
335
|
+
def get_nearby_entities(self, dist_sq: int | None = None) -> list[int]:
|
|
336
|
+
"""Return ids of all entities on tiles within dist_sq (defaults to vision radius)."""
|
|
337
|
+
...
|
|
338
|
+
|
|
339
|
+
def get_nearby_buildings(self, dist_sq: int | None = None) -> list[int]:
|
|
340
|
+
"""Return ids of all buildings within dist_sq (defaults to vision radius)."""
|
|
341
|
+
...
|
|
342
|
+
|
|
343
|
+
def get_nearby_units(self, dist_sq: int | None = None) -> list[int]:
|
|
344
|
+
"""Return ids of all units within dist_sq (defaults to vision radius)."""
|
|
345
|
+
...
|
|
346
|
+
|
|
347
|
+
def get_map_width(self) -> int:
|
|
348
|
+
"""Return the width of the map in tiles."""
|
|
349
|
+
...
|
|
350
|
+
|
|
351
|
+
def get_map_height(self) -> int:
|
|
352
|
+
"""Return the height of the map in tiles."""
|
|
353
|
+
...
|
|
354
|
+
|
|
355
|
+
def get_current_round(self) -> int:
|
|
356
|
+
"""Return the current round number (starts at 1)."""
|
|
357
|
+
...
|
|
358
|
+
|
|
359
|
+
def get_global_resources(self) -> int:
|
|
360
|
+
"""Return this team's titanium balance."""
|
|
361
|
+
...
|
|
362
|
+
|
|
363
|
+
def get_scale_percent(self) -> float:
|
|
364
|
+
"""Return this team's current cost scale as a percentage (100.0 = base cost; used in the scaling formula)."""
|
|
365
|
+
...
|
|
366
|
+
|
|
367
|
+
def get_cpu_time_elapsed(self) -> int:
|
|
368
|
+
"""Return the CPU time elapsed this turn in microseconds."""
|
|
369
|
+
...
|
|
370
|
+
|
|
371
|
+
# --- Cost getters ---
|
|
372
|
+
|
|
373
|
+
def get_conveyor_cost(self) -> int:
|
|
374
|
+
"""Return the current scaled cost to build a conveyor."""
|
|
375
|
+
...
|
|
376
|
+
|
|
377
|
+
def get_splitter_cost(self) -> int:
|
|
378
|
+
"""Return the current scaled cost to build a splitter."""
|
|
379
|
+
...
|
|
380
|
+
|
|
381
|
+
def get_harvester_cost(self) -> int:
|
|
382
|
+
"""Return the current scaled cost to build a harvester."""
|
|
383
|
+
...
|
|
384
|
+
|
|
385
|
+
def get_barrier_cost(self) -> int:
|
|
386
|
+
"""Return the current scaled cost to build a barrier."""
|
|
387
|
+
...
|
|
388
|
+
|
|
389
|
+
def get_gunner_cost(self) -> int:
|
|
390
|
+
"""Return the current scaled cost to build a gunner."""
|
|
391
|
+
...
|
|
392
|
+
|
|
393
|
+
def get_sentinel_cost(self) -> int:
|
|
394
|
+
"""Return the current scaled cost to build a sentinel."""
|
|
395
|
+
...
|
|
396
|
+
|
|
397
|
+
def get_launcher_cost(self) -> int:
|
|
398
|
+
"""Return the current scaled cost to build a launcher."""
|
|
399
|
+
...
|
|
400
|
+
|
|
401
|
+
def get_builder_bot_cost(self) -> int:
|
|
402
|
+
"""Return the current scaled cost to spawn a builder bot."""
|
|
403
|
+
...
|
|
404
|
+
|
|
405
|
+
def get_unit_count(self) -> int:
|
|
406
|
+
"""Return the number of living units currently on this unit's team, including the core."""
|
|
407
|
+
...
|
|
408
|
+
|
|
409
|
+
# --- Movement ---
|
|
410
|
+
|
|
411
|
+
def move(self, direction: Direction) -> None:
|
|
412
|
+
"""Move this builder bot one step in direction. Raises GameError if the move is not legal."""
|
|
413
|
+
...
|
|
414
|
+
|
|
415
|
+
def can_move(self, direction: Direction) -> bool:
|
|
416
|
+
"""Return True if this builder bot can move in direction this turn."""
|
|
417
|
+
...
|
|
418
|
+
|
|
419
|
+
# --- Building ---
|
|
420
|
+
|
|
421
|
+
def can_build_conveyor(self, position: Position, direction: Direction) -> bool:
|
|
422
|
+
"""Return True if a conveyor facing direction can be built at position."""
|
|
423
|
+
...
|
|
424
|
+
|
|
425
|
+
def can_build_splitter(self, position: Position, direction: Direction) -> bool:
|
|
426
|
+
"""Return True if a splitter facing direction can be built at position."""
|
|
427
|
+
...
|
|
428
|
+
|
|
429
|
+
def can_build_harvester(self, position: Position) -> bool:
|
|
430
|
+
"""Return True if a harvester can be built at position (must be an ore tile)."""
|
|
431
|
+
...
|
|
432
|
+
|
|
433
|
+
def can_build_barrier(self, position: Position) -> bool:
|
|
434
|
+
"""Return True if a barrier can be built at position."""
|
|
435
|
+
...
|
|
436
|
+
|
|
437
|
+
def can_build_gunner(self, position: Position, direction: Direction) -> bool:
|
|
438
|
+
"""Return True if a gunner facing direction can be built at position.
|
|
439
|
+
Respects the global unit cap."""
|
|
440
|
+
...
|
|
441
|
+
|
|
442
|
+
def can_build_sentinel(self, position: Position, direction: Direction) -> bool:
|
|
443
|
+
"""Return True if a sentinel facing direction can be built at position.
|
|
444
|
+
Respects the global unit cap."""
|
|
445
|
+
...
|
|
446
|
+
|
|
447
|
+
def can_build_launcher(self, position: Position) -> bool:
|
|
448
|
+
"""Return True if a launcher can be built at position.
|
|
449
|
+
Respects the global unit cap."""
|
|
450
|
+
...
|
|
451
|
+
|
|
452
|
+
def build_conveyor(self, position: Position, direction: Direction) -> int:
|
|
453
|
+
"""Build a conveyor facing direction at position. Raises GameError if not legal."""
|
|
454
|
+
...
|
|
455
|
+
|
|
456
|
+
def build_splitter(self, position: Position, direction: Direction) -> int:
|
|
457
|
+
"""Build a splitter facing direction at position. Raises GameError if not legal."""
|
|
458
|
+
...
|
|
459
|
+
|
|
460
|
+
def build_harvester(self, position: Position) -> int:
|
|
461
|
+
"""Build a harvester at position (must be an ore tile). Raises GameError if not legal."""
|
|
462
|
+
...
|
|
463
|
+
|
|
464
|
+
def build_barrier(self, position: Position) -> int:
|
|
465
|
+
"""Build a barrier at position. Raises GameError if not legal."""
|
|
466
|
+
...
|
|
467
|
+
|
|
468
|
+
def build_gunner(self, position: Position, direction: Direction) -> int:
|
|
469
|
+
"""Build a gunner facing direction at position. Raises GameError if not legal."""
|
|
470
|
+
...
|
|
471
|
+
|
|
472
|
+
def build_sentinel(self, position: Position, direction: Direction) -> int:
|
|
473
|
+
"""Build a sentinel facing direction at position. Raises GameError if not legal."""
|
|
474
|
+
...
|
|
475
|
+
|
|
476
|
+
def build_launcher(self, position: Position) -> int:
|
|
477
|
+
"""Build a launcher at position. Raises GameError if not legal."""
|
|
478
|
+
...
|
|
479
|
+
|
|
480
|
+
def can_build(
|
|
481
|
+
self,
|
|
482
|
+
entity_type: EntityType,
|
|
483
|
+
position: Position,
|
|
484
|
+
extra: Direction | Position | None = None,
|
|
485
|
+
) -> bool:
|
|
486
|
+
"""Return True if entity_type can be built at position.
|
|
487
|
+
For entity types that require a direction (conveyor, splitter,
|
|
488
|
+
gunner, sentinel), extra must be a Direction.
|
|
489
|
+
For harvester, barrier, launcher, extra is unused."""
|
|
490
|
+
...
|
|
491
|
+
|
|
492
|
+
def build(
|
|
493
|
+
self,
|
|
494
|
+
entity_type: EntityType,
|
|
495
|
+
position: Position,
|
|
496
|
+
extra: Direction | Position | None = None,
|
|
497
|
+
) -> int:
|
|
498
|
+
"""Build entity_type at position. Raises GameError if not legal.
|
|
499
|
+
For entity types that require a direction (conveyor, splitter,
|
|
500
|
+
gunner, sentinel), extra must be a Direction.
|
|
501
|
+
For harvester, barrier, launcher, extra is unused."""
|
|
502
|
+
...
|
|
503
|
+
|
|
504
|
+
# --- Healing ---
|
|
505
|
+
|
|
506
|
+
def heal(self, position: Position) -> None:
|
|
507
|
+
"""Heal all friendly entities on a tile within this builder bot's action radius by 4 HP.
|
|
508
|
+
If both a friendly builder bot and a friendly building are on the tile, both are healed.
|
|
509
|
+
Costs 1 titanium and one action cooldown. Raises GameError if not legal."""
|
|
510
|
+
...
|
|
511
|
+
|
|
512
|
+
def can_heal(self, position: Position) -> bool:
|
|
513
|
+
"""Return True if this builder bot can heal the tile at position this turn.
|
|
514
|
+
position must be within the builder bot's action radius.
|
|
515
|
+
Requires action cooldown == 0, enough titanium, and at least one damaged friendly entity
|
|
516
|
+
on the tile."""
|
|
517
|
+
...
|
|
518
|
+
|
|
519
|
+
# --- Destruction ---
|
|
520
|
+
|
|
521
|
+
def can_destroy(self, building_pos: Position) -> bool:
|
|
522
|
+
"""Return True if this builder bot can destroy the allied building at building_pos."""
|
|
523
|
+
...
|
|
524
|
+
|
|
525
|
+
def destroy(self, building_pos: Position) -> None:
|
|
526
|
+
"""Destroy the allied building at building_pos. Does not cost action cooldown.
|
|
527
|
+
Raises GameError if not legal."""
|
|
528
|
+
...
|
|
529
|
+
|
|
530
|
+
def self_destruct(self) -> None:
|
|
531
|
+
"""Destroy this unit. Builder bots no longer deal explosion damage when they self-destruct."""
|
|
532
|
+
...
|
|
533
|
+
|
|
534
|
+
def resign(self, message: str | None = None) -> None:
|
|
535
|
+
"""Forfeit the game immediately. Destroys this team's core, ending the game as a loss.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
message: Optional reason for resigning. Displayed in match results.
|
|
539
|
+
"""
|
|
540
|
+
...
|
|
541
|
+
|
|
542
|
+
# --- Communication Store ---
|
|
543
|
+
|
|
544
|
+
def read_store(self, index: int) -> int:
|
|
545
|
+
"""Return the u32 value at index in this team's global communication store.
|
|
546
|
+
Reads the snapshot from the start of this turn. index must be in 0..STORE_SIZE."""
|
|
547
|
+
...
|
|
548
|
+
|
|
549
|
+
def write_store(self, index: int, value: int) -> None:
|
|
550
|
+
"""Buffer a write to this team's communication store at index.
|
|
551
|
+
The write takes effect at the start of next turn and is visible to all units.
|
|
552
|
+
index must be in 0..STORE_SIZE; value is treated as u32 (0..2^32-1)."""
|
|
553
|
+
...
|
|
554
|
+
|
|
555
|
+
# --- Turrets ---
|
|
556
|
+
|
|
557
|
+
def can_fire(self, target: Position) -> bool:
|
|
558
|
+
"""Return True if this builder bot or turret can fire at target this turn.
|
|
559
|
+
Builder bots may only target their own tile and only damage the building on it.
|
|
560
|
+
For gunners, only empty tiles fail to block the firing line. Walls block the line but
|
|
561
|
+
are not targetable. Builder bots and buildings are both targetable and blocking.
|
|
562
|
+
Use can_launch() instead for launchers."""
|
|
563
|
+
...
|
|
564
|
+
|
|
565
|
+
def can_fire_from(
|
|
566
|
+
self,
|
|
567
|
+
position: Position,
|
|
568
|
+
direction: Direction,
|
|
569
|
+
turret_type: EntityType,
|
|
570
|
+
target: Position,
|
|
571
|
+
) -> bool:
|
|
572
|
+
"""Return True if a hypothetical turret at position facing direction could fire at target.
|
|
573
|
+
This uses the current map state for occupancy and walls, but ignores ammo and cooldown.
|
|
574
|
+
For gunners, the target tile must be occupied. Only empty tiles do not block the line;
|
|
575
|
+
walls, builder bots, and buildings do, with walls remaining untargetable.
|
|
576
|
+
For launchers this only checks raw throw range, and direction is ignored."""
|
|
577
|
+
...
|
|
578
|
+
|
|
579
|
+
def fire(self, target: Position) -> None:
|
|
580
|
+
"""Fire this builder bot or turret at target. Builder bots may only target their own tile.
|
|
581
|
+
For gunners, walls, builder bots, and buildings stop the firing line; walls themselves
|
|
582
|
+
are not valid targets.
|
|
583
|
+
Use launch() instead for launchers.
|
|
584
|
+
Raises GameError if not legal."""
|
|
585
|
+
...
|
|
586
|
+
|
|
587
|
+
def can_rotate(self, direction: Direction) -> bool:
|
|
588
|
+
"""Return True if this gunner can rotate to a different compass direction this turn.
|
|
589
|
+
Also checks that your team can afford the global titanium cost."""
|
|
590
|
+
...
|
|
591
|
+
|
|
592
|
+
def rotate(self, direction: Direction) -> None:
|
|
593
|
+
"""Rotate this gunner to a different compass direction.
|
|
594
|
+
Costs 10 titanium and sets action cooldown to 1.
|
|
595
|
+
Raises GameError if not legal."""
|
|
596
|
+
...
|
|
597
|
+
|
|
598
|
+
def get_gunner_target(self) -> Position | None:
|
|
599
|
+
"""Return the position of the closest targetable tile in the gunner's facing direction,
|
|
600
|
+
or None if nothing is in range. Empty tiles are skipped. Walls block the line without
|
|
601
|
+
being targetable. Only valid on gunners."""
|
|
602
|
+
...
|
|
603
|
+
|
|
604
|
+
def get_attackable_tiles(self) -> list[Position]:
|
|
605
|
+
"""Return all in-bounds tiles in this turret's raw attack pattern.
|
|
606
|
+
This ignores ammo, cooldown, occupancy, and other target-specific legality checks.
|
|
607
|
+
For gunners this includes the full firing line within range, even behind walls.
|
|
608
|
+
Use get_gunner_target(), can_fire(), or can_launch() for actual legal targets.
|
|
609
|
+
Raises GameError if this unit is not a turret."""
|
|
610
|
+
...
|
|
611
|
+
|
|
612
|
+
def get_attackable_tiles_from(
|
|
613
|
+
self,
|
|
614
|
+
position: Position,
|
|
615
|
+
direction: Direction,
|
|
616
|
+
turret_type: EntityType,
|
|
617
|
+
) -> list[Position]:
|
|
618
|
+
"""Return all in-bounds tiles in a hypothetical turret's raw attack pattern.
|
|
619
|
+
This ignores ammo, cooldown, occupancy, and other target-specific legality checks.
|
|
620
|
+
For gunners this includes the full firing line within range, even behind walls.
|
|
621
|
+
Launchers ignore direction."""
|
|
622
|
+
...
|
|
623
|
+
|
|
624
|
+
def can_launch(self, bot_pos: Position, target: Position) -> bool:
|
|
625
|
+
"""Return True if this launcher can pick up the builder bot at bot_pos and throw it to target."""
|
|
626
|
+
...
|
|
627
|
+
|
|
628
|
+
def launch(self, bot_pos: Position, target: Position) -> None:
|
|
629
|
+
"""Pick up the builder bot at bot_pos and throw it to target.
|
|
630
|
+
Raises GameError if not legal."""
|
|
631
|
+
...
|
|
632
|
+
|
|
633
|
+
# --- Core ---
|
|
634
|
+
|
|
635
|
+
def spawn_builder(self, position: Position) -> int:
|
|
636
|
+
"""Spawn a builder bot on a tile immediately surrounding the core's footprint at position.
|
|
637
|
+
Costs one action cooldown. Raises GameError if not legal."""
|
|
638
|
+
...
|
|
639
|
+
|
|
640
|
+
def can_spawn(self, position: Position) -> bool:
|
|
641
|
+
"""Return True if the core can spawn a builder bot at position this turn.
|
|
642
|
+
Also requires spare room under the global 50-unit cap."""
|
|
643
|
+
...
|
|
644
|
+
|
|
645
|
+
# --- Indicators ---
|
|
646
|
+
|
|
647
|
+
def draw_indicator_line(
|
|
648
|
+
self, pos_a: Position, pos_b: Position, r: int, g: int, b: int
|
|
649
|
+
) -> None:
|
|
650
|
+
"""Draw a debug line from pos_a to pos_b with RGB colour. Saved to the replay."""
|
|
651
|
+
...
|
|
652
|
+
|
|
653
|
+
def draw_indicator_dot(self, pos: Position, r: int, g: int, b: int) -> None:
|
|
654
|
+
"""Draw a debug dot at pos with RGB colour. Saved to the replay."""
|
|
655
|
+
...
|