Skip to content

Commit

Permalink
Merge pull request #117 from CyberiaResurrection/VerifyStarCanonicali…
Browse files Browse the repository at this point in the history
…sation

Verify and extend star canonicalisation
  • Loading branch information
CyberiaResurrection authored Jun 30, 2024
2 parents 7d9cb3f + 3def9e9 commit 8c42a3d
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 18 deletions.
9 changes: 9 additions & 0 deletions PyRoute/Star.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,18 @@ def fix_ex(self):
if not self.economics:
return

resources = self._ehex_to_int(self.economics[1])
labor = self._ehex_to_int(self.economics[2])
infrastructure = self._ehex_to_int(self.economics[3])

if 8 > self.uwp.tl_code:
nu_resources = self._int_to_ehex(max(0, min(12, resources)))
self.economics = self.economics[0:1] + nu_resources + self.economics[2:]
else:
max_resources = 12 + self.ggCount + self.belts
nu_resources = self._int_to_ehex(max(0, min(max_resources, resources)))
self.economics = self.economics[0:1] + nu_resources + self.economics[2:]

if labor != max(self.popCode - 1, 0):
nu_labour = self._int_to_ehex(max(self.popCode - 1, 0))
self.economics = self.economics[0:2] + nu_labour + self.economics[3:]
Expand Down
13 changes: 8 additions & 5 deletions PyRoute/SystemData/UWP.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

class UWP(object):
# Port code, size, atmo, hydro, pop, gov, law, the all-important hyphen, then TL
match_string = '^([A-HXYa-hxy\?])([0-9A-Fa-f\?])([0-9A-Fa-f\?])([0-9Aa\?])([0-9A-Fa-f\?])([0-9A-Za-z\?])([0-9A-Ja-j\?])-([0-9A-Za-z\?])'
match_string = '^([A-HXYa-hxy\?])([0-9A-Fa-f\?])([0-9A-Za-z\?])([0-9A-Za-z\?])([0-9A-Fa-f\?])([0-9A-Za-z\?])([0-9A-Ja-j\?])-([0-9A-Za-z\?])'

match = re.compile(match_string)

Expand Down Expand Up @@ -319,24 +319,27 @@ def canonicalise(self):
def _canonicalise_physicals(self):
size_is_zero = self.size_is_zero
if '0' == self.size:
self.atmo = '0'
self.hydro = '0'
self.atmo_code = 0
self.hydro_code = 0

if not size_is_zero and '?' != self.atmo:
max_atmo, min_atmo = self._get_atmo_bounds()
self.atmo_code = max(min_atmo, min(max_atmo, self.atmo_code))

# Handle short-circuit values first, then (if needed) drop to the general case
if '1' == str(self.size):
self.hydro = '0'
self.hydro_code = 0

elif not size_is_zero and '?' != self.atmo and '?' != self.hydro:
max_hydro, min_hydro = self._get_hydro_bounds()
self.hydro_code = max(min_hydro, min(max_hydro, self.hydro_code))

def _canonicalise_socials(self):
if 'X' == self.gov:
pass
if 0 < self.pop_code:
self.gov_code = 0
max_gov, min_gov = self._get_gov_bounds()
self.gov_code = max(min_gov, min(max_gov, self.gov_code))
elif '?' != self.gov:
max_gov, min_gov = self._get_gov_bounds()
self.gov_code = max(min_gov, min(max_gov, self.gov_code))
Expand Down
21 changes: 14 additions & 7 deletions PyRoute/TradeCodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,14 @@ def _preprocess_initial_codes(self, initial_codes):
if ')' == raw:
continue
if raw.startswith('Di('):
codes.append(raw)
if not raw.endswith(')') and i < num_codes - 1:
next = raw_codes[i + 1]
if next.endswith(')'):
combo = raw + ' ' + next
codes.append(combo)
raw_codes[i + 1] = ''
else:
codes.append(raw)
continue
if 7 < len(raw) and '(' == raw[0] and ')' == raw[-2]: # Let older-style sophont codes through
codes.append(raw)
Expand Down Expand Up @@ -282,7 +289,7 @@ def calculate_pcode(self):
return self.pcode

def _check_planet_code(self, star, code, size, atmo, hydro, listmsg=None):
size = '0123456789ABC' if size is None else size
size = '0123456789ABCDEF' if size is None else size
atmo = '0123456789ABCDEF' if atmo is None else atmo
hydro = '0123456789A' if hydro is None else hydro
star_match = star.size in size and star.atmo in atmo and star.hydro in hydro
Expand Down Expand Up @@ -353,7 +360,7 @@ def check_world_codes(self, star, msg=None, fix_pop=False):
check = self._check_planet_code(star, 'He', '3456789ABC', '2479ABC', '012', msg) and check
check = self._check_planet_code(star, 'Ic', None, '01', '123456789A', msg) and check
check = self._check_planet_code(star, 'Po', None, '2345', '0123', msg) and check
check = self._check_planet_code(star, 'Oc', 'ABCD', '3456789DEF', 'A', msg) and check
check = self._check_planet_code(star, 'Oc', 'ABCDEF', '3456789DEF', 'A', msg) and check
check = self._check_planet_code(star, 'Va', None, '0', None, msg) and check
check = self._check_planet_code(star, 'Wa', '3456789', '3456789DEF', 'A', msg) and check

Expand All @@ -376,7 +383,7 @@ def _check_all_pop_codes(self, check, msg, star):
check = self._check_pop_code(star, 'Lo', '123', msg) and check
check = self._check_pop_code(star, 'Ni', '456', msg) and check
check = self._check_pop_code(star, 'Ph', '8', msg) and check
check = self._check_pop_code(star, 'Hi', '9ABCD', msg) and check
check = self._check_pop_code(star, 'Hi', '9ABCDEF', msg) and check
return check

def owned_by(self, star):
Expand Down Expand Up @@ -654,7 +661,7 @@ def canonicalise(self, star):
self._fix_trade_code(star, 'He', '3456789ABC', '2479ABC', '012')
self._fix_trade_code(star, 'Wa', '3456789', '3456789DEF', 'A')
self._fix_trade_code(star, 'Ga', '678', '568', '567')
self._fix_trade_code(star, 'Oc', 'ABCD', '3456789DEF', 'A')
self._fix_trade_code(star, 'Oc', 'ABCDEF', '3456789DEF', 'A')
self._fix_trade_code(star, 'Va', None, '0', None)

self._fix_econ_code(star, 'Na', '0123', '0123', '6789ABCD')
Expand All @@ -668,7 +675,7 @@ def canonicalise(self, star):
self._fix_all_pop_codes(star)

def _fix_trade_code(self, star, code, size, atmo, hydro):
size = '0123456789ABC' if size is None else size
size = '0123456789ABCDEF' if size is None else size
atmo = '0123456789ABCDEF' if atmo is None else atmo
hydro = '0123456789A' if hydro is None else hydro

Expand Down Expand Up @@ -715,7 +722,7 @@ def _fix_all_pop_codes(self, star):
self._fix_pop_code(star, 'Lo', '123')
self._fix_pop_code(star, 'Ni', '456')
self._fix_pop_code(star, 'Ph', '8')
self._fix_pop_code(star, 'Hi', '9ABCD')
self._fix_pop_code(star, 'Hi', '9ABCDEF')

def _drop_invalid_trade_code(self, targcode):
self.codes = [code for code in self.codes if code != targcode]
Expand Down
8 changes: 7 additions & 1 deletion Tests/Hypothesis/Inputs/testHypothesisStarlineParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ def comparison_line(draw):
'0000 000000000000000 0000000-0 000000000000 (0 - - 0 000 ?0)',
'0000 000000000000000 ???????-? 000000000000000 - - A 000 --0',
'0000 000000000000000 ???????-? 000000000000000 - - A 000 --0',
'0000 000000000000000 ???????-? 000000000000000 {0} - [0000] - A 000 ?0'
'0000 000000000000000 ???????-? 000000000000000 {0} - [0000] - A 000 ?0',
'0000 000000000000000 ???????-? 000000000000 (0 - (000-0) [0000] B - A 000 00?',
'0000 000000000000000 0000000-0 (00000000000000 B - A 000 ?0)0000000000',
'0000 000000000000000 0000000-0 (00000000000000 - (000-0) - B - A 000 0?'
]

candidate = draw(from_regex(regex=ParseStarInput.starline, alphabet='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWYXZ -{}()[]?\'+*'))
Expand Down Expand Up @@ -317,6 +320,9 @@ class testHypothesisStarlineParser(unittest.TestCase):
@example('0000 000000000000000 0000000-0 000000000000000 - - A 000 00?0000-0 ', 'weird')
@example('2234 00001111111111[ 1234446-N (111114GK\'ehilp - (005-1) - B - U 112 WW', 'weird')
@example('0000 000000000000000 ???????-? (0 000000000)0? - - A 000 00?', 'weird')
@example('0000 000000000000000 ???????-? 000000000000 (0 - (000-0) [0000] B - A 000 00?', 'weird')
@example('0000 000000000000000 0000000-0 (00000000000000 B - A 000 ?0)0000000000', 'weird')
@example('0000 000000000000000 0000000-0 (00000000000000 - (000-0) - B - A 000 0?', 'weird')
def test_starline_parser_against_regex(self, s, match):
# if it's a known weird-parse case, assume it out now
assume(match != 'weird')
Expand Down
63 changes: 63 additions & 0 deletions Tests/Hypothesis/testDeltaStar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import unittest

from PyRoute.DeltaStar import DeltaStar
from PyRoute.Galaxy import Sector


class testDeltaStar(unittest.TestCase):

def test_plain_canonicalisation(self):
cases = [
# Commented out for the moment - Barren/Dieback is a bit more intricate than other pop codes, and don't want
# perfect to be the enemy of good
#('0917 Deyis II E874000-0 Ba Da (Kebkh) Re { -3 } (200-5) [0000] - - A 000 10 ImDi K4 II ',
# '0917 Deyis II E874000-2 Ba Da Di(Kebkh) Re { -3 } (200-5) [0000] - - A 000 10 ImDi K4 II '),
('0235 Oduart C7B3004-5 Fl Di(Oduart) Da { -2 } (800+2) [0000] - M A 004 10 HvFd G4 V M3 V M7 V',
'0235 Oduart C7B3004-5 Ba Da Di(Oduart) Fl { -2 } (800+2) [0000] - M A 004 10 HvFd G4 V M3 V M7 V '),
('0101 X73A000-0 Ba Lo Ni Wa - - - 012 --',
'0101 X738000-3 Ba { -3 } - - - - - 012 0 -- '),
('0527 Wellington Base AEFA422-E Ht Ni Oc - M R 711 He',
'0527 Wellington Base AEFA422-A Ht Ni Oc { 1 } - - - M R 711 0 He '),
('0239 Etromen CFB8558-B Fl Ni - - A 523 Na',
'0239 Etromen CFB8558-8 Fl Ni { -2 } - - - - A 523 0 Na '),
('1220 Gateway A002688-B As Ic Na Ni Va Cx { 1 } (C55+1) [675B] - - - 822 16 GaFd A2 V F0 V',
'1220 Gateway A000688-9 As Cx Na Ni Va { 0 } (C55+1) [675B] - - - 822 16 GaFd A2 V F0 V '),
('0825 Corstation C005100-8 As Ic Lo Va { -2 } (500-5) [1113] - - - 211 14 GaFd M0 V M1 V',
'0825 Corstation C000100-8 As Lo Va { -2 } (500-5) [1113] - - - 211 14 GaFd M0 V M1 VI '),
('3222 Ardh C001554-A As Ic Ni Va {+0} (843+1) [658A] - C - 601 Ve F9 V',
'3222 Ardh C000554-A As Ni Va { 0 } (843+1) [658A] - C - 601 0 Ve F9 V '),
('2738 Taen B001685-8 As Ic Ni Na Va {-1} (J52-2) [3559] - N - 533 Ve M9 V',
'2738 Taen B000685-8 As Na Ni Va { -1 } (J52-2) [3559] - N - 533 0 Ve M9 V '),
# Population zero, TL 0, alongside a specific dieback should keep the barren
('0924 Ognar X867000-0 Ba Ga Di(Ogna) {-3 } (300+1) [0000] - - R 004 10 Og K1 V',
'0924 Ognar X867000-2 Ba Di(Ogna) Ga { -3 } (300+1) [0000] - - R 004 10 Og K1 V '),
# Dieback of two-word sophont name tripped things up
('2117 Sabmiqys A560056-H De Fo Di(Gya Ks) { 2 } (600-4) [0000] - - R 004 9 ImDa G3 V ',
'2117 Sabmiqys A561056-6 Ba Di(Gya Ks) Fo { -1 } (600-4) [0000] - - R 004 9 ImDa G3 V '),
# Treat Gov code X with population as Gov 0
('1702 Eslalyat B493AXB-A Hi In - M R 211 Dr ',
'1702 Eslalyat B494A5A-A Hi In { 4 } - - - M R 211 0 Dr '),
# <TL8 worlds should have max resources 12
('1620 Daydyor Yashas X683973-4 Hi Pr { 1 } (E8D+2) [9AAD] - - - 323 14 NaXX F7 V',
'1620 Daydyor Yashas X683973-4 Hi Pr { -1 } (C8B+2) [98A9] - - - 323 14 NaXX F7 V '),
# Atmo should be capped to F, hydro capped to A
('1920 Rainbow Sun AAVV997-D Hi In Sp - KM - 223 Na',
'1920 Rainbow Sun AAFA997-B Hi Oc Sp { 5 } - - - KM - 223 0 Na '),
# TL8+ worlds should have max resources 12 + belts + GGs
('1620 Daydyor Yashas X683973-D Hi Pr { 1 } (F8D+2) [9AAD] - - - 311 14 NaXX F7 V',
'1620 Daydyor Yashas X683973-8 Hi Pr { -1 } (E8D+2) [9AAD] - - - 311 14 NaXX F7 V '),
]

sector = Sector('# Core', '# 0, 0')
for line, expected in cases:
with self.subTest():
foo = DeltaStar.parse_line_into_star(line, sector, 'fixed', 'fixed')
self.assertIsNotNone(foo, "Line should parse to star")
foo.canonicalise()
foo_string = foo.parse_to_line()

self.assertEqual(expected, foo_string, 'Unexpected canonicalisation result')


if __name__ == '__main__':
unittest.main()
64 changes: 61 additions & 3 deletions Tests/Hypothesis/testTradeCodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import timedelta

from hypothesis import given, assume, example, HealthCheck, settings
from hypothesis.strategies import text, from_regex, composite, sampled_from, lists, floats
from hypothesis.strategies import text, from_regex, composite, sampled_from, lists, floats, booleans

from PyRoute.Galaxy import Sector
from PyRoute.Inputs.ParseStarInput import ParseStarInput
Expand All @@ -15,7 +15,7 @@


@composite
def trade_code(draw):
def trade_code(draw, unique=False, min_size=2, max_size=12):
if 0 == len(tradecodes):
tradecodes.extend(TradeCodes.pcodes)
tradecodes.extend(TradeCodes.dcodes)
Expand All @@ -24,7 +24,7 @@ def trade_code(draw):

strat = sampled_from(tradecodes)

return draw(lists(strat, min_size=2, max_size=12))
return draw(lists(strat, min_size=min_size, max_size=max_size, unique=unique))


@composite
Expand Down Expand Up @@ -138,6 +138,64 @@ def test_verify_canonicalisation_is_idempotent(self, s, trade_line):
badline = '' if result else msg[0]
self.assertEqual(0, len(msg), "Canonicalisation failed. " + badline + '\n' + hyp_input)

@given(trade_code(unique=True))
def test_verify_trade_codes_from_direct_selection(self, trade_line):
if isinstance(trade_line, list):
trade_line = ' '.join(trade_line)

trade = TradeCodes(trade_line)

result, _ = trade.is_well_formed()
assume(result)

trade_string = str(trade)

nu_trade = TradeCodes(trade_string)
result, msg = nu_trade.is_well_formed()
self.assertTrue(result, msg)

nu_trade_string = str(nu_trade)
msg = "Re-parsed TradeCodes string does not equal original parsed string"
self.assertEqual(trade_string, nu_trade_string, msg)

@given(from_regex(regex=UWP.match, alphabet='0123456789abcdefghjklmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWYXZ -{}()[]?\'+*'),
trade_code(max_size=1, min_size=1),
booleans())
def test_has_code_but_not_uwp(self, s, trade_line, forward):
s = s[0:9]
if isinstance(trade_line, list):
trade_line = ' '.join(trade_line)
hyp_input = 'Hypothesis input: \'' + s + '\', \'' + trade_line + '\''
starline = '0101 000000000000000 {} {} - - A 000 0000D'.format(s, trade_line.ljust(38))
sector = Sector('# Core', '# 0, 0')
pop_code = 'scaled'
ru_calc = 'scaled'

foo = None

try:
foo = Star.parse_line_into_star(starline, sector, pop_code, ru_calc)
except KeyError:
pass
assume(foo is not None)

# filter out malformed tradeCode objects while we're at it
result, _ = foo.tradeCode.is_well_formed()
assume(result)
trade = foo.tradeCode

result, msg = trade.check_canonical(foo)
assume(0 < len(msg))
if forward:
submsg = [item for item in msg if "Found invalid" in item]
else:
submsg = [item for item in msg if "not in trade codes" in item]
assume(0 < len(submsg))

trade.canonicalise(foo)
result, msg = trade.check_canonical(foo)
badline = '' if result else msg[0]
self.assertEqual(0, len(msg), "Canonicalisation failed. " + badline + '\n' + hyp_input)

if __name__ == '__main__':
unittest.main()
4 changes: 2 additions & 2 deletions Tests/Hypothesis/testUWP.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ def test_initial_parsing(self, uwp_line, expected_oldskool):
@example('?B00000-0', '?B61000-2')
@example('?170000-0', '?160000-4')
@example('?000060-0', '?000050-4')
@example('?0005X0-0', '?0005X0-6')
@example('?0005X0-0', '?000500-6')
@example('?000F00-0', '?000FA5-8')
@example('?000?x0-0', '?000?X0-5')
@example('?0006X0-0', '?0006X0-5') # Not sure exactly what to do with this one - Lintsec likes treating gov X as gov 0
@example('?0006X0-0', '?000610-5') # Not sure exactly what to do with this one - Lintsec likes treating gov X as gov 0
@example('?000F00-0', '?000FA5-8')
@example('?0000G0-0', '?000050-4')
@example('?000?F0-0', '?000?FA-4')
Expand Down

0 comments on commit 8c42a3d

Please sign in to comment.