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.
This commit is contained in:
Dan Albert 2023-07-22 14:01:02 -07:00
parent d3269bca93
commit 31289adb50
8 changed files with 181 additions and 8 deletions

View File

@ -1 +1,3 @@
from .ilasercoderegistry import ILaserCodeRegistry
from .lasercode import LaserCode
from .lasercoderegistry import LaserCodeRegistry

View File

@ -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:
...

View File

@ -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)

View File

@ -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

View File

@ -133,7 +133,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)

View File

@ -37,13 +37,13 @@ from game.ground_forces.ai_ground_planner import (
DISTANCE_FROM_FRONTLINE,
)
from game.ground_forces.combat_stance import CombatStance
from game.lasercodes import LaserCodeRegistry
from game.naming import namegen
from game.radio.radios import RadioRegistry
from game.theater.controlpoint import ControlPoint
from game.unitmap import UnitMap
from game.utils import Heading
from .frontlineconflictdescription import FrontLineConflictDescription
from game.lasercodes import LaserCodeRegistry
from .missiondata import JtacInfo, MissionData
if TYPE_CHECKING:
@ -136,13 +136,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.lua_plugin_manager.is_option_enabled("ctld", "fc3LaserCode"):
code = 1113
code = self.laser_code_registry.fc3_code
else:
code = self.laser_code_registry.alloc_laser_code()

View File

@ -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

View File

@ -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