From 723e191f106aa33fb05cdbc87774ed12daa30eef Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 22 Jul 2023 14:01:02 -0700 Subject: [PATCH] Create a checked, releasable type for laser codes. The release behavior isn't used yet, but I'm working on pre-allocating laser codes for front lines and flights to make it easier for players to pick the laser codes for their weapons. --- game/lasercodes/__init__.py | 2 + game/lasercodes/ilasercoderegistry.py | 17 +++++ game/lasercodes/lasercode.py | 56 ++++++++++++++ game/lasercodes/lasercoderegistry.py | 20 ++++- .../aircraft/flightgroupconfigurator.py | 2 +- game/missiongenerator/flotgenerator.py | 3 +- tests/lasercodes/test_lasercode.py | 75 +++++++++++++++++++ tests/lasercodes/test_lasercoderegistry.py | 12 ++- 8 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 game/lasercodes/ilasercoderegistry.py create mode 100644 game/lasercodes/lasercode.py create mode 100644 tests/lasercodes/test_lasercode.py diff --git a/game/lasercodes/__init__.py b/game/lasercodes/__init__.py index 4b2e1986..1361ef6d 100644 --- a/game/lasercodes/__init__.py +++ b/game/lasercodes/__init__.py @@ -1 +1,3 @@ +from .ilasercoderegistry import ILaserCodeRegistry +from .lasercode import LaserCode from .lasercoderegistry import LaserCodeRegistry diff --git a/game/lasercodes/ilasercoderegistry.py b/game/lasercodes/ilasercoderegistry.py new file mode 100644 index 00000000..e59a9d66 --- /dev/null +++ b/game/lasercodes/ilasercoderegistry.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .lasercode import LaserCode + + +class ILaserCodeRegistry(ABC): + @abstractmethod + def alloc_laser_code(self) -> LaserCode: + ... + + @abstractmethod + def release_code(self, code: LaserCode) -> None: + ... diff --git a/game/lasercodes/lasercode.py b/game/lasercodes/lasercode.py new file mode 100644 index 00000000..c80cf5db --- /dev/null +++ b/game/lasercodes/lasercode.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .ilasercoderegistry import ILaserCodeRegistry + + +class LaserCode: + def __init__(self, code: int, registry: ILaserCodeRegistry) -> None: + self.verify_laser_code(code) + self.code = code + self.registry = registry + + def release(self) -> None: + self.registry.release_code(self) + + @staticmethod + def verify_laser_code(code: int) -> None: + # https://forum.dcs.world/topic/211574-valid-laser-codes/ + # Valid laser codes are as follows + # First digit is always 1 + # Second digit is 5-7 + # Third and fourth digits are 1 - 8 + # We iterate backward (reversed()) so that 1687 follows 1688 + + # Special case used by FC3 aircraft like the A-10A that is not valid for other + # aircraft. + if code == 1113: + return + + # Must be 4 digits with no leading 0 + if code < 1000 or code >= 2000: + raise ValueError + + # The first digit was already verified above. Isolate the remaining three + # digits. The resulting list is ordered by significance, not printed position. + digits = [code // 10**i % 10 for i in range(3)] + + if digits[0] < 1 or digits[0] > 8: + raise ValueError + if digits[1] < 1 or digits[1] > 8: + raise ValueError + if digits[2] < 5 or digits[2] > 7: + raise ValueError + + def __str__(self) -> str: + return f"{self.code}" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, LaserCode): + return False + return self.code == other.code + + def __hash__(self) -> int: + return hash(self.code) diff --git a/game/lasercodes/lasercoderegistry.py b/game/lasercodes/lasercoderegistry.py index 2b4226b3..7913de1a 100644 --- a/game/lasercodes/lasercoderegistry.py +++ b/game/lasercodes/lasercoderegistry.py @@ -1,19 +1,33 @@ +import logging from collections import deque +from .ilasercoderegistry import ILaserCodeRegistry +from .lasercode import LaserCode -class LaserCodeRegistry: + +class LaserCodeRegistry(ILaserCodeRegistry): def __init__(self) -> None: self.allocated_codes: set[int] = set() self.available_codes = LaserCodeRegistry._all_valid_laser_codes() + self.fc3_code = LaserCode(1113, self) - def alloc_laser_code(self) -> int: + def alloc_laser_code(self) -> LaserCode: try: code = self.available_codes.popleft() self.allocated_codes.add(code) - return code + return LaserCode(code, self) except IndexError: raise RuntimeError("All laser codes have been allocated") + def release_code(self, code: LaserCode) -> None: + if code.code in self.allocated_codes: + self.allocated_codes.remove(code.code) + self.available_codes.appendleft(code.code) + else: + logging.error( + "attempted to release laser code %d which was not allocated", code.code + ) + @staticmethod def _all_valid_laser_codes() -> deque[int]: # Valid laser codes are as follows diff --git a/game/missiongenerator/aircraft/flightgroupconfigurator.py b/game/missiongenerator/aircraft/flightgroupconfigurator.py index fa7e5764..457c48df 100644 --- a/game/missiongenerator/aircraft/flightgroupconfigurator.py +++ b/game/missiongenerator/aircraft/flightgroupconfigurator.py @@ -148,7 +148,7 @@ class FlightGroupConfigurator: ) -> None: self.set_skill(unit, member) if member.loadout.has_weapon_of_type(WeaponTypeEnum.TGP) and member.is_player: - laser_codes.append(self.laser_code_registry.alloc_laser_code()) + laser_codes.append(self.laser_code_registry.alloc_laser_code().code) else: laser_codes.append(None) settings = self.flight.coalition.game.settings diff --git a/game/missiongenerator/flotgenerator.py b/game/missiongenerator/flotgenerator.py index 7027b966..978815b0 100644 --- a/game/missiongenerator/flotgenerator.py +++ b/game/missiongenerator/flotgenerator.py @@ -144,13 +144,12 @@ class FlotGenerator: # Add JTAC if self.game.blue.faction.has_jtac: - code: int freq = self.radio_registry.alloc_uhf() # If the option fc3LaserCode is enabled, force all JTAC # laser codes to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs. # Otherwise use 1688 for the first JTAC, 1687 for the second etc. if self.game.settings.plugins.get("ctld.fc3LaserCode"): - code = 1113 + code = self.laser_code_registry.fc3_code else: code = self.laser_code_registry.alloc_laser_code() diff --git a/tests/lasercodes/test_lasercode.py b/tests/lasercodes/test_lasercode.py new file mode 100644 index 00000000..581de8ef --- /dev/null +++ b/tests/lasercodes/test_lasercode.py @@ -0,0 +1,75 @@ +import pytest + +from game.lasercodes import ILaserCodeRegistry +from game.lasercodes.lasercode import LaserCode + + +class MockRegistry(ILaserCodeRegistry): + def __init__(self) -> None: + self.release_count = 0 + + def alloc_laser_code(self) -> LaserCode: + raise NotImplementedError + + def release_code(self, code: LaserCode) -> None: + self.release_count += 1 + + +@pytest.fixture(name="registry") +def mock_registry() -> MockRegistry: + return MockRegistry() + + +def test_lasercode_code(registry: ILaserCodeRegistry) -> None: + + assert LaserCode(1688, registry).code == 1688 + + # 1113 doesn't comply to the rules, but is the only code valid for FC3 aircraft like + # the A-10A. + assert LaserCode(1113, registry).code == 1113 + + # The first digit must be 1 + with pytest.raises(ValueError): + # And be exactly 4 digits + LaserCode(2688, registry) + + # The code must be exactly 4 digits + with pytest.raises(ValueError): + LaserCode(888, registry) + with pytest.raises(ValueError): + LaserCode(18888, registry) + + # 0 and 9 are invalid digits + with pytest.raises(ValueError): + LaserCode(1088, registry) + with pytest.raises(ValueError): + LaserCode(1608, registry) + with pytest.raises(ValueError): + LaserCode(1680, registry) + with pytest.raises(ValueError): + LaserCode(1988, registry) + with pytest.raises(ValueError): + LaserCode(1698, registry) + with pytest.raises(ValueError): + LaserCode(1689, registry) + + # The second digit is further constrained to be 5, 6, or 7. + with pytest.raises(ValueError): + LaserCode(1188, registry) + with pytest.raises(ValueError): + LaserCode(1288, registry) + with pytest.raises(ValueError): + LaserCode(1388, registry) + with pytest.raises(ValueError): + LaserCode(1488, registry) + with pytest.raises(ValueError): + LaserCode(1888, registry) + + +def test_lasercode_release(registry: MockRegistry) -> None: + code = LaserCode(1688, registry) + assert registry.release_count == 0 + code.release() + assert registry.release_count == 1 + code.release() + assert registry.release_count == 2 diff --git a/tests/lasercodes/test_lasercoderegistry.py b/tests/lasercodes/test_lasercoderegistry.py index be5d595d..2f8a3a8f 100644 --- a/tests/lasercodes/test_lasercoderegistry.py +++ b/tests/lasercodes/test_lasercoderegistry.py @@ -10,6 +10,16 @@ def test_initial_laser_codes() -> None: def test_alloc_laser_code() -> None: reg = LaserCodeRegistry() - assert reg.alloc_laser_code() == 1688 + assert reg.alloc_laser_code().code == 1688 assert 1688 not in reg.available_codes assert len(reg.available_codes) == 191 + + +def test_release_code() -> None: + reg = LaserCodeRegistry() + code = reg.alloc_laser_code() + code.release() + assert code.code in reg.available_codes + assert len(reg.available_codes) == 192 + code.release() + assert len(reg.available_codes) == 192