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 committed by Raffson
parent cb6bffe3ec
commit 723e191f10
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
8 changed files with 180 additions and 7 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

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

View File

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

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