Module botroyale.logic.maps
Maps (initial states for battle).
It is recommended to use get_map_state()
in order to "initialize" a
State
object.
Expand source code Browse git
"""Maps (initial states for battle).
It is recommended to use `botroyale.logic.maps.get_map_state` in order to "initialize" a
`botroyale.logic.state.State` object.
"""
from typing import Optional
import json
from itertools import chain
from pathlib import Path
from botroyale.util import PACKAGE_DIR
from botroyale.util.file import file_load, file_dump, get_usr_dir
from botroyale.util import settings
from botroyale.util.hexagon import Hexagon, Hex, ORIGIN
from botroyale.logic.state import State
from botroyale.logic.plate import Plate
BUILTIN_MAP_DIR = PACKAGE_DIR / "logic" / "maps"
"""Directory where builtin maps are stored on disk."""
USR_MAP_DIR = get_usr_dir("maps")
"""Directory where custom user maps are stored on disk."""
if not USR_MAP_DIR.is_dir():
USR_MAP_DIR.mkdir(parents=True, exist_ok=True)
def _find_maps(use_custom: bool = True) -> dict[str, Path]:
"""Return list of map names and their paths as found on disk.
Setting *use_custom* to True will include the "maps" usr dir. Custom user
maps override builtin maps.
"""
map_files = {}
path_iter = BUILTIN_MAP_DIR.iterdir()
path_iter = chain(USR_MAP_DIR.iterdir(), path_iter)
for file in path_iter:
if not len(file.suffixes) == 1 or file.suffix != ".json":
continue
if file.stem == "custom" and not use_custom:
continue
if file.stem in map_files:
continue
map_files[file.stem] = file
return map_files
DEFAULT_STATE: State = State(death_radius=12)
"""A `botroyale.logic.state.State` object representing a default, "empty" map.
Should be copied (`botroyale.logic.state.State.copy`) before use.
"""
MAPS: tuple[str, ...] = tuple(sorted(_find_maps().keys()))
"""Tuple of available map names.
Searches the "maps" dir from `botroyale.util.file.get_usr_dir` and builtin maps.
"""
DEFAULT_MAP_NAME: str = settings.get("battle.default_map")
"""Default map name as configured in settings."""
assert DEFAULT_MAP_NAME in MAPS
def get_map_state(map_name: Optional[str] = None) -> State:
"""Return the state based on the map by name.
Passing None to *map_name* will use `DEFAULT_MAP_NAME`.
"""
if map_name is None:
map_name = DEFAULT_MAP_NAME
return _load_map(map_name)
def _load_map(map_name: Optional[str] = None, use_default: bool = True) -> State:
"""Returns a state based on the map saved on disk by name.
The default state will be returned if `map_name` is None or if `use_default`
is true and the map wasn't found.
"""
if map_name is None:
return DEFAULT_STATE.copy()
map_files = _find_maps()
if map_name not in map_files:
if use_default:
print(f"{map_name=} not found in {map_files=}")
return DEFAULT_STATE.copy()
raise FileNotFoundError(f'Could not find map: "{map_name}"')
data = json.loads(file_load(map_files[map_name]))
return State(
death_radius=data["death_radius"],
positions=[Hex(x, y) for x, y in data["positions"]],
pits={Hex(x, y) for x, y in data["pits"]},
walls={Hex(x, y) for x, y in data["walls"]},
plates={Plate.from_exported(p) for p in data.get("plates", [])},
)
def _save_map(map_name: str, state: State, allow_overwrite: bool = True):
"""Saves the map based on the current state to disk by name.
The *allow_overwrite* argument will allow overwriting an existing file.
"""
data = {
"death_radius": state.death_radius,
"positions": [p.xy for p in state.positions],
"pits": [p.xy for p in state.pits],
"walls": [w.xy for w in state.walls],
"plates": [p.export() for p in state.plates],
}
map_file = USR_MAP_DIR / f"{map_name}.json"
if not allow_overwrite and map_file.is_file():
raise FileExistsError(f"Not allowed to overwrite saved map: {map_file}")
file_dump(map_file, json.dumps(data))
class MapCreator:
"""The MapCreator object is used to interactively create maps.
Most use cases do not require the MapCreator, see the `get_map_state`
function.
"""
def __init__(
self,
initial_state: Optional[State] = None,
mirror_mode: int = 1,
):
"""Initialize the class."""
self.set_mirror_mode(mirror_mode)
if initial_state is None:
initial_state = _load_map()
self.state = initial_state
def set_mirror_mode(self, mode: int = 1):
"""Sets the mirror mode. Must be one of: 1, 2, 3, 6."""
assert mode in (1, 2, 3, 6)
self.mirror_mode = mode
self.mirror_rot = int(6 / mode)
def save(self, file_name: Optional[str] = None):
"""Save the map to file."""
if file_name is None:
file_name = "custom"
_save_map(file_name, self.state)
def load(self, file_name: Optional[str] = None):
"""Load the map from file."""
if file_name is None:
file_name = "custom"
self.state = _load_map(file_name)
def increment_death_radius(self, delta: int):
"""Increase the death radius by a delta. Can be negative.
Will not set a value lower than 3.
"""
new_val = self.state.death_radius + delta
self.state.death_radius = max(3, new_val)
def add_spawn(self, hex: Hexagon):
"""Clear contents and add a spawn at hex."""
for h in self._get_mirrored(hex):
self.clear_contents(h)
self.state.positions.append(h)
self._refresh_state()
def add_pit(self, hex: Hexagon):
"""Clear contents and add a pit at hex."""
for h in self._get_mirrored(hex):
self.clear_contents(h)
self.state.pits.add(h)
def add_wall(self, hex: Hexagon):
"""Clear contents and add a wall at hex."""
for h in self._get_mirrored(hex):
self.clear_contents(h)
self.state.walls.add(h)
def add_plate(self, plate: Plate):
"""Clear contents and add a plate at hex."""
assert isinstance(plate, Plate)
for p in self._get_mirrored_plate(plate):
self.clear_contents(p)
self.state.plates.add(p)
def clear_contents(self, hex: Hexagon, mirrored: bool = False):
"""Clear the contents of hex: clears spawns, walls, and pits.
Passing True to the *mirrored* argument will clear mirrored hexes based
on the mirror mode.
"""
hexes = [hex]
if mirrored:
hexes = self._get_mirrored(hex)
for h in hexes:
if h in self.state.positions:
self.state.positions.remove(h)
self._refresh_state()
if h in self.state.pits:
self.state.pits.remove(h)
if h in self.state.walls:
self.state.walls.remove(h)
if h in self.state.plates:
self.state.plates.remove(h)
def clear_all(self):
"""Resets the map to the default state."""
self.state = DEFAULT_STATE.copy()
def check_valid(self, check_spawn: bool = True, check_overlap: bool = True) -> bool:
"""Checks that the map is valid.
Args:
check_spawn: Assert that spawns won't instantly die.
check_overlap: Assert that walls and pits don't overlap.
"""
if self.state.death_radius < 3:
return False
if set(self.state.positions) & self.state.walls:
return False
if len(self.state.positions) == 0:
return False
if check_spawn:
if set(self.state.positions) & self.state.pits:
return False
for spawn in self.state.positions:
if spawn.get_distance(ORIGIN) >= self.state.death_radius - 1:
return False
if check_overlap:
if self.state.pits & self.state.walls:
return False
return True
def _get_mirrored(self, hex: Hexagon) -> list[Hexagon]:
"""Returns a list of hexes that are mirrors of hex, based on the mirror mode."""
return [hex.rotate(-self.mirror_rot * rot) for rot in range(self.mirror_mode)]
def _get_mirrored_plate(self, plate: Plate) -> list[Plate]:
"""Returns a list of plates mirrors, based on the mirror mode."""
new_plates = []
for r in range(self.mirror_mode):
rot = -self.mirror_rot * r
h = plate.rotate(rot)
p = plate.with_new_hex(h)
new_targets = {t.rotate(rot) for t in plate.targets}
p.targets = new_targets
new_plates.append(p)
return new_plates
def _refresh_state(self):
"""Recreates the state.
This is used to refresh the state when adding or removing spawns.
"""
self.state = State(
death_radius=self.state.death_radius,
positions=self.state.positions,
pits=self.state.pits,
walls=self.state.walls,
plates=self.state.plates,
)
Global variables
var BUILTIN_MAP_DIR
-
Directory where builtin maps are stored on disk.
var DEFAULT_MAP_NAME : str
-
Default map name as configured in settings.
var DEFAULT_STATE : State
-
A
State
object representing a default, "empty" map.Should be copied (
State.copy()
) before use. var MAPS : tuple[str, ...]
-
Tuple of available map names.
Searches the "maps" dir from
get_usr_dir()
and builtin maps. var USR_MAP_DIR
-
Directory where custom user maps are stored on disk.
Functions
def get_map_state(map_name: Optional[str] = None) ‑> State
-
Return the state based on the map by name.
Passing None to map_name will use
DEFAULT_MAP_NAME
.Expand source code Browse git
def get_map_state(map_name: Optional[str] = None) -> State: """Return the state based on the map by name. Passing None to *map_name* will use `DEFAULT_MAP_NAME`. """ if map_name is None: map_name = DEFAULT_MAP_NAME return _load_map(map_name)
Classes
class MapCreator (initial_state: Optional[State] = None, mirror_mode: int = 1)
-
The MapCreator object is used to interactively create maps.
Most use cases do not require the MapCreator, see the
get_map_state()
function.Initialize the class.
Expand source code Browse git
class MapCreator: """The MapCreator object is used to interactively create maps. Most use cases do not require the MapCreator, see the `get_map_state` function. """ def __init__( self, initial_state: Optional[State] = None, mirror_mode: int = 1, ): """Initialize the class.""" self.set_mirror_mode(mirror_mode) if initial_state is None: initial_state = _load_map() self.state = initial_state def set_mirror_mode(self, mode: int = 1): """Sets the mirror mode. Must be one of: 1, 2, 3, 6.""" assert mode in (1, 2, 3, 6) self.mirror_mode = mode self.mirror_rot = int(6 / mode) def save(self, file_name: Optional[str] = None): """Save the map to file.""" if file_name is None: file_name = "custom" _save_map(file_name, self.state) def load(self, file_name: Optional[str] = None): """Load the map from file.""" if file_name is None: file_name = "custom" self.state = _load_map(file_name) def increment_death_radius(self, delta: int): """Increase the death radius by a delta. Can be negative. Will not set a value lower than 3. """ new_val = self.state.death_radius + delta self.state.death_radius = max(3, new_val) def add_spawn(self, hex: Hexagon): """Clear contents and add a spawn at hex.""" for h in self._get_mirrored(hex): self.clear_contents(h) self.state.positions.append(h) self._refresh_state() def add_pit(self, hex: Hexagon): """Clear contents and add a pit at hex.""" for h in self._get_mirrored(hex): self.clear_contents(h) self.state.pits.add(h) def add_wall(self, hex: Hexagon): """Clear contents and add a wall at hex.""" for h in self._get_mirrored(hex): self.clear_contents(h) self.state.walls.add(h) def add_plate(self, plate: Plate): """Clear contents and add a plate at hex.""" assert isinstance(plate, Plate) for p in self._get_mirrored_plate(plate): self.clear_contents(p) self.state.plates.add(p) def clear_contents(self, hex: Hexagon, mirrored: bool = False): """Clear the contents of hex: clears spawns, walls, and pits. Passing True to the *mirrored* argument will clear mirrored hexes based on the mirror mode. """ hexes = [hex] if mirrored: hexes = self._get_mirrored(hex) for h in hexes: if h in self.state.positions: self.state.positions.remove(h) self._refresh_state() if h in self.state.pits: self.state.pits.remove(h) if h in self.state.walls: self.state.walls.remove(h) if h in self.state.plates: self.state.plates.remove(h) def clear_all(self): """Resets the map to the default state.""" self.state = DEFAULT_STATE.copy() def check_valid(self, check_spawn: bool = True, check_overlap: bool = True) -> bool: """Checks that the map is valid. Args: check_spawn: Assert that spawns won't instantly die. check_overlap: Assert that walls and pits don't overlap. """ if self.state.death_radius < 3: return False if set(self.state.positions) & self.state.walls: return False if len(self.state.positions) == 0: return False if check_spawn: if set(self.state.positions) & self.state.pits: return False for spawn in self.state.positions: if spawn.get_distance(ORIGIN) >= self.state.death_radius - 1: return False if check_overlap: if self.state.pits & self.state.walls: return False return True def _get_mirrored(self, hex: Hexagon) -> list[Hexagon]: """Returns a list of hexes that are mirrors of hex, based on the mirror mode.""" return [hex.rotate(-self.mirror_rot * rot) for rot in range(self.mirror_mode)] def _get_mirrored_plate(self, plate: Plate) -> list[Plate]: """Returns a list of plates mirrors, based on the mirror mode.""" new_plates = [] for r in range(self.mirror_mode): rot = -self.mirror_rot * r h = plate.rotate(rot) p = plate.with_new_hex(h) new_targets = {t.rotate(rot) for t in plate.targets} p.targets = new_targets new_plates.append(p) return new_plates def _refresh_state(self): """Recreates the state. This is used to refresh the state when adding or removing spawns. """ self.state = State( death_radius=self.state.death_radius, positions=self.state.positions, pits=self.state.pits, walls=self.state.walls, plates=self.state.plates, )
Subclasses
Methods
def add_pit(self, hex: Hexagon)
-
Clear contents and add a pit at hex.
Expand source code Browse git
def add_pit(self, hex: Hexagon): """Clear contents and add a pit at hex.""" for h in self._get_mirrored(hex): self.clear_contents(h) self.state.pits.add(h)
def add_plate(self, plate: Plate)
-
Clear contents and add a plate at hex.
Expand source code Browse git
def add_plate(self, plate: Plate): """Clear contents and add a plate at hex.""" assert isinstance(plate, Plate) for p in self._get_mirrored_plate(plate): self.clear_contents(p) self.state.plates.add(p)
def add_spawn(self, hex: Hexagon)
-
Clear contents and add a spawn at hex.
Expand source code Browse git
def add_spawn(self, hex: Hexagon): """Clear contents and add a spawn at hex.""" for h in self._get_mirrored(hex): self.clear_contents(h) self.state.positions.append(h) self._refresh_state()
def add_wall(self, hex: Hexagon)
-
Clear contents and add a wall at hex.
Expand source code Browse git
def add_wall(self, hex: Hexagon): """Clear contents and add a wall at hex.""" for h in self._get_mirrored(hex): self.clear_contents(h) self.state.walls.add(h)
def check_valid(self, check_spawn: bool = True, check_overlap: bool = True) ‑> bool
-
Checks that the map is valid.
Args
check_spawn
- Assert that spawns won't instantly die.
check_overlap
- Assert that walls and pits don't overlap.
Expand source code Browse git
def check_valid(self, check_spawn: bool = True, check_overlap: bool = True) -> bool: """Checks that the map is valid. Args: check_spawn: Assert that spawns won't instantly die. check_overlap: Assert that walls and pits don't overlap. """ if self.state.death_radius < 3: return False if set(self.state.positions) & self.state.walls: return False if len(self.state.positions) == 0: return False if check_spawn: if set(self.state.positions) & self.state.pits: return False for spawn in self.state.positions: if spawn.get_distance(ORIGIN) >= self.state.death_radius - 1: return False if check_overlap: if self.state.pits & self.state.walls: return False return True
def clear_all(self)
-
Resets the map to the default state.
Expand source code Browse git
def clear_all(self): """Resets the map to the default state.""" self.state = DEFAULT_STATE.copy()
def clear_contents(self, hex: Hexagon, mirrored: bool = False)
-
Clear the contents of hex: clears spawns, walls, and pits.
Passing True to the mirrored argument will clear mirrored hexes based on the mirror mode.
Expand source code Browse git
def clear_contents(self, hex: Hexagon, mirrored: bool = False): """Clear the contents of hex: clears spawns, walls, and pits. Passing True to the *mirrored* argument will clear mirrored hexes based on the mirror mode. """ hexes = [hex] if mirrored: hexes = self._get_mirrored(hex) for h in hexes: if h in self.state.positions: self.state.positions.remove(h) self._refresh_state() if h in self.state.pits: self.state.pits.remove(h) if h in self.state.walls: self.state.walls.remove(h) if h in self.state.plates: self.state.plates.remove(h)
def increment_death_radius(self, delta: int)
-
Increase the death radius by a delta. Can be negative.
Will not set a value lower than 3.
Expand source code Browse git
def increment_death_radius(self, delta: int): """Increase the death radius by a delta. Can be negative. Will not set a value lower than 3. """ new_val = self.state.death_radius + delta self.state.death_radius = max(3, new_val)
def load(self, file_name: Optional[str] = None)
-
Load the map from file.
Expand source code Browse git
def load(self, file_name: Optional[str] = None): """Load the map from file.""" if file_name is None: file_name = "custom" self.state = _load_map(file_name)
def save(self, file_name: Optional[str] = None)
-
Save the map to file.
Expand source code Browse git
def save(self, file_name: Optional[str] = None): """Save the map to file.""" if file_name is None: file_name = "custom" _save_map(file_name, self.state)
def set_mirror_mode(self, mode: int = 1)
-
Sets the mirror mode. Must be one of: 1, 2, 3, 6.
Expand source code Browse git
def set_mirror_mode(self, mode: int = 1): """Sets the mirror mode. Must be one of: 1, 2, 3, 6.""" assert mode in (1, 2, 3, 6) self.mirror_mode = mode self.mirror_rot = int(6 / mode)