├── Tests ├── __init__.py ├── AreaItems │ ├── __init__.py │ └── testAreaItem.py ├── Mapping │ └── __init__.py ├── SystemData │ ├── __init__.py │ └── testUtilities.py ├── Pathfinding │ ├── __init__.py │ ├── Schemas │ │ └── testLandmarksExtremesRegression.py │ └── testTradeMPCalculation.py ├── DeltaFiles │ ├── ReadSectorsDummy1.sec │ ├── ReadSectorsDummy2.sec │ ├── border_blowup_3 │ │ └── Raakaan.sec │ ├── no_subsectors_named │ │ ├── Knaeleng empty.sec │ │ ├── Ngathksirz empty.sec │ │ ├── Zao Kfeng Ig Grilokh empty.sec │ │ └── Zao Kfeng Ig Grilokh - subsector P - trimmed.sec │ ├── sector_subset_blowup_on_vland_empty │ │ └── Vland.sec │ ├── border_blowup_2 │ │ └── Zdiedeiant.sec │ ├── border_blowup_1 │ │ └── Zdiedeiant.sec │ ├── insufficient_exhaust_value │ │ └── Core.sec │ ├── single_system_border │ │ └── Deneb.sec │ ├── zao_kfeng_jump_4_template_blowup │ │ └── Zao Kfeng Ig Grilokh.sec │ ├── high_pop_worlds_blowup │ │ └── Spinward Marches.sec │ ├── Dagudashaag-star-object-no-sector-attribute.sec │ ├── stat_calc_division_by_zero_population │ │ └── Dagudashaag-zero.sec │ ├── stars_node_types │ │ └── Antares.sec │ ├── xroute_routes_pass_1_2 │ │ ├── Core.sec │ │ ├── Diaspora.sec │ │ ├── Fornast.sec │ │ └── Massilia.sec │ ├── Passes │ │ ├── Dagudashaag-auxiliary-reduction-two-lines.sec │ │ └── Dagudashaag-subsector-full-reduce.sec │ ├── dagudashaag-allegiance-pax-balance │ │ └── Dagudashaag-delta-error.sec │ ├── xroute_routes_pass_3_4 │ │ └── Dagudashaag.sec │ ├── xroute_routes_pass_3_2 │ │ └── Zarushagar.sec │ ├── xroute_routes_pass_1_4 │ │ ├── Delphi.sec │ │ ├── Core.sec │ │ └── Massilia.sec │ ├── dijkstra_restart_blowup │ │ └── Lishun.sec │ ├── xroute_calculation_blowups │ │ └── Antares-Sakhag-Celebes.sec │ ├── Dagudashaag-subsector-reduced.sec │ ├── Zarushagar-imbalanced-routes.sec │ ├── xroute_routes_pass_2_4 │ │ └── Dagudashaag.sec │ └── duplicate_node_blowup │ │ └── Verge.sec ├── TradeMPFiles │ ├── trademplist.txt │ └── run_trademp_parameters ├── OutputFiles │ ├── verify_coreward_rimward │ │ ├── Ngathksirz Sector.pdf │ │ └── Zao Kfeng Ig Grilokh Sector.pdf │ ├── verify_spinward_trailing │ │ ├── Knaeleng Sector.pdf │ │ └── Zao Kfeng Ig Grilokh Sector.pdf │ ├── verify_quadripoint_trade_write │ │ ├── Deneb Sector.pdf │ │ ├── Corridor Sector.pdf │ │ ├── Provence Sector.pdf │ │ └── Tuglikki Sector.pdf │ ├── verify_single_system_border_write │ │ └── Deneb Sector.pdf │ ├── verify_subsector_xroute_write │ │ └── Zarushagar Sector.pdf │ ├── verify_empty_sector_write │ │ └── Zao Kfeng Ig Grilokh empty.pdf │ ├── verify_subsector_comm_write │ │ ├── Zao Kfeng Ig Grilokh - subsector P - comm.pdf │ │ └── comm stars.txt │ └── verify_subsector_trade_write │ │ └── Zao Kfeng Ig Grilokh - subsector P - trade.pdf ├── StatCalculation │ ├── testUWPCollection.py │ └── testPopulations.py ├── Inputs │ └── testReadSector.py ├── Hypothesis │ ├── Inputs │ │ └── testStarlineTransformer.py │ ├── testNobles.py │ └── testTradeBalance.py ├── Utilities │ └── testNoNoneDefaultDict.py ├── testDeltaDebug.py ├── Dummy │ └── DeltaDebug │ │ └── DummyReduce.py ├── testBase.py ├── DeltaDictionary │ └── testAllegianceSubset.py ├── testNobles.py ├── PathfindingFiles │ └── single_source_distances_ibara_subsector_from_0101.json ├── testUniqueSectors.py ├── BorderGeneration │ ├── Far Frontiers-partial-allymap-border.json │ └── Far Frontiers-partial-allymap-allymap.json ├── testAllyGen.py ├── Outputs │ ├── testCursor.py │ └── testClipping.py ├── baseTest.py └── Pyfakefs │ ├── baseTest.py │ └── AreaItems │ └── testGalaxy.py ├── PyRoute ├── Calculation │ ├── __init__.py │ ├── OwnedWorldCalculation.py │ └── NoneCalculation.py ├── DeltaDebug │ ├── __init__.py │ ├── DeltaLogicError.py │ └── DeltaGalaxy.py ├── StatCalculation │ ├── __init__.py │ ├── UWPCollection.py │ └── Populations.py ├── Fonts │ ├── FreeMono.ttf │ ├── Symbola-hint.ttf │ ├── DejaVuSerifCondensed.ttf │ ├── LiberationMono-Bold.ttf │ ├── ZapfDingbats-Regular.ttf │ └── DejaVuSerifCondensed-Bold.ttf ├── Pathfinding │ ├── minmaxheap.pyx │ ├── setup.py │ ├── minmaxheap.pxd │ ├── LandmarkSchemes │ │ ├── LandmarksWTNExtremes.py │ │ ├── LandmarksQExtremes.py │ │ ├── LandmarksRExtremes.py │ │ ├── LandmarksSExtremes.py │ │ └── LandmarkAvoidHelper.py │ ├── DistanceBase.py │ ├── RouteLandmarkGraph.py │ ├── DistanceGraph.py │ └── single_source_dijkstra.py ├── Errors │ └── MultipleWPopError.py ├── Outputs │ ├── Colour.py │ ├── LightModePDFSectorMap.py │ ├── LightModeGraphicSectorMap.py │ ├── GraphicLine.py │ ├── DarkModePDFSectorMap.py │ ├── Cursor.py │ └── FontLayer.py ├── Inputs │ ├── StarlineTransformer.py │ ├── StarlineStationTransformer.py │ ├── StarlineParser.py │ └── StarlineStationParser.py ├── __init__.py ├── DataClasses │ └── ReadSectorOptions.py ├── templates │ ├── subsectors.wiki │ ├── allegiances.wiki │ ├── sector_data.wiki │ ├── sectors.wiki │ ├── sector_econ.wiki │ └── summary.wiki ├── UWPCodes.py ├── DeltaPasses │ ├── BeyondLineReducer.py │ ├── FullLineReduce.py │ ├── IdentityLineReduce.py │ ├── ImportanceLineReduce.py │ ├── NBZLineReduce.py │ ├── BaseLineReduce.py │ ├── ZoneLineReduce.py │ ├── AuxiliaryLineReduce.py │ ├── CapitalLineReduce.py │ ├── NoblesTrimLineReduce.py │ ├── BaseTrimLineReduce.py │ ├── ZoneTrimLineReduce.py │ ├── TradeCodeLineReduce.py │ ├── PortAndTlLineReduce.py │ ├── TradeCodeTrimLineReduce.py │ ├── Canonicalisation.py │ ├── WidenHoleReducer.py │ └── TwoLineReducer.py ├── Utilities │ ├── UnpackFilename.py │ └── NoNoneDefaultDict.py ├── AreaItems │ ├── AreaItem.py │ └── Subsector.py ├── SystemData │ └── Utilities.py ├── unique_sectors.py ├── Nobles.py └── downloadsec.py ├── deepspace.txt ├── sectorttwlist.txt ├── requirements.txt ├── sectorimplist.txt ├── sectorzholist.txt ├── pytest.ini ├── .gitignore ├── LICENSE ├── pyproject.toml ├── .github └── workflows │ └── python-package.yml ├── sectorlist.txt └── RunSuite.txt /Tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/AreaItems/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/Mapping/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/SystemData/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PyRoute/Calculation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PyRoute/DeltaDebug/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/Pathfinding/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PyRoute/StatCalculation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/ReadSectorsDummy1.sec: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /deepspace.txt: -------------------------------------------------------------------------------- 1 | Reft,2528 2 | Reft,0618 3 | -------------------------------------------------------------------------------- /Tests/TradeMPFiles/trademplist.txt: -------------------------------------------------------------------------------- 1 | Zdiedeiant 2 | Stiatlchepr -------------------------------------------------------------------------------- /Tests/DeltaFiles/ReadSectorsDummy2.sec: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | 4 | # Downtown Woop Woop 5 | # -999, -999 6 | -------------------------------------------------------------------------------- /PyRoute/Fonts/FreeMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/PyRoute/Fonts/FreeMono.ttf -------------------------------------------------------------------------------- /PyRoute/Fonts/Symbola-hint.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/PyRoute/Fonts/Symbola-hint.ttf -------------------------------------------------------------------------------- /PyRoute/Pathfinding/minmaxheap.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | 3 | from minmaxheap cimport MinMaxHeap, dijkstra_t, astar_t 4 | -------------------------------------------------------------------------------- /PyRoute/Fonts/DejaVuSerifCondensed.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/PyRoute/Fonts/DejaVuSerifCondensed.ttf -------------------------------------------------------------------------------- /PyRoute/Fonts/LiberationMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/PyRoute/Fonts/LiberationMono-Bold.ttf -------------------------------------------------------------------------------- /PyRoute/Fonts/ZapfDingbats-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/PyRoute/Fonts/ZapfDingbats-Regular.ttf -------------------------------------------------------------------------------- /PyRoute/Fonts/DejaVuSerifCondensed-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/PyRoute/Fonts/DejaVuSerifCondensed-Bold.ttf -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_coreward_rimward/Ngathksirz Sector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_coreward_rimward/Ngathksirz Sector.pdf -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_spinward_trailing/Knaeleng Sector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_spinward_trailing/Knaeleng Sector.pdf -------------------------------------------------------------------------------- /PyRoute/Errors/MultipleWPopError.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 01, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | 8 | class MultipleWPopError(ValueError): 9 | 10 | pass 11 | -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_quadripoint_trade_write/Deneb Sector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_quadripoint_trade_write/Deneb Sector.pdf -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_quadripoint_trade_write/Corridor Sector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_quadripoint_trade_write/Corridor Sector.pdf -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_quadripoint_trade_write/Provence Sector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_quadripoint_trade_write/Provence Sector.pdf -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_quadripoint_trade_write/Tuglikki Sector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_quadripoint_trade_write/Tuglikki Sector.pdf -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_single_system_border_write/Deneb Sector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_single_system_border_write/Deneb Sector.pdf -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_subsector_xroute_write/Zarushagar Sector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_subsector_xroute_write/Zarushagar Sector.pdf -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_coreward_rimward/Zao Kfeng Ig Grilokh Sector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_coreward_rimward/Zao Kfeng Ig Grilokh Sector.pdf -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_empty_sector_write/Zao Kfeng Ig Grilokh empty.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_empty_sector_write/Zao Kfeng Ig Grilokh empty.pdf -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_spinward_trailing/Zao Kfeng Ig Grilokh Sector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_spinward_trailing/Zao Kfeng Ig Grilokh Sector.pdf -------------------------------------------------------------------------------- /PyRoute/Outputs/Colour.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Union 3 | from typing_extensions import TypeAlias 4 | 5 | 6 | Colour: TypeAlias = Union[str, tuple[float, float, float], tuple[float, float, float, float], None] 7 | -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_subsector_comm_write/Zao Kfeng Ig Grilokh - subsector P - comm.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_subsector_comm_write/Zao Kfeng Ig Grilokh - subsector P - comm.pdf -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_subsector_trade_write/Zao Kfeng Ig Grilokh - subsector P - trade.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makhidkarun/traveller_pyroute/HEAD/Tests/OutputFiles/verify_subsector_trade_write/Zao Kfeng Ig Grilokh - subsector P - trade.pdf -------------------------------------------------------------------------------- /PyRoute/Inputs/StarlineTransformer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Dec 27, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.Inputs.BaseTransformer import BaseTransformer 7 | 8 | 9 | class StarlineTransformer(BaseTransformer): 10 | pass 11 | -------------------------------------------------------------------------------- /PyRoute/Inputs/StarlineStationTransformer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 20, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.Inputs.BaseTransformer import BaseTransformer 7 | 8 | 9 | class StarlineStationTransformer(BaseTransformer): 10 | pass 11 | -------------------------------------------------------------------------------- /PyRoute/DeltaDebug/DeltaLogicError.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Mar 03, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | 8 | class DeltaLogicError(AssertionError): 9 | 10 | @staticmethod 11 | def delta_assert(condition, message): 12 | if not condition: 13 | raise DeltaLogicError(message) 14 | -------------------------------------------------------------------------------- /Tests/TradeMPFiles/run_trademp_parameters: -------------------------------------------------------------------------------- 1 | --borders 2 | erode 3 | --min-btn 4 | 18 5 | --pop-code 6 | fixed 7 | --routes 8 | trade-mp 9 | --max-jump 10 | 2 11 | --route-reuse 12 | 10 13 | --speculative-version 14 | None 15 | --output 16 | Tests/maps 17 | --no-subsector-maps 18 | --input 19 | Tests/TradeMPFiles/ 20 | --sectors 21 | Tests/TradeMPFiles/trademplist.txt -------------------------------------------------------------------------------- /Tests/OutputFiles/verify_subsector_comm_write/comm stars.txt: -------------------------------------------------------------------------------- 1 | 16 17 {'distance': 1, 'weight': 95, 'trade': 500000000000, 'btn': 9, 'count': 1} 2 | 16 18 {'distance': 2, 'weight': 82, 'trade': 500000000000, 'btn': 13, 'count': 1} 3 | 17 18 {'distance': 1, 'weight': 87, 'trade': 500000000000, 'btn': 9, 'count': 1} 4 | 20 26 {'distance': 1, 'weight': 54, 'trade': 500000000000, 'btn': 19, 'count': 1} 5 | -------------------------------------------------------------------------------- /PyRoute/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ["Galaxy", "Star", "SpeculativeTrade", "StatCalculation", "ObjectStatistics"] 3 | 4 | from PyRoute.AreaItems.Galaxy import Galaxy 5 | from PyRoute.SpeculativeTrade import SpeculativeTrade 6 | from PyRoute.Star import Star 7 | from PyRoute.StatCalculation.ObjectStatistics import ObjectStatistics 8 | from PyRoute.StatCalculation.StatCalculation import StatCalculation 9 | -------------------------------------------------------------------------------- /PyRoute/Pathfinding/setup.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from setuptools import setup 3 | from Cython.Build import cythonize 4 | 5 | 6 | setup( 7 | ext_modules=cythonize( 8 | ['astar_numpy.py', 'single_source_dijkstra_core.py', 'ApproximateShortestPathForestUnified.py', 9 | 'minmaxheap.pyx'], 10 | annotate=False 11 | ), 12 | include_dirs=[numpy.get_include()] 13 | ) 14 | -------------------------------------------------------------------------------- /PyRoute/Outputs/LightModePDFSectorMap.py: -------------------------------------------------------------------------------- 1 | 2 | from PyRoute.AreaItems.Galaxy import Galaxy 3 | 4 | from PyRoute.Outputs.PDFMap import PDFMap 5 | from PyRoute.Outputs.SectorMap import SectorMap 6 | 7 | 8 | class LightModePDFSectorMap(PDFMap, SectorMap): 9 | def __init__(self, galaxy: Galaxy, routes: str, output_path: str, writer: str): 10 | super(LightModePDFSectorMap, self).__init__(galaxy, routes, output_path, writer) 11 | -------------------------------------------------------------------------------- /sectorttwlist.txt: -------------------------------------------------------------------------------- 1 | Ktiinz'gat 2 | Mugheen't 3 | Grikr!ng 4 | Ukoarriit!!b 5 | Kring Noor 6 | Mbil!!gh 7 | Ingukrax 8 | Gn'hk'r 9 | Gur 10 | Un'k!!k'ng 11 | Xaagr 12 | Eekrookrigz 13 | Star's End 14 | Gh!hken 15 | Ruupiin 16 | Raakaan 17 | Uuk Sector 18 | Gnaa Iimb'kr 19 | Gateway 20 | Luretiir!girr 21 | X'kug 22 | Kilong 23 | Bar'kakr 24 | Mighabohk 25 | Crucis Margin 26 | Numbis 27 | Gzirr!k'l 28 | K'trekreer 29 | Nuughe 30 | N!!krumbiix 31 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | networkx 2 | inflect 3 | Cython 4 | Pillow >= 10.0 5 | wikitools3==3.0.1 6 | coverage[toml] 7 | hypothesis 8 | jinja2 9 | jsonpickle 10 | lark==1.1.8 11 | mutmut == 3.3.1 12 | mypy 13 | numpy == 2.0.0 14 | pymupdf 15 | pyfakefs 16 | pytest >= 8.0.0 17 | pytest-console-scripts 18 | pytest-randomly 19 | pytest-subtests 20 | pytest-timeout 21 | reportlab >= 4.1.0 22 | ruff >= 0.3.0 23 | setuptools 24 | types-networkx 25 | types-reportlab 26 | typing_extensions 27 | -------------------------------------------------------------------------------- /sectorimplist.txt: -------------------------------------------------------------------------------- 1 | Alpha Crucis 2 | Antares 3 | Core 4 | Corridor 5 | Dagudashaag 6 | Daibei 7 | Dark Nebula 8 | Delphi 9 | Deneb 10 | Diaspora 11 | Empty Quarter 12 | Fornast 13 | Gateway 14 | Glimmerdrift Reaches 15 | Gushemege 16 | Hinterworlds 17 | Ilelish 18 | Ley Sector 19 | Lishun 20 | Magyar 21 | Massilia 22 | Old Expanses 23 | Reaver's Deep 24 | Reft Sector 25 | Riftspan Reaches 26 | Solomani Rim 27 | Spica 28 | Spinward Marches 29 | Trojan Reach 30 | Verge 31 | Vland 32 | Zarushagar 33 | -------------------------------------------------------------------------------- /Tests/StatCalculation/testUWPCollection.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Dec 03 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.StatCalculation.UWPCollection import UWPCollection 7 | from Tests.baseTest import baseTest 8 | 9 | 10 | class testUWPCollection(baseTest): 11 | 12 | def test_setitem_1(self) -> None: 13 | collection = UWPCollection() 14 | 15 | collection['Starport'] = {'A': 1} 16 | actual = collection['Starport'] 17 | self.assertEqual({'A': 1}, actual) 18 | -------------------------------------------------------------------------------- /Tests/StatCalculation/testPopulations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Dec 03, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.StatCalculation.Populations import Populations 7 | from Tests.baseTest import baseTest 8 | 9 | 10 | class testPopulations(baseTest): 11 | 12 | def test_init(self) -> None: 13 | pop = Populations() 14 | self.assertEqual("", pop.code) 15 | self.assertEqual([], pop.homeworlds) 16 | self.assertEqual(0, pop.count) 17 | self.assertEqual(0, pop.population) 18 | -------------------------------------------------------------------------------- /PyRoute/Outputs/LightModeGraphicSectorMap.py: -------------------------------------------------------------------------------- 1 | 2 | from PyRoute.AreaItems.Galaxy import Galaxy 3 | 4 | from PyRoute.Outputs.GraphicMap import GraphicMap 5 | from PyRoute.Outputs.SectorMap import SectorMap 6 | 7 | 8 | class LightModeGraphicSectorMap(GraphicMap, SectorMap): 9 | def __init__(self, galaxy: Galaxy, routes: str, output_path: str, writer: str): 10 | self.input_scale = 4 11 | self.output_scale = 1 12 | 13 | super(LightModeGraphicSectorMap, self).__init__(galaxy, routes, output_path, writer) 14 | -------------------------------------------------------------------------------- /Tests/Inputs/testReadSector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 18, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaDebug.DeltaDictionary import SectorDictionary 7 | from Tests.baseTest import baseTest 8 | 9 | 10 | class testReadSector(baseTest): 11 | 12 | def testReadKilongSector(self) -> None: 13 | sourcefile = self.unpack_filename('DeltaFiles/read_kilong_sector/Kilong.sec') 14 | 15 | sector = SectorDictionary.load_traveller_map_file(sourcefile) 16 | self.assertEqual(518, len(sector.lines), "Unexpected # of lines") 17 | -------------------------------------------------------------------------------- /PyRoute/DataClasses/ReadSectorOptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Apr 04, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from dataclasses import dataclass 7 | 8 | 9 | @dataclass(frozen=True) 10 | class ReadSectorOptions: 11 | sectors: list 12 | pop_code: str = 'scaled' 13 | ru_calc: str = 'scaled' 14 | route_reuse: int = 10 15 | trade_choice: str = 'trade' 16 | route_btn: int = 13 17 | mp_threads: int = 1 18 | debug_flag: bool = False 19 | fix_pop: bool = False 20 | deep_space: dict = None 21 | map_type: str = 'classic' 22 | -------------------------------------------------------------------------------- /Tests/Hypothesis/Inputs/testStarlineTransformer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 02, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | import unittest 8 | 9 | from PyRoute.Inputs.StarlineTransformer import StarlineTransformer 10 | 11 | 12 | class testStarlineTransformer(unittest.TestCase): 13 | def test_boil_down_double_spaces(self) -> None: 14 | original = '{ 2}' 15 | expected = '{ 2}' 16 | actual = StarlineTransformer.boil_down_double_spaces(original) 17 | self.assertEqual(expected, actual) 18 | 19 | 20 | if __name__ == '__main__': 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /PyRoute/templates/subsectors.wiki: -------------------------------------------------------------------------------- 1 | __NOTOC__ 2 | {% import 'statistics_table.wiki' as stats %} 3 | {% for sector in sectors.values() -%} 4 | == {{sector.name}} statistics == 5 | {{ stats.text_area_long('sector', sector, plural) }} 6 | 7 | === Polities === 8 | {{ stats.allegiance_statistics(plural, sector) }} 9 | 10 | {%- for subsector in sector.subsectors.values() | sort (attribute="position") %} 11 | === {{ subsector.subsector_name() }} statistics === 12 | {{ stats.text_area_long('subsector', subsector, plural) }} 13 | 14 | {{ stats.allegiance_statistics(plural, subsector) }} 15 | {%- endfor -%} 16 | {%- endfor -%} 17 | 18 | -------------------------------------------------------------------------------- /sectorzholist.txt: -------------------------------------------------------------------------------- 1 | Stinj Tianz 2 | Bliardlie 3 | Zhiensh 4 | Savria 5 | Datsatl 6 | Gakghang 7 | Thaku Fung 8 | Viajlefliez 9 | Bleblqansh 10 | Driasera 11 | Dalchie Jdatl 12 | Chit Boshti 13 | Ghoekhnael 14 | Ksinanirz 15 | Brieplanz 16 | Sidiadl 17 | Zdiedeiant 18 | Stiatlchepr 19 | Itvikiastaf 20 | Knoellighz 21 | Dhuerorrg 22 | Pliabriebl 23 | Eiaplial 24 | Zhdant 25 | Tienspevnekr 26 | Ziafrplians 27 | Gvurrdon 28 | Tuglikki 29 | Tsadra Davr 30 | Tsadra 31 | Yiklerzdanzh 32 | Far Frontiers 33 | Spinward Marches 34 | Deneb 35 | Chiep Zhez 36 | Astron 37 | Fulani 38 | Vanguard Reaches 39 | The Beyond 40 | Trojan Reach 41 | Reft Sector 42 | -------------------------------------------------------------------------------- /Tests/Utilities/testNoNoneDefaultDict.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyRoute.Utilities.NoNoneDefaultDict import NoNoneDefaultDict 4 | 5 | 6 | class testNoNoneDefaultDict(unittest.TestCase): 7 | def test_reject_none_key(self) -> None: 8 | foo = NoNoneDefaultDict(int) 9 | exc = None 10 | 11 | try: 12 | foo[None] += 1 13 | except ValueError as e: 14 | exc = e 15 | 16 | self.assertTrue(isinstance(exc, ValueError), "ValueError not raised") 17 | self.assertEqual("Supplied key must not be NoneType", exc.args[0]) 18 | 19 | 20 | if __name__ == '__main__': 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /Tests/testDeltaDebug.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from Tests.baseTest import baseTest 5 | from pytest_console_scripts import ScriptRunner 6 | 7 | 8 | class testDeltaDebug(baseTest): 9 | 10 | def test_delta_debug_command_empty_args(self) -> None: 11 | fullpath = self.unpack_filename('../PyRoute/DeltaDebug/DeltaDebug.py') 12 | 13 | cwd = os.getcwd() 14 | runner = ScriptRunner(launch_mode="inprocess", rootdir=cwd) 15 | 16 | foo = runner.run(command=fullpath) 17 | 18 | self.assertEqual(0, foo.returncode, "DeltaDebug did not complete successfully: " + foo.stderr) 19 | 20 | 21 | if __name__ == '__main__': 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /Tests/Dummy/DeltaDebug/DummyReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Oct 09, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaDebug.DeltaReduce import DeltaReduce 7 | 8 | 9 | class DummyReduce(DeltaReduce): 10 | 11 | def __init__(self, sectors, args, interesting_line=None, interesting_type=None): 12 | super().__init__(sectors, args, interesting_line=interesting_line, interesting_type=interesting_type) 13 | self.jam_interesting = None 14 | 15 | def _check_interesting(self, args, sectors): 16 | if self.jam_interesting is not None: 17 | return self.jam_interesting, "", None 18 | 19 | return DeltaReduce._check_interesting(args, sectors) 20 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = test* 3 | python_classes = test* 4 | pythonpath = 5 | ./ 6 | 7 | testpaths = 8 | Tests/**.py 9 | Tests/Allies/**.py 10 | Tests/AreaItems/**.py 11 | Tests/Calculation/**.py 12 | Tests/DeltaDictionary/**.py 13 | Tests/Hypothesis/**.py 14 | Tests/Hypothesis/Inputs/**.py 15 | Tests/Hypothesis/Position/**.py 16 | Tests/Inputs/**.py 17 | Tests/Mapping/**.py 18 | Tests/Outputs/**.py 19 | Tests/Pathfinding/**.py 20 | Tests/Pathfinding/Schemas/**.py 21 | Tests/Position/**.py 22 | Tests/StatCalculation/**.py 23 | Tests/SystemData/**.py 24 | Tests/Utilities/**.py 25 | Tests/Pyfakefs/AreaItems/**.py 26 | -------------------------------------------------------------------------------- /PyRoute/Outputs/GraphicLine.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Sep 12, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PIL import ImageColor # type: ignore 7 | 8 | from PyRoute.Outputs.Cursor import Cursor 9 | 10 | 11 | class GraphicLine(object): 12 | def __init__(self, image, lineStart: Cursor, lineEnd: Cursor, colorname: str, width: int): 13 | self.image = image 14 | self.lineStart: Cursor = lineStart 15 | self.lineEnd: Cursor = lineEnd 16 | self.color = ImageColor.getrgb(colorname) 17 | self.width = max(1, int(width)) 18 | 19 | def _draw(self): 20 | self.image.line([self.lineStart.as_tuple(), self.lineEnd.as_tuple()], self.color, 21 | self.width) 22 | -------------------------------------------------------------------------------- /PyRoute/Pathfinding/minmaxheap.pxd: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # Adapted from https://github.com/kilian-gebhardt/MinMaxHeap 3 | 4 | from libcpp.vector cimport vector 5 | 6 | cdef extern from "_minmaxheap.h" namespace "minmaxheap": 7 | cdef struct astar_t: 8 | double augment; 9 | double dist; 10 | int curnode; 11 | int parent; 12 | 13 | cdef struct dijkstra_t: 14 | double act_wt; 15 | int act_nod; 16 | 17 | cdef cppclass MinMaxHeap[T]: 18 | MinMaxHeap() 19 | # MinMaxHeap(size_t reserve) 20 | # short level(size_t n) 21 | size_t size() 22 | void insert(T key) 23 | void clear() 24 | T peekmin() 25 | T peekmax() 26 | T popmin() 27 | T popmax() 28 | void reserve(size_t n) 29 | vector[T] getheap() 30 | -------------------------------------------------------------------------------- /PyRoute/Outputs/DarkModePDFSectorMap.py: -------------------------------------------------------------------------------- 1 | 2 | from PyRoute.AreaItems.Galaxy import Galaxy 3 | 4 | from PyRoute.Outputs.PDFMap import PDFMap 5 | from PyRoute.Outputs.SectorMap import SectorMap 6 | 7 | 8 | class DarkModePDFSectorMap(PDFMap, SectorMap): 9 | def __init__(self, galaxy: Galaxy, routes: str, output_path: str, writer: str): 10 | super(DarkModePDFSectorMap, self).__init__(galaxy, routes, output_path, writer) 11 | self.colours['background'] = 'black' 12 | self.colours['sector'] = 'white' 13 | self.colours['system_port'] = (242, 242, 242) 14 | self.colours['system_uwp'] = (242, 242, 242) 15 | self.colours['system_name'] = (242, 242, 242) 16 | self.colours['base code'] = (242, 242, 242) 17 | -------------------------------------------------------------------------------- /PyRoute/StatCalculation/UWPCollection.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Dec 03, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from collections import OrderedDict 7 | 8 | from PyRoute.UWPCodes import UWPCodes 9 | from PyRoute.StatCalculation.ObjectStatistics import ObjectStatistics 10 | 11 | 12 | class UWPCollection(object): 13 | def __init__(self): 14 | self.uwp = OrderedDict() 15 | for uwpCode in UWPCodes.uwpCodes: 16 | self.uwp[uwpCode] = {} 17 | 18 | def stats(self, code, value) -> ObjectStatistics: 19 | return self.uwp[code].setdefault(value, ObjectStatistics()) 20 | 21 | def __getitem__(self, index): 22 | return self.uwp[index] 23 | 24 | def __setitem__(self, index, value): 25 | self.uwp[index] = value 26 | -------------------------------------------------------------------------------- /PyRoute/UWPCodes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Dec 15, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from collections import OrderedDict 7 | 8 | 9 | class UWPCodes(object): 10 | uwpCodes = ['Starport', 11 | 'Size', 12 | 'Atmosphere', 13 | 'Hydrographics', 14 | 'Population', 15 | 'Government', 16 | 'Law Level', 17 | 'Tech Level', 18 | 'Pop Code', 19 | 'Starport Size', 20 | 'Primary Type', 21 | 'Importance', 22 | 'Resources'] 23 | 24 | def __init__(self): 25 | self.codes = OrderedDict() 26 | for uwpCode in UWPCodes.uwpCodes: 27 | self.codes[uwpCode] = "X" 28 | -------------------------------------------------------------------------------- /PyRoute/templates/allegiances.wiki: -------------------------------------------------------------------------------- 1 | {% import 'statistics_table.wiki' as stats %} 2 | 3 | == Allegiances == 4 | {| class="wikitable sortable" width="90%" 5 | |+ Analysis per Polity 6 | ! Name !! Code !! Worlds !! Population (millions) !! % of Population !! Economy (Bcr) !! Per Capita (Cr) !! RU !! Trade Volume (BCr / year) !! Int. Trade (BCr / year) !! Int. Trade (MDton / year) !! Ext. Trade (BCr/year) !! Ext. Trade (MDton / year) !! Shipyard Capacity (MTons) !! Colonial Army (BEs) !! Travellers (M / year) !! SPA Pop 7 | {% for alleg in global_alg if alleg.world_count() >= min_alg_count -%} 8 | |- 9 | | {{alleg.wiki_name()}} 10 | | {{alleg.code}} 11 | {{ stats.stats_table(alleg.stats, global_stats) }} 12 | {%- endfor %} 13 | |} 14 | 15 | {{ stats.allegiance_statistics(plural, area) }} 16 | -------------------------------------------------------------------------------- /Tests/SystemData/testUtilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Dec 15, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | import unittest 7 | 8 | from PyRoute.SystemData.Utilities import Utilities 9 | 10 | 11 | class testUtilities(unittest.TestCase): 12 | 13 | def test_ehex_to_int_bad_value(self) -> None: 14 | msg = None 15 | try: 16 | Utilities.ehex_to_int(None) 17 | except ValueError as e: 18 | msg = str(e) 19 | self.assertEqual("Value must be string", msg) 20 | 21 | def test_int_to_ehex_bad_value(self) -> None: 22 | msg = None 23 | try: 24 | Utilities.int_to_ehex(None) 25 | except ValueError as e: 26 | msg = str(e) 27 | self.assertEqual("Value must be integer", msg) 28 | -------------------------------------------------------------------------------- /PyRoute/Pathfinding/LandmarkSchemes/LandmarksWTNExtremes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Feb 19, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | 8 | class LandmarksWTNExtremes: 9 | 10 | def __init__(self, galaxy): 11 | self.galaxy = galaxy 12 | 13 | def get_landmarks(self, index=False) -> list[dict]: 14 | result = [] 15 | 16 | result.append(dict()) 17 | for component_id in self.galaxy.trade.components: 18 | stars = [item for item in self.galaxy.star_mapping.values() if component_id == item.component] 19 | source = max(stars, key=lambda item: item.wtn) 20 | if index: 21 | result[0][component_id] = source.index 22 | else: 23 | result[0][component_id] = source 24 | 25 | return result 26 | -------------------------------------------------------------------------------- /Tests/AreaItems/testAreaItem.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Nov 27, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | import unittest 8 | 9 | from PyRoute.AreaItems.AreaItem import AreaItem 10 | 11 | 12 | class testAreaItem(unittest.TestCase): 13 | def test_init(self) -> None: 14 | foo = AreaItem('foobar') 15 | self.assertEqual('foobar', foo.name) 16 | self.assertEqual([], foo.worlds) 17 | self.assertEqual({}, foo.alg) 18 | self.assertEqual([], foo.alg_sorted) 19 | self.assertFalse(foo.debug_flag) 20 | self.assertEqual('foobar', str(foo)) 21 | 22 | def test_is_well_formed(self) -> None: 23 | foo = AreaItem('foobar') 24 | well_formed, msg = foo.is_well_formed() 25 | self.assertTrue(well_formed) 26 | self.assertEqual("", msg) 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | PyRoute/*.pyc 3 | PyRoute/DeltaDebug/*.pyc 4 | PyRoute/Pathfinding/*.pyc 5 | PyRoute/Calculation/*.pyc 6 | PyRoute/Position/*.pyc 7 | PyRoute/DeltaPasses/*.pyc 8 | Tests/*.pyc 9 | PyRoute/*.class 10 | PyRoute/DeltaDebug/*.class 11 | PyRoute/Pathfinding/*.class 12 | PyRoute/Position/*.class 13 | Tests/*.class 14 | Tests/.pytest_cache/ 15 | 16 | # ignore venvs 17 | venv/ 18 | venv2/ 19 | 20 | # ignore IDE-related detritus 21 | .idea/ 22 | .project 23 | .pydevproject 24 | .settings/ 25 | .hypothesis/ 26 | 27 | # ignore downloaded/generated files 28 | sectors/ 29 | maps*/ 30 | diffs/ 31 | test_data/ 32 | mutants/ 33 | PyRoute/Pathfinding/*.c 34 | PyRoute/Pathfinding/*.cpp 35 | PyRoute/Pathfinding/*.cpython* 36 | PyRoute/Pathfinding/*.html 37 | b*/ 38 | c*/ 39 | d*/ 40 | D*/ 41 | f*/ 42 | k*/ 43 | n*/ 44 | p*/ 45 | s*/ 46 | t*/ 47 | .coverage 48 | *.log 49 | *.wiki 50 | *.png 51 | *.lprof 52 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/BeyondLineReducer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Apr 05, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaDebug.DeltaDictionary import DeltaDictionary 7 | from PyRoute.DeltaPasses.WidenHoleReducer import WidenHoleReducer 8 | 9 | 10 | class BeyondLineReducer(object): 11 | 12 | def __init__(self, reducer): 13 | from PyRoute.DeltaDebug.DeltaReduce import DeltaReduce 14 | self.reducer: DeltaReduce = reducer 15 | self.breacher = WidenHoleReducer(reducer) 16 | 17 | def preflight(self) -> bool: 18 | return self.reducer is not None and self.reducer.sectors is not None and 0 < len(self.reducer.sectors.lines) 19 | 20 | def write_files(self, sectors=None) -> None: 21 | if isinstance(sectors, DeltaDictionary): 22 | sectors.write_files(self.reducer.args.mindir) 23 | else: 24 | self.reducer.sectors.write_files(self.reducer.args.mindir) 25 | -------------------------------------------------------------------------------- /PyRoute/templates/sector_data.wiki: -------------------------------------------------------------------------------- 1 |
2 | {| class="wikitable sortable" width="90%" 3 | |+ {{sector.sector_name()}} sector data 4 | !Hex!!Name!!UWP!!Remarks!!{Ix}!!(Ex)!![Cx]!!Noble!!Base!!Zone!!PBG!!Worlds!!Alg!!Stellar
5 | {%- for subsector in sector.subsectors.values() -%} 6 |
7 | {%- for world in subsector.worlds %} 8 | |- 9 | | {{ world.position }} 10 | | [[{{ world.wiki_short_name() }}]] 11 | | {{ world.uwp }} 12 | | {{ world.tradeCode }} 13 | | { {{world.importance }} } 14 | | {{ world. economics }} 15 | | {{ world.social }} 16 | | {{ world.nobles }} 17 | | {{ world.baseCode }} 18 | | {{ world.zone }} 19 | | {{ world.popM}}{{world.belts}}{{world.ggCount}} 20 | | {{ world.worlds }} 21 | | {{ world.alg }} 22 | | {{ world.stars }} 23 | {% endfor -%} 24 |
25 | {%- endfor %} 26 | |} 27 | [[Category: Sectors with sector data]] 28 | -------------------------------------------------------------------------------- /PyRoute/Utilities/UnpackFilename.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 19, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | import os 8 | 9 | 10 | class UnpackFilename(object): 11 | 12 | @staticmethod 13 | def unpack_filename(filename: str) -> str: 14 | # try unpacked filename directly 15 | sourcefile = os.path.abspath(filename) 16 | if not os.path.isfile(sourcefile): 17 | sourcefile = os.path.abspath('../' + filename) 18 | if not os.path.isfile(sourcefile): 19 | sourcefile = os.path.abspath('Tests/' + filename) 20 | if not os.path.isfile(sourcefile): 21 | sourcefile = os.path.abspath('../Tests/' + filename) 22 | if not os.path.isfile(sourcefile): 23 | sourcefile = os.path.abspath('Tests/Tests/' + filename) 24 | if not os.path.isfile(sourcefile): 25 | raise BaseException(sourcefile + " not mapped to a file") 26 | 27 | return sourcefile 28 | -------------------------------------------------------------------------------- /PyRoute/AreaItems/AreaItem.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 21 Jul, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.StatCalculation.ObjectStatistics import ObjectStatistics 7 | 8 | 9 | class AreaItem(object): 10 | def __init__(self, name): 11 | self.name = name 12 | self.worlds = [] 13 | self.stats = ObjectStatistics() 14 | self.alg = {} 15 | self.alg_sorted = [] 16 | self._wiki_name = '[[{}]]'.format(name) 17 | self.debug_flag = False 18 | 19 | def wiki_title(self) -> str: 20 | return self.wiki_name() 21 | 22 | def wiki_name(self) -> str: 23 | return self._wiki_name 24 | 25 | def __str__(self): 26 | return self.name 27 | 28 | def world_count(self) -> int: 29 | return len(self.worlds) 30 | 31 | def __getitem__(self, item): 32 | return getattr(self, item) 33 | 34 | def __setitem__(self, key, value): 35 | setattr(self, key, value) 36 | 37 | def is_well_formed(self) -> tuple[bool, str]: 38 | return True, "" 39 | -------------------------------------------------------------------------------- /PyRoute/Utilities/NoNoneDefaultDict.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Nov 30, 2023 3 | 4 | @author: CyberiaResurrection 5 | 6 | An extension of defaultdict that, as the name says, loudly refuses to accept NoneType keys. 7 | """ 8 | from collections import defaultdict 9 | 10 | 11 | class NoNoneDefaultDict(defaultdict): 12 | 13 | def __init__(self, default_factory=None): 14 | if default_factory is None: 15 | raise ValueError("Supplied factory must not be NoneType") 16 | super(NoNoneDefaultDict, self).__init__(default_factory) 17 | 18 | def __eq__(self, other) -> bool: 19 | if not isinstance(other, NoNoneDefaultDict): 20 | return False 21 | if self.default_factory != other.default_factory: 22 | return False 23 | return super().__eq__(other) 24 | 25 | def __hash__(self) -> int: 26 | return 0 27 | 28 | def __missing__(self, key): 29 | if key is None: 30 | raise ValueError("Supplied key must not be NoneType") 31 | return super().__missing__(key) 32 | -------------------------------------------------------------------------------- /PyRoute/Pathfinding/LandmarkSchemes/LandmarksQExtremes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Feb 19, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | 8 | class LandmarksQExtremes: 9 | 10 | def __init__(self, galaxy): 11 | self.galaxy = galaxy 12 | 13 | def get_landmarks(self, index=False) -> list[dict]: 14 | result = [] 15 | 16 | result.append(dict()) 17 | result.append(dict()) 18 | for component_id in self.galaxy.trade.components: 19 | stars = [item for item in self.galaxy.star_mapping.values() if component_id == item.component] 20 | source = max(stars, key=lambda item: item.hex.q) 21 | if index: 22 | result[0][component_id] = source.index 23 | else: 24 | result[0][component_id] = source 25 | 26 | source = min(stars, key=lambda item: item.hex.q) 27 | if index: 28 | result[1][component_id] = source.index 29 | else: 30 | result[1][component_id] = source 31 | 32 | return result 33 | -------------------------------------------------------------------------------- /PyRoute/Pathfinding/LandmarkSchemes/LandmarksRExtremes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Feb 19, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | 8 | class LandmarksRExtremes: 9 | 10 | def __init__(self, galaxy): 11 | self.galaxy = galaxy 12 | 13 | def get_landmarks(self, index=False) -> list[dict]: 14 | result = [] 15 | 16 | result.append(dict()) 17 | result.append(dict()) 18 | for component_id in self.galaxy.trade.components: 19 | stars = [item for item in self.galaxy.star_mapping.values() if component_id == item.component] 20 | source = max(stars, key=lambda item: item.hex.r) 21 | if index: 22 | result[0][component_id] = source.index 23 | else: 24 | result[0][component_id] = source 25 | 26 | source = min(stars, key=lambda item: item.hex.r) 27 | if index: 28 | result[1][component_id] = source.index 29 | else: 30 | result[1][component_id] = source 31 | 32 | return result 33 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/FullLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jun 13, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class FullLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with full line reduction" 14 | self.start_msg = "Commencing full within-line reduction" 15 | # build substitution list - reduce _everything_ 16 | subs_list = [] 17 | segment = [] 18 | for line in self.reducer.sectors.lines: 19 | canon = DeltaStar.reduce_all(line) 20 | assert isinstance(canon, 21 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 22 | # Skip already-reduced lines 23 | if line.startswith(canon): 24 | continue 25 | subs_list.append((line, canon)) 26 | segment.append(line) 27 | return segment, subs_list 28 | -------------------------------------------------------------------------------- /PyRoute/Pathfinding/LandmarkSchemes/LandmarksSExtremes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Feb 19, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | 8 | class LandmarksSExtremes: 9 | 10 | def __init__(self, galaxy): 11 | self.galaxy = galaxy 12 | 13 | def get_landmarks(self, index=False) -> list[dict]: 14 | result = [] 15 | 16 | result.append(dict()) 17 | result.append(dict()) 18 | for component_id in self.galaxy.trade.components: 19 | stars = [item for item in self.galaxy.star_mapping.values() if component_id == item.component] 20 | source = max(stars, key=lambda item: -item.hex.q - item.hex.r) 21 | if index: 22 | result[0][component_id] = source.index 23 | else: 24 | result[0][component_id] = source 25 | 26 | source = min(stars, key=lambda item: -item.hex.q - item.hex.r) 27 | if index: 28 | result[1][component_id] = source.index 29 | else: 30 | result[1][component_id] = source 31 | 32 | return result 33 | -------------------------------------------------------------------------------- /PyRoute/Calculation/OwnedWorldCalculation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Aug 09, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.Allies.AllyGen import AllyGen 7 | from PyRoute.Calculation.RouteCalculation import RouteCalculation 8 | 9 | 10 | class OwnedWorldCalculation(RouteCalculation): 11 | def __init__(self, galaxy): 12 | super(OwnedWorldCalculation, self).__init__(galaxy) 13 | 14 | # Pure HG Base jump distance cost 15 | distance_weight = [0, 30, 50, 75, 130, 230, 490] 16 | 17 | def generate_routes(self) -> None: 18 | self.generate_base_routes() 19 | pass 20 | 21 | def calculate_routes(self) -> None: 22 | pass 23 | 24 | def base_route_filter(self, star, neighbor) -> bool: 25 | return not AllyGen.are_owned_allies(star.alg_code, neighbor.alg_code) 26 | 27 | def base_range_routes(self, star, neighbor) -> int: 28 | return star.distance(neighbor) 29 | 30 | def route_weight(self, star, target) -> float: 31 | dist = star.distance(target) 32 | weight = self.distance_weight[dist] 33 | return weight 34 | -------------------------------------------------------------------------------- /Tests/testBase.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import tempfile 3 | import unittest 4 | 5 | 6 | class testBase(unittest.TestCase): 7 | def _make_args(self) -> argparse.ArgumentParser: 8 | args = argparse.ArgumentParser(description='PyRoute input minimiser.') 9 | args.btn = 8 10 | args.max_jump = 2 11 | args.route_btn = 13 12 | args.pop_code = 'scaled' 13 | args.ru_calc = 'scaled' 14 | args.routes = 'trade' 15 | args.route_reuse = 10 16 | args.interestingline = "Weight of edge" 17 | args.interestingtype = None 18 | args.maps = None 19 | args.borders = 'range' 20 | args.ally_match = 'collapse' 21 | args.owned = False 22 | args.trade = True 23 | args.speculative_version = 'CT' 24 | args.ally_count = 10 25 | args.json_data = False 26 | args.output = tempfile.gettempdir() 27 | return args 28 | 29 | def _make_args_no_line(self) -> argparse.ArgumentParser: 30 | args = self._make_args() 31 | args.interestingline = None 32 | 33 | return args 34 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/IdentityLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 04, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class IdentityLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with identity reduction" 14 | self.start_msg = "Commencing identity within-line reduction" 15 | 16 | subs_list = [] 17 | segment = [] 18 | num_lines = len(self.reducer.sectors.lines) 19 | for line in self.reducer.sectors.lines: 20 | canon = DeltaStar.reduce(line) 21 | assert isinstance(canon, 22 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 23 | # Skip already-reduced lines 24 | if 2 < num_lines and line.startswith(canon): 25 | continue 26 | subs_list.append((line, canon)) 27 | segment.append(line) 28 | return segment, subs_list 29 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/ImportanceLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jul 14, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class ImportanceLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with importance line reduction" 14 | self.start_msg = "Commencing importance-related within-line reduction" 15 | # build substitution list - reduce _everything_ 16 | subs_list = [] 17 | segment = [] 18 | for line in self.reducer.sectors.lines: 19 | canon = DeltaStar.reduce_importance(line) 20 | assert isinstance(canon, 21 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 22 | # Skip already-reduced lines 23 | if line.startswith(canon): 24 | continue 25 | subs_list.append((line, canon)) 26 | segment.append(line) 27 | return segment, subs_list 28 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/border_blowup_3/Raakaan.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2024-09-28T17:24:06-07:00 3 | 4 | # Raakaan 5 | # 6,1 6 | 7 | # Name: Raakaan (kk) 8 | 9 | # Abbreviation: Raak 10 | 11 | # Milieu: M1105 12 | 13 | # Credits: Raakaan sector was designed by Jim Kundert. 14 | 15 | # Author: Jim Kundert 16 | 17 | # Subsector A: Miataatrat 18 | # Subsector B: Rarroo'ghat 19 | # Subsector C: Teeghaa 20 | # Subsector D: Gn'koorr 21 | # Subsector E: Aar'krex 22 | # Subsector F: U'keeghan 23 | # Subsector G: Xtulak' 24 | # Subsector H: Graakrakook' 25 | # Subsector I: Kaxuut 26 | # Subsector J: Urr'ka 27 | # Subsector K: Rrrut! 28 | # Subsector L: Atarate 29 | # Subsector M: Karaxt!m 30 | # Subsector N: Tari 31 | # Subsector O: Kariake 32 | # Subsector P: Nak'grar 33 | 34 | # Alleg: Kk: "The Two Thousand Worlds" 35 | 36 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 37 | ---- -------------------- --------- -------------------- ---- ---- ---- - - - --- - -- ------- 38 | 1109 ?537???-? - - - ?24 Kk 39 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/no_subsectors_named/Knaeleng empty.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2023-12-23T00:50:36-08:00 3 | 4 | # Knaeleng 5 | # -1,4 6 | 7 | # Name: Knaeleng (va) 8 | 9 | # Abbreviation: Knae 10 | 11 | # Milieu: M1105 12 | 13 | # Author: John G. Wood 14 | # Source: Underdeveloped Sectors 15 | # Ref: https://web.archive.org/web/20160205040546/http://homepage.ntlworld.com/elvwood/Traveller/Sectors/ 16 | 17 | # Subsector A: 18 | # Subsector B: 19 | # Subsector C: 20 | # Subsector D: 21 | # Subsector E: 22 | # Subsector F: 23 | # Subsector G: 24 | # Subsector H: 25 | # Subsector I: 26 | # Subsector J: 27 | # Subsector K: 28 | # Subsector L: 29 | # Subsector M: 30 | # Subsector N: 31 | # Subsector O: 32 | # Subsector P: 33 | 34 | # Alleg: Va: "Non-Aligned, Vargr-dominated" 35 | # Alleg: VA: "Alliance of Tju" 36 | # Alleg: Ve: "Empire of Varroerth" 37 | # Alleg: VK: "Koenotz Empire" 38 | 39 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 40 | ---- -------------------- --------- -------------------- ---- ---- ---- - -- - --- - -- ------------------------ 41 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/NBZLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Apr 03, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class NBZLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with nbz reduction" 14 | self.start_msg = "Commencing nbz within-line reduction" 15 | # build substitution list - reduce _everything_ 16 | subs_list = [] 17 | segment = [] 18 | num_lines = len(self.reducer.sectors.lines) 19 | for line in self.reducer.sectors.lines: 20 | canon = DeltaStar.reduce_nbz(line) 21 | assert isinstance(canon, 22 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 23 | # Skip already-reduced lines 24 | if 2 < num_lines and line.startswith(canon): 25 | continue 26 | subs_list.append((line, canon)) 27 | segment.append(line) 28 | return segment, subs_list 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Thomas Jones-Low 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PyRoute/Calculation/NoneCalculation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Aug 09, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.Allies.AllyGen import AllyGen 7 | from PyRoute.Calculation.RouteCalculation import RouteCalculation 8 | 9 | 10 | class NoneCalculation(RouteCalculation): 11 | def __init__(self, galaxy): 12 | super(NoneCalculation, self).__init__(galaxy) 13 | 14 | # Pure HG Base jump distance cost 15 | distance_weight = [0, 30, 50, 75, 130, 230, 490] 16 | 17 | def generate_routes(self) -> None: 18 | # self.generate_base_routes() 19 | pass 20 | 21 | def calculate_routes(self) -> None: 22 | pass 23 | 24 | def base_route_filter(self, star, neighbor) -> bool: 25 | return not AllyGen.are_owned_allies(star.alg_code, neighbor.alg_code) 26 | 27 | def base_range_routes(self, star, neighbor) -> int: 28 | return star.distance(neighbor) 29 | 30 | def route_weight(self, star, target) -> float: 31 | dist = star.distance(target) 32 | weight = self.distance_weight[dist] 33 | return weight 34 | 35 | def cross_check_totals(self) -> None: 36 | pass 37 | -------------------------------------------------------------------------------- /Tests/DeltaDictionary/testAllegianceSubset.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Apr 23, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaDebug.DeltaDictionary import SectorDictionary, DeltaDictionary 7 | from PyRoute.Inputs.ParseStarInput import ParseStarInput 8 | from Tests.baseTest import baseTest 9 | 10 | 11 | class testAllegianceSubset(baseTest): 12 | 13 | def setUp(self): 14 | ParseStarInput.deep_space = {} 15 | 16 | def test_blowup_1(self) -> None: 17 | source = self.unpack_filename('DeltaFiles/allegiance_subset_blowup_1/Reaver\'s Deep.sec') 18 | 19 | delta = DeltaDictionary() 20 | for src in [source]: 21 | sector = SectorDictionary.load_traveller_map_file(src) 22 | delta[sector.name] = sector 23 | result, msg = delta.is_well_formed() 24 | self.assertTrue(result, msg) 25 | 26 | allegiances = delta.allegiance_list() 27 | nu_allegiances = allegiances.copy() 28 | nu_allegiances.remove("CaTe") 29 | 30 | nu_delta = delta.allegiance_subset(nu_allegiances) 31 | result, msg = nu_delta.is_well_formed() 32 | self.assertTrue(result, msg) 33 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/BaseLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 06, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class BaseLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with base reduction" 14 | self.start_msg = "Commencing base within-line reduction" 15 | 16 | # build substitution list - reduce _everything_ 17 | subs_list = [] 18 | segment = [] 19 | num_lines = len(self.reducer.sectors.lines) 20 | for line in self.reducer.sectors.lines: 21 | canon = DeltaStar.reduce(line, drop_base_codes=True) 22 | assert isinstance(canon, 23 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 24 | # Skip already-reduced lines 25 | if 2 < num_lines and line.startswith(canon): 26 | continue 27 | subs_list.append((line, canon)) 28 | segment.append(line) 29 | return segment, subs_list 30 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/ZoneLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 07, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class ZoneLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with zone reduction" 14 | self.start_msg = "Commencing zone within-line reduction" 15 | 16 | # build substitution list - reduce _everything_ 17 | subs_list = [] 18 | segment = [] 19 | num_lines = len(self.reducer.sectors.lines) 20 | for line in self.reducer.sectors.lines: 21 | canon = DeltaStar.reduce(line, drop_trade_zone=True) 22 | assert isinstance(canon, 23 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 24 | # Skip already-reduced lines 25 | if 2 < num_lines and line.startswith(canon): 26 | continue 27 | subs_list.append((line, canon)) 28 | segment.append(line) 29 | return segment, subs_list 30 | -------------------------------------------------------------------------------- /PyRoute/templates/sectors.wiki: -------------------------------------------------------------------------------- 1 | {% import 'statistics_table.wiki' as stats %} 2 | 3 | == Sectors == 4 | {| class="wikitable sortable" width="90%" 5 | |+ Analysis per Sector 6 | !Sector!! X,Y !! Worlds !! Population (millions) !! % of Population !! Economy (Bcr) !! Per Capita (Cr) !! RU !! Trade Volume (BCr / year) !! Int. Trade (BCr / year) !! Int. Trade (MDton / year) !! Ext. Trade (BCr/year) !! Ext. Trade (MDton / year) !! Shipyard Capacity (MTons) !! Colonial Army (BEs) !! Travellers (M / year) !! SPA Pop 7 | {% for sector in sectors.values() -%} 8 | |- 9 | | {{sector.wiki_name()}} 10 | | {{sector.x}},{{sector.y}} 11 | {{ stats.stats_table(sector.stats, global_stats) }} 12 | {%- endfor -%} 13 | {%- if im_stats %} 14 | |- 15 | | Imperial Totals || 16 | {{ stats.stats_table(im_stats.stats, global_stats) }} 17 | {%- endif -%} 18 | |- 19 | | Global Totals || 20 | {{ stats.stats_table(global_stats, global_stats) }} 21 | |} 22 | 23 | 24 | {% for sector in sectors.values() -%} 25 | 26 | == {{sector.name}} statistics == 27 | {{ stats.text_area_long('sector', sector, plural) }} 28 | 29 | === Polities === 30 | {{ stats.allegiance_statistics(plural, sector) }} 31 | {%- endfor -%} 32 | 33 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/AuxiliaryLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jul 13, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class AuxiliaryLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with auxiliary line reduction" 14 | self.start_msg = "Commencing auxiliary within-line reduction" 15 | # build substitution list - reduce _everything_ 16 | subs_list = [] 17 | segment = [] 18 | num_lines = len(self.reducer.sectors.lines) 19 | for line in self.reducer.sectors.lines: 20 | canon = DeltaStar.reduce_auxiliary(line) 21 | assert isinstance(canon, 22 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 23 | # Skip already-reduced lines 24 | if 2 < num_lines and line.startswith(canon): 25 | continue 26 | subs_list.append((line, canon)) 27 | segment.append(line) 28 | return segment, subs_list 29 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/CapitalLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jul 17, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class CapitalLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with capital line reduction" 14 | self.start_msg = "Commencing capital within-line reduction" 15 | # build substitution list - reduce _everything_ 16 | subs_list = [] 17 | segment = [] 18 | num_lines = len(self.reducer.sectors.lines) 19 | for line in self.reducer.sectors.lines: 20 | canon = DeltaStar.reduce(line, reset_capitals=True) 21 | assert isinstance(canon, 22 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 23 | # Skip already-reduced lines 24 | if 2 < num_lines and line.startswith(canon): 25 | continue 26 | subs_list.append((line, canon)) 27 | segment.append(line) 28 | return segment, subs_list 29 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/NoblesTrimLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 05, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class NoblesTrimLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with nobles trim" 14 | self.start_msg = "Commencing nobles trim within-line reduction" 15 | 16 | # build substitution list - reduce _everything_ 17 | subs_list = [] 18 | segment = [] 19 | num_lines = len(self.reducer.sectors.lines) 20 | for line in self.reducer.sectors.lines: 21 | canon = DeltaStar.reduce(line, trim_noble_codes=True) 22 | assert isinstance(canon, 23 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 24 | # Skip already-reduced lines 25 | if 2 < num_lines and line.startswith(canon): 26 | continue 27 | subs_list.append((line, canon)) 28 | segment.append(line) 29 | return segment, subs_list 30 | -------------------------------------------------------------------------------- /PyRoute/StatCalculation/Populations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Dec 03, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.Star import Star 7 | 8 | 9 | class Populations(object): 10 | def __init__(self) -> None: 11 | self.code = "" 12 | self.homeworlds: list[Star] = [] 13 | self.count = 0 14 | self.population = 0 15 | 16 | def add_population(self, population, homeworld) -> None: 17 | self.count += 1 18 | self.population += population 19 | if homeworld: 20 | self.homeworlds.append(homeworld) 21 | 22 | def __lt__(self, other): 23 | return self.population < other.population 24 | 25 | def __eq__(self, other): 26 | if self.__hash__() != other.__hash__(): 27 | return False 28 | if self.code != other.code: 29 | return False 30 | if self.count != other.count: 31 | return False 32 | if self.population != other.population: 33 | return False 34 | return self.homeworlds == other.homeworlds 35 | 36 | def __hash__(self) -> int: 37 | key = (self.code, self.count, self.population) 38 | return hash(key) 39 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/BaseTrimLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 08, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class BaseTrimLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with base trim reduction" 14 | self.start_msg = "Commencing base trim within-line reduction" 15 | 16 | # build substitution list - reduce _everything_ 17 | subs_list = [] 18 | segment = [] 19 | num_lines = len(self.reducer.sectors.lines) 20 | for line in self.reducer.sectors.lines: 21 | canon = DeltaStar.reduce(line, trim_base_codes=True) 22 | assert isinstance(canon, 23 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 24 | # Skip already-reduced lines 25 | if 2 < num_lines and line.startswith(canon): 26 | continue 27 | subs_list.append((line, canon)) 28 | segment.append(line) 29 | return segment, subs_list 30 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/ZoneTrimLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 07, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class ZoneTrimLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with zone trim reduction" 14 | self.start_msg = "Commencing zone trim within-line reduction" 15 | 16 | # build substitution list - reduce _everything_ 17 | subs_list = [] 18 | segment = [] 19 | num_lines = len(self.reducer.sectors.lines) 20 | for line in self.reducer.sectors.lines: 21 | canon = DeltaStar.reduce(line, trim_trade_zone=True) 22 | assert isinstance(canon, 23 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 24 | # Skip already-reduced lines 25 | if 2 < num_lines and line.startswith(canon): 26 | continue 27 | subs_list.append((line, canon)) 28 | segment.append(line) 29 | return segment, subs_list 30 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/TradeCodeLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 04, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class TradeCodeLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with trade-code reduction" 14 | self.start_msg = "Commencing trade-code within-line reduction" 15 | 16 | # build substitution list - reduce _everything_ 17 | subs_list = [] 18 | segment = [] 19 | num_lines = len(self.reducer.sectors.lines) 20 | for line in self.reducer.sectors.lines: 21 | canon = DeltaStar.reduce(line, drop_trade_codes=True) 22 | assert isinstance(canon, 23 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 24 | # Skip already-reduced lines 25 | if 2 < num_lines and line.startswith(canon): 26 | continue 27 | subs_list.append((line, canon)) 28 | segment.append(line) 29 | return segment, subs_list 30 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/PortAndTlLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 07, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class PortAndTlLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with port and TL reduction" 14 | self.start_msg = "Commencing port and TL within-line reduction" 15 | 16 | # build substitution list - reduce _everything_ 17 | subs_list = [] 18 | segment = [] 19 | num_lines = len(self.reducer.sectors.lines) 20 | for line in self.reducer.sectors.lines: 21 | canon = DeltaStar.reduce(line, reset_port=True, reset_tl=True) 22 | assert isinstance(canon, 23 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 24 | # Skip already-reduced lines 25 | if 2 < num_lines and line.startswith(canon): 26 | continue 27 | subs_list.append((line, canon)) 28 | segment.append(line) 29 | return segment, subs_list 30 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/TradeCodeTrimLineReduce.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 05, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.WithinLineReducer import WithinLineReducer 7 | from PyRoute.DeltaStar import DeltaStar 8 | 9 | 10 | class TradeCodeTrimLineReduce(WithinLineReducer): 11 | 12 | def _build_subs_list(self): 13 | self.full_msg = "Reduction found with trade codes trim" 14 | self.start_msg = "Commencing trade codes trim within-line reduction" 15 | 16 | # build substitution list - reduce _everything_ 17 | subs_list = [] 18 | segment = [] 19 | num_lines = len(self.reducer.sectors.lines) 20 | for line in self.reducer.sectors.lines: 21 | canon = DeltaStar.reduce(line, trim_trade_codes=True) 22 | assert isinstance(canon, 23 | str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 24 | # Skip already-reduced lines 25 | if 2 < num_lines and line.startswith(canon): 26 | continue 27 | subs_list.append((line, canon)) 28 | segment.append(line) 29 | return segment, subs_list 30 | -------------------------------------------------------------------------------- /Tests/testNobles.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 1, 2019 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | import unittest 8 | 9 | from PyRoute.Nobles import Nobles 10 | 11 | 12 | class TestNobles(unittest.TestCase): 13 | def testDefaultString(self) -> None: 14 | nobles = Nobles() 15 | expected = '-' 16 | self.assertEqual(expected, nobles.__str__()) 17 | 18 | def testStringWithOneViscount(self) -> None: 19 | nobles = Nobles() 20 | nobles.nobles['Viscounts'] = 1 21 | 22 | expected = 'e' 23 | self.assertEqual(expected, nobles.__str__()) 24 | 25 | def testCountWithViscount(self) -> None: 26 | nobles = Nobles() 27 | nobles.count(['e']) 28 | 29 | expected = 1 30 | actual = nobles.nobles['Viscounts'] 31 | 32 | self.assertEqual(expected, actual) 33 | 34 | def testAccumulateSelf(self) -> None: 35 | nobles = Nobles() 36 | nobles.nobles['Viscounts'] = 1 37 | 38 | nobles.accumulate(nobles) 39 | 40 | expected = 2 41 | actual = nobles.nobles['Viscounts'] 42 | 43 | self.assertEqual(expected, actual) 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /Tests/Hypothesis/testNobles.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from hypothesis import given, assume, settings, HealthCheck 4 | from hypothesis.strategies import text 5 | 6 | from PyRoute.Nobles import Nobles 7 | 8 | 9 | class testNobles(unittest.TestCase): 10 | 11 | @given(text(min_size=1, max_size=20, alphabet='ABcCDEfFGHI')) 12 | @settings(suppress_health_check=[HealthCheck(10)]) 13 | def test_nobles_creation(self, noble_line) -> None: 14 | hyp_line = "Hypothesis input: " + noble_line 15 | foo = Nobles() 16 | foo.count(noble_line) 17 | 18 | result, msg = foo.is_well_formed() 19 | self.assertTrue(result, msg + '. ' + hyp_line) 20 | 21 | @given(text(min_size=1, max_size=6, alphabet='ABcCDEfFGHI')) 22 | @settings(suppress_health_check=[HealthCheck(10)]) 23 | def test_nobles_str_round_trip(self, noble_line) -> None: 24 | hyp_line = "Hypothesis input: " + noble_line 25 | foo = Nobles() 26 | foo.count(noble_line) 27 | assume(0 < foo.sum_value) 28 | 29 | str_rep = str(foo) 30 | 31 | nu_foo = Nobles() 32 | nu_foo.count(str_rep) 33 | 34 | nu_rep = str(nu_foo) 35 | self.assertEqual(str_rep, nu_rep, hyp_line) 36 | -------------------------------------------------------------------------------- /PyRoute/templates/sector_econ.wiki: -------------------------------------------------------------------------------- 1 |
2 | {| class="wikitable sortable" width="90%" 3 | |+ {{ sector.sector_name() }} economic data 4 | !Hex!!Name!!UWP!!PBG!!{Ix}!!WTN!!MSPR!!RU!!Per Capita (Cr)!!GWP (BCr)!!Trade (MCr/year) 5 | !Passengers (per year)!!Build!!Army!!Port!!SPA Population 6 | !Trade Goods!!Subsector
7 | {%- for subsector in sector.subsectors.values() -%} 8 |
9 | {%- for world in subsector.worlds %} 10 | |- 11 | | {{ world.position }} 12 | | {{ "[[:s|]]".format(world.wiki_short_name()) }} 13 | | {{ world.uwp }} 14 | | {{ world.popM }}{{world.belts}}{{world.ggCount}} 15 | | { {{world.importance}} } 16 | | {{world.wtn}} 17 | | {{world.mspr}} 18 | | {{"{:,d}".format(world.ru_int)}} 19 | | {{"{:,d}".format(world.perCapita)}} 20 | | {{"{:,d}".format(world.gwp)}} 21 | | {{"{:,d}".format(world.tradeIn // 10000000) }} 22 | | {{"{:,d}".format(world.passIn) }} 23 | | {{"{:,d}".format(world.ship_capacity) }} 24 | | {{"{:,d}".format(world.raw_be) }} 25 | | {{ world.starportSize }} 26 | | {{"{:,d}".format(world.starportPop) }} 27 | | {{ world.trade_id }} 28 | | {{ subsector.wiki_name() }} 29 | {% endfor -%} 30 |
31 | {%- endfor %} 32 | |} 33 | 34 | [[Category: Sectors with sector economic data]] 35 | -------------------------------------------------------------------------------- /PyRoute/templates/summary.wiki: -------------------------------------------------------------------------------- 1 | {% import 'statistics_table.wiki' as stats %} 2 | == Top Level Summary == 3 | {{ stats.top_level_summary(global_stats, sectors) }} 4 | 5 | {| 6 | |- style="vertical-align:top;text-align:center;" 7 | | 8 | {{ stats.uwp_table ('Starport', uwp.uwp['Starport'], global_stats) }} 9 | | 10 | {{ stats.uwp_table ('Size', uwp.uwp['Size'], global_stats) }} 11 | | 12 | {{ stats.uwp_table ('Atmosphere', uwp.uwp['Atmosphere'], global_stats) }} 13 | | 14 | {{ stats.uwp_table ('Hydrographics', uwp.uwp['Hydrographics'], global_stats) }} 15 | |- style="vertical-align:top;text-align:center;" 16 | | 17 | {{ stats.uwp_table ('Population', uwp.uwp['Population'], global_stats) }} 18 | | 19 | {{ stats.uwp_table ('Government', uwp.uwp['Government'], global_stats) }} 20 | | 21 | {{ stats.uwp_table ('Law Level', uwp.uwp['Law Level'], global_stats) }} 22 | | 23 | {{ stats.uwp_table ('Tech Level', uwp.uwp['Tech Level'], global_stats) }} 24 | |- style="vertical-align:top;text-align:center;" 25 | | 26 | {{ stats.uwp_table ('Importance', uwp.uwp['Importance'], global_stats) }} 27 | | 28 | {{ stats.uwp_table ('Resources', uwp.uwp['Resources'], global_stats) }} 29 | | 30 | {{ stats.uwp_table ('Starport Size', uwp.uwp['Starport Size'], global_stats) }} 31 | | 32 | {{ stats.uwp_table ('Primary Type', uwp.uwp['Primary Type'], global_stats) }} 33 | |} 34 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/sector_subset_blowup_on_vland_empty/Vland.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:36:38-08:00 3 | 4 | # Vland 5 | # -1,1 6 | 7 | # Name: Vland 8 | 9 | # Abbreviation: Vlan 10 | 11 | # Milieu: M1105 12 | 13 | # Credits: Vland sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Joe D. Fugate Sr. and appears in The Travellers Digest and The MegaTraveller Alien, Volume 1: Vilani & Vargr (DGP, 1990) 14 | 15 | # Source: Traveller 5 Second Survey 16 | 17 | # Subsector A: Voskhod 18 | # Subsector B: Vhodan 19 | # Subsector C: Anarsi 20 | # Subsector D: Theton 21 | # Subsector E: Lalaki Kharir 22 | # Subsector F: Kagamira 23 | # Subsector G: Vland 24 | # Subsector H: Shiigus 25 | # Subsector I: Dusa 26 | # Subsector J: Akumid 27 | # Subsector K: Kasear 28 | # Subsector L: Anakod 29 | # Subsector M: Parsi 30 | # Subsector N: Daangiilu 31 | # Subsector O: Nulisud 32 | # Subsector P: Kakadan 33 | 34 | # Alleg: CsIm: "Client state, Third Imperium" 35 | 36 | 37 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 38 | ---- -------------------- --------- ------------------------- ------ ------- ------ ---- -- - --- -- ---- --------------- --------------------------------------- -------------------------------------------------------------------------------- /Tests/DeltaFiles/border_blowup_2/Zdiedeiant.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2024-09-28T17:30:10-07:00 3 | 4 | # Zdiedeiant 5 | # -7,3 6 | 7 | # Name: Zdiedeiant (zh) 8 | 9 | # Abbreviation: Zdie 10 | 11 | # Milieu: M1105 12 | 13 | # Credits: Zdiedeiant sector was designed by P-O Bergstedt. 14 | 15 | # Author: P-O Bergstedt 16 | # Source: The Zhodani Base 17 | # Ref: http://zho.berka.com/data/CLASSIC/sector.pl?sector=ZDIEDEIA 18 | 19 | # Subsector A: Iefla 20 | # Subsector B: Qliamria 21 | # Subsector C: Kletsshavri 22 | # Subsector D: Jetlta 23 | # Subsector E: Krietieqatl 24 | # Subsector F: A'letl 25 | # Subsector G: Kretlshtiazh 26 | # Subsector H: Etsia 27 | # Subsector I: Leo 28 | # Subsector J: Adliev 29 | # Subsector K: Sadliavl 30 | # Subsector L: Tlabqlel 31 | # Subsector M: Qrtlievl 32 | # Subsector N: Ieprsidr 33 | # Subsector O: Abrrnzh 34 | # Subsector P: Tliekrbliepl 35 | 36 | # Alleg: Na: "Non-aligned" 37 | # Alleg: Dr: "Droyne" 38 | 39 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 40 | ---- -------------------- --------- -------------------- ---- ---- ---- - -- - --- - ---- ------- 41 | 0332 Plit-79 X252000-0 Ba Po - - R 020 Na 42 | 0432 Uevro C3515X6-9 Ni Po - - R 322 Dr 43 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/border_blowup_1/Zdiedeiant.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2024-09-28T17:30:10-07:00 3 | 4 | # Zdiedeiant 5 | # -7,3 6 | 7 | # Name: Zdiedeiant (zh) 8 | 9 | # Abbreviation: Zdie 10 | 11 | # Milieu: M1105 12 | 13 | # Credits: Zdiedeiant sector was designed by P-O Bergstedt. 14 | 15 | # Author: P-O Bergstedt 16 | # Source: The Zhodani Base 17 | # Ref: http://zho.berka.com/data/CLASSIC/sector.pl?sector=ZDIEDEIA 18 | 19 | # Subsector A: Iefla 20 | # Subsector B: Qliamria 21 | # Subsector C: Kletsshavri 22 | # Subsector D: Jetlta 23 | # Subsector E: Krietieqatl 24 | # Subsector F: A'letl 25 | # Subsector G: Kretlshtiazh 26 | # Subsector H: Etsia 27 | # Subsector I: Leo 28 | # Subsector J: Adliev 29 | # Subsector K: Sadliavl 30 | # Subsector L: Tlabqlel 31 | # Subsector M: Qrtlievl 32 | # Subsector N: Ieprsidr 33 | # Subsector O: Abrrnzh 34 | # Subsector P: Tliekrbliepl 35 | 36 | # Alleg: Dr: "Droyne" 37 | # Alleg: ZhIa: "Zhodani Consulate, Iabrensh Province" 38 | 39 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 40 | ---- -------------------- --------- -------------------- ---- ---- ---- - -- - --- - ---- ------- 41 | 0332 Plit-79 X252000-0 Ba Po - - R 020 ZhIa 42 | 0432 Uevro C3515X6-9 Ni Po - - R 322 Dr 43 | -------------------------------------------------------------------------------- /Tests/PathfindingFiles/single_source_distances_ibara_subsector_from_0101.json: -------------------------------------------------------------------------------- 1 | {"Engaki (Zarushagar 0502)": 175.0, "Strela (Zarushagar 0407)": 210.0, "Ymirial (Zarushagar 0106)": 193.0, 2 | "San Nuska Kilna (Zarushagar 0108)": 239.0, "Zed (Zarushagar 0109)": 263.0, "Imled (Zarushagar 0406)": 190.0, 3 | "Dugemaa (Zarushagar 0503)": 174.0, "Leniv Fenl (Zarushagar 0504)": 176.0, "Villenuve (Zarushagar 0507)": 213.0, 4 | "Toulon-Cadiz (Zarushagar 0510)": 302.0, "Aslungi (Zarushagar 0605)": 191.0, "Apkenumir (Zarushagar 0606)": 273.0, 5 | "Miller's World (Zarushagar 0607)": 234.0, "Norsec (Zarushagar 0110)": 285.0, "Allis (Zarushagar 0204)": 145.0, 6 | "Selsinia (Zarushagar 0201)": 54.0, "Arxani (Zarushagar 0209)": 283.0, "Point Zulu (Zarushagar 0302)": 74.0, 7 | "Makkus (Zarushagar 0304)": 119.0, "Freeport (Zarushagar 0305)": 145.0, "Woden (Zarushagar 0306)": 165.0, 8 | "Ichiban (Zarushagar 0309)": 255.0, "Shadishi (Zarushagar 0310)": 283.0, "Rogreis (Zarushagar 0402)": 101.0, 9 | "Bolten (Zarushagar 0404)": 144.0, "Ulaun (Zarushagar 0405)": 191.0, "Nedadzia (Zarushagar 0701)": 277.0, 10 | "Ledadzia (Zarushagar 0704)": 239.0, "Dorevann (Zarushagar 0708)": 313.0, "New Orlando (Zarushagar 0710)": 403.0, 11 | "Airvae (Zarushagar 0801)": 305.0, "Sufizi (Zarushagar 0802)": 281.0, "Cyrpus (Zarushagar 0803)": 261.0, 12 | "Gishin (Zarushagar 0804)": 236.0, "Ginshe (Zarushagar 0805)": 263.0, "Madagast (Zarushagar 0806)": 239.0} -------------------------------------------------------------------------------- /Tests/DeltaFiles/insufficient_exhaust_value/Core.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2024-09-28T17:11:16-07:00 3 | 4 | # Core 5 | # 0,0 6 | 7 | # Name: Core 8 | # Name: Ukan (vi) 9 | 10 | # Abbreviation: Core 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Core sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). 15 | 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Apge 19 | # Subsector B: Perite 20 | # Subsector C: Ameros 21 | # Subsector D: Shinkan 22 | # Subsector E: Sanches 23 | # Subsector F: Mekee 24 | # Subsector G: Core 25 | # Subsector H: Kaskii 26 | # Subsector I: Bunkeria 27 | # Subsector J: Cemplas 28 | # Subsector K: Chant 29 | # Subsector L: Dingtra 30 | # Subsector M: Cadion 31 | # Subsector N: Ch'naar 32 | # Subsector O: Dunea 33 | # Subsector P: Saregon 34 | 35 | # Alleg: ImDc: "Third Imperium, Domain of Sylea" 36 | 37 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 38 | ---- -------------------- --------- ------------------------------------- ------ ------- ------ ----- -- - --- -- ---- --------------- 39 | 1108 Uumeaxan B765995-C Hi Ga Pr Ht { 4 } (G8F+2) [7D3A] BcEf NS - 414 11 ImDc M2 V 40 | 1109 Uuruun Kuu C987346-8 Lo { -2 } (A20-3) [2147] B - - 114 10 ImDc M0 V 41 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/no_subsectors_named/Ngathksirz empty.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2023-12-23T00:53:40-08:00 3 | 4 | # Ngathksirz 5 | # -2,3 6 | 7 | # Name: Ngathksirz 8 | 9 | # Abbreviation: Ngat 10 | 11 | # Milieu: M1105 12 | 13 | # Credits: Ngathksirz sector was designed by John G. Wood. Stellar positions were developed by Joe D. Fugate Sr. and appear in The MegaTraveller Alien, Volume 1: Vilani & Vargr (Digest Group Publications, 1990). 14 | 15 | # Author: John G. Wood 16 | # Source: Underdeveloped Sectors 17 | # Ref: https://web.archive.org/web/20160205040546/http://homepage.ntlworld.com/elvwood/Traveller/Sectors/ 18 | 19 | # Subsector A: Odzgoegh 20 | # Subsector B: Kfirzin 21 | # Subsector C: Kueroe 22 | # Subsector D: Aengangvuer 23 | # Subsector E: Dhaendzouts 24 | # Subsector F: Ruevoga 25 | # Subsector G: Aenael 26 | # Subsector H: Roenvoegazdu 27 | # Subsector I: Zarrkoe 28 | # Subsector J: Thafo 29 | # Subsector K: Uengou 30 | # Subsector L: Raez 31 | # Subsector M: Vaerr 32 | # Subsector N: Nouksaezor 33 | # Subsector O: Oegerag 34 | # Subsector P: Serrken 35 | 36 | # Alleg: Va: "Non-Aligned, Vargr-dominated" 37 | # Alleg: Ve: "Empire of Varroerth" 38 | 39 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 40 | ---- ------------------------------ --------- ----------------------- ---- ------- ------ - - - --- -- -- -------------------------- 41 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/no_subsectors_named/Zao Kfeng Ig Grilokh empty.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:37:18-08:00 3 | 4 | # Zao Kfeng Ig Grilokh 5 | # -2,4 6 | 7 | # Name: Zao Kfeng Ig Grilokh (va) 8 | 9 | # Abbreviation: Zaok 10 | 11 | # Milieu: M1105 12 | 13 | # Credits: Zao Kfeng Ig Grilokh sector was designed by John G. Wood and WG Zeist. Stellar positions were developed by Joe D. Fugate Sr. and appear in The MegaTraveller Alien, Volume 1: Vilani & Vargr (Digest Group Publications, 1990). 14 | 15 | # Author: John G. Wood, WG Zeist 16 | # Source: Underdeveloped Sectors 17 | # Ref: https://web.archive.org/web/20160205040546/http://homepage.ntlworld.com/elvwood/Traveller/Sectors/ 18 | 19 | # Subsector A: 20 | # Subsector B: 21 | # Subsector C: 22 | # Subsector D: 23 | # Subsector E: 24 | # Subsector F: 25 | # Subsector G: 26 | # Subsector H: 27 | # Subsector I: 28 | # Subsector J: 29 | # Subsector K: 30 | # Subsector L: 31 | # Subsector M: 32 | # Subsector N: 33 | # Subsector O: 34 | # Subsector P: 35 | 36 | # Alleg: Va: "Non-Aligned, Vargr-dominated" 37 | # Alleg: Ve: "Empire of Varroerth" 38 | # Alleg: VF: "Far Stars Unanimity" 39 | # Alleg: VK: "Koenotz Empire" 40 | # Alleg: VS: "Jarrgh Subjugate" 41 | 42 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 43 | ---- -------------------- --------- -------------------- ---- ---- ---- - -- - --- - -- ---------------- ------ 44 | -------------------------------------------------------------------------------- /PyRoute/Pathfinding/DistanceBase.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Feb 25, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | import numpy as np 7 | from networkx.classes import Graph 8 | 9 | 10 | class DistanceBase: 11 | 12 | def __init__(self, graph: Graph): 13 | raw_nodes = graph.nodes() 14 | self._nodes = list(raw_nodes) 15 | num_nodes = len(self._nodes) 16 | self._indexes = {node: i for (i, node) in enumerate(self._nodes)} 17 | positions = [ 18 | raw_nodes[u]['star'].hex.hex_position() for u in self._nodes 19 | ] 20 | self._positions = np.zeros((num_nodes, 2), dtype=int) 21 | for i in range(num_nodes): 22 | self._positions[i, :] = np.array(positions[i]) 23 | 24 | def __len__(self) -> int: 25 | return len(self._nodes) 26 | 27 | def lighten_edge(self, u: int, v: int, weight: float) -> None: 28 | raise NotImplementedError("Base Class") 29 | 30 | def distances_from_target(self, active_nodes, target: int) -> int: 31 | if len(active_nodes) == len(self): 32 | dq = self._positions[:, 0] - self._positions[target, 0] 33 | dr = self._positions[:, 1] - self._positions[target, 1] 34 | else: 35 | dq = self._positions[active_nodes, 0] - self._positions[target, 0] 36 | dr = self._positions[active_nodes, 1] - self._positions[target, 1] 37 | 38 | return (abs(dq) + abs(dr) + abs(dq + dr)) // 2 39 | 40 | def _lighten_arc(self, u: int, v: int, weight: float) -> None: 41 | self._arcs[u][1][self._arcs[u][0] == v] = weight 42 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/single_system_border/Deneb.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2023-12-23T00:43:40-08:00 3 | 4 | # Deneb 5 | # -3,1 6 | 7 | # Name: Deneb 8 | # Name: Nieklsdia (zh) 9 | 10 | # Abbreviation: Dene 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Deneb sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by James Holden. Portions appear in The Travellers Digest and The MegaTraveller Journal #3 (Digest Group Publications, 1992). 15 | 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Pretoria 19 | # Subsector B: Lamas 20 | # Subsector C: Antra 21 | # Subsector D: Million 22 | # Subsector E: Sabine 23 | # Subsector F: Inar 24 | # Subsector G: Dunmag 25 | # Subsector H: Atsah 26 | # Subsector I: Star Lane 27 | # Subsector J: Vincennes 28 | # Subsector K: Usani 29 | # Subsector L: Geniishir 30 | # Subsector M: Gulf 31 | # Subsector N: Zeng 32 | # Subsector O: Kamlar 33 | # Subsector P: Vast Heavens 34 | 35 | # Alleg: CsIm: "Client state, Third Imperium" 36 | # Alleg: ImDd: "Third Imperium, Domain of Deneb" 37 | # Alleg: NaHu: "Non-Aligned, Human-dominated" 38 | # Alleg: NaVa: "Non-Aligned, Vargr-dominated" 39 | # Alleg: VAug: "United Followers of Augurgh" 40 | # Alleg: VDzF: "Dzarrgh Federate" 41 | 42 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 43 | ---- -------------------- --------- -------------------------------- ------ ------- ------ ---- -- - --- -- ---- -------------- 44 | 1018 Hessel B658100-C Lo Ht { 1 } (401-3) [1217] B N - 801 12 ImDd F4 V K4 V 45 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/zao_kfeng_jump_4_template_blowup/Zao Kfeng Ig Grilokh.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2023-11-02T04:28:16-07:00 3 | 4 | # Zao Kfeng Ig Grilokh 5 | # -2,4 6 | 7 | # Name: Zao Kfeng Ig Grilokh (va) 8 | 9 | # Abbreviation: Zaok 10 | 11 | # Milieu: M1105 12 | 13 | # Credits: Zao Kfeng Ig Grilokh sector was designed by John G. Wood and WG Zeist. Stellar positions were developed by Joe D. Fugate Sr. and appear in The MegaTraveller Alien, Volume 1: Vilani & Vargr (Digest Group Publications, 1990). 14 | 15 | # Author: John G. Wood, WG Zeist 16 | # Source: Underdeveloped Sectors 17 | # Ref: https://web.archive.org/web/20160205040546/http://homepage.ntlworld.com/elvwood/Traveller/Sectors/ 18 | 19 | # Subsector A: 20 | # Subsector B: 21 | # Subsector C: 22 | # Subsector D: 23 | # Subsector E: 24 | # Subsector F: 25 | # Subsector G: 26 | # Subsector H: 27 | # Subsector I: 28 | # Subsector J: 29 | # Subsector K: 30 | # Subsector L: 31 | # Subsector M: 32 | # Subsector N: 33 | # Subsector O: 34 | # Subsector P: 35 | 36 | # Alleg: Va: "Non-Aligned, Vargr-dominated" 37 | # Alleg: Ve: "Empire of Varroerth" 38 | # Alleg: VF: "Far Stars Unanimity" 39 | # Alleg: VK: "Koenotz Empire" 40 | # Alleg: VS: "Jarrgh Subjugate" 41 | 42 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 43 | ---- -------------------- --------- -------------------- ---- ---- ---- - -- - --- - -- ---------------- 44 | 2739 Sueuvorrerr C000848-8 As Na - C - 404 Ve D 45 | 2835 Uekhngaeng C211888-5 Ic Na - C - 900 Ve F5 V 46 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/high_pop_worlds_blowup/Spinward Marches.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:33:42-08:00 3 | 4 | # Spinward Marches 5 | # -4,1 6 | 7 | # Name: Spinward Marches 8 | # Name: Tloql (zh) 9 | 10 | # Abbreviation: Spin 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: The Spinward Marches were designed by Marc W. Miller and appear in Traveller Supplement 3: The Spinward Marches (GDW, 1979). 15 | 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Cronor 19 | # Subsector B: Jewell 20 | # Subsector C: Regina 21 | # Subsector D: Aramis 22 | # Subsector E: Querion 23 | # Subsector F: Vilis 24 | # Subsector G: Lanth 25 | # Subsector H: Rhylanor 26 | # Subsector I: Darrian 27 | # Subsector J: Sword Worlds 28 | # Subsector K: Lunion 29 | # Subsector L: Mora 30 | # Subsector M: Five Sisters 31 | # Subsector N: District 268 32 | # Subsector O: Glisten 33 | # Subsector P: Trin's Veil 34 | 35 | # Alleg: CsIm: "Client state, Third Imperium" 36 | # Alleg: CsZh: "Client state, Zhodani Consulate" 37 | # Alleg: DaCf: "Darrian Confederation" 38 | # Alleg: ImDd: "Third Imperium, Domain of Deneb" 39 | # Alleg: NaHu: "Non-Aligned, Human-dominated" 40 | # Alleg: NaXX: "Non-Aligned, unclaimed" 41 | # Alleg: SwCf: "Sword Worlds Confederation" 42 | # Alleg: ZhIN: "Zhodani Consulate, Iadr Nsobl Province" 43 | 44 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 45 | ---- -------------------- --------- ---------------------------------------- ------ ------- ------ ----- -- - --- -- ---- -------------- --------------------------------- 46 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/no_subsectors_named/Zao Kfeng Ig Grilokh - subsector P - trimmed.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:37:18-08:00 3 | 4 | # Zao Kfeng Ig Grilokh 5 | # -2,4 6 | 7 | # Name: Zao Kfeng Ig Grilokh (va) 8 | 9 | # Abbreviation: Zaok 10 | 11 | # Milieu: M1105 12 | 13 | # Credits: Zao Kfeng Ig Grilokh sector was designed by John G. Wood and WG Zeist. Stellar positions were developed by Joe D. Fugate Sr. and appear in The MegaTraveller Alien, Volume 1: Vilani & Vargr (Digest Group Publications, 1990). 14 | 15 | # Author: John G. Wood, WG Zeist 16 | # Source: Underdeveloped Sectors 17 | # Ref: https://web.archive.org/web/20160205040546/http://homepage.ntlworld.com/elvwood/Traveller/Sectors/ 18 | 19 | # Subsector A: 20 | # Subsector B: 21 | # Subsector C: 22 | # Subsector D: 23 | # Subsector E: 24 | # Subsector F: 25 | # Subsector G: 26 | # Subsector H: 27 | # Subsector I: 28 | # Subsector J: 29 | # Subsector K: 30 | # Subsector L: 31 | # Subsector M: 32 | # Subsector N: 33 | # Subsector O: 34 | # Subsector P: 35 | 36 | # Alleg: Va: "Non-Aligned, Vargr-dominated" 37 | # Alleg: Ve: "Empire of Varroerth" 38 | # Alleg: VF: "Far Stars Unanimity" 39 | # Alleg: VK: "Koenotz Empire" 40 | # Alleg: VS: "Jarrgh Subjugate" 41 | 42 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 43 | ---- -------------------- --------- -------------------- ---- ---- ---- - -- - --- - -- ---------------- ------ 44 | 2633 Khaenukh E400559-8 Ni Va - C - 502 Va G4 V 45 | 2734 Afousoerr C456975-5 Hi In - K - 803 Va F0 V 46 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/Dagudashaag-star-object-no-sector-attribute.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:21:52-08:00 3 | 4 | # Dagudashaag 5 | # -1,0 6 | 7 | # Name: Dagudashaag 8 | # Name: Dagudashag 9 | 10 | # Abbreviation: Dagu 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Dagudashaag sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Duncan Law-Green, Leighton Piper and Jae Campbell, and appears in Signal GK Magazine. 15 | 16 | # Author: Duncan Law-Green, Leighton Piper and Jae Campbell; Martin J. Dougherty 17 | # Source: Traveller 5 Second Survey 18 | 19 | # Subsector A: Mimu 20 | # Subsector B: Old Suns 21 | # Subsector C: Arnakhish 22 | # Subsector D: Iiradu 23 | # Subsector E: Shallows 24 | # Subsector F: Ushra 25 | # Subsector G: Khandi 26 | # Subsector H: Kuriishe 27 | # Subsector I: Zeda 28 | # Subsector J: Remnants 29 | # Subsector K: Pact 30 | # Subsector L: Gadde 31 | # Subsector M: Bolivar 32 | # Subsector N: Argi 33 | # Subsector O: Sapphyre 34 | # Subsector P: Laraa 35 | 36 | # Alleg: ImAp: "Third Imperium, Amec Protectorate" 37 | # Alleg: ImDv: "Third Imperium, Domain of Vland" 38 | # Alleg: ImLc: "Third Imperium, Lancian Cultural Region" 39 | 40 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 41 | ---- -------------------- --------- ------------------------------------- ------ ------- ------ ---- -- - --- -- ---- --------------- ----------------------------------------- 42 | 3240 Saven A5A0734-D He { 2 } (D6D+1) [593B] B N - 613 11 ImDv G2 IV Xb:3139 Xb:Core-0139 Xb:Zaru-3202 43 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/stat_calc_division_by_zero_population/Dagudashaag-zero.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:21:52-08:00 3 | 4 | # Dagudashaag 5 | # -1,0 6 | 7 | # Name: Dagudashaag 8 | # Name: Dagudashag 9 | 10 | # Abbreviation: Dagu 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Dagudashaag sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Duncan Law-Green, Leighton Piper and Jae Campbell, and appears in Signal GK Magazine. 15 | 16 | # Author: Duncan Law-Green, Leighton Piper and Jae Campbell; Martin J. Dougherty 17 | # Source: Traveller 5 Second Survey 18 | 19 | # Subsector A: Mimu 20 | # Subsector B: Old Suns 21 | # Subsector C: Arnakhish 22 | # Subsector D: Iiradu 23 | # Subsector E: Shallows 24 | # Subsector F: Ushra 25 | # Subsector G: Khandi 26 | # Subsector H: Kuriishe 27 | # Subsector I: Zeda 28 | # Subsector J: Remnants 29 | # Subsector K: Pact 30 | # Subsector L: Gadde 31 | # Subsector M: Bolivar 32 | # Subsector N: Argi 33 | # Subsector O: Sapphyre 34 | # Subsector P: Laraa 35 | 36 | # Alleg: ImAp: "Third Imperium, Amec Protectorate" 37 | # Alleg: ImDv: "Third Imperium, Domain of Vland" 38 | # Alleg: ImLc: "Third Imperium, Lancian Cultural Region" 39 | 40 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 41 | ---- -------------------- --------- ------------------------------------- ------ ------- ------ ---- -- - --- -- ---- --------------- ----------------------------------------- 42 | 2123 Kediiga B778411-8 Ni Pa { -1 } (832-5) [1314] Bc - - 920 9 ImDv G6 V 43 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/stars_node_types/Antares.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:20:28-08:00 3 | 4 | # Antares 5 | # 1,1 6 | 7 | # Name: Antares 8 | # Name: Mikasirka (vi) 9 | 10 | # Abbreviation: Anta 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Antares sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). 15 | 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Pelusium 19 | # Subsector B: Ninik 20 | # Subsector C: Dartho 21 | # Subsector D: Oulduktak 22 | # Subsector E: Shurlarlem 23 | # Subsector F: Sarar 24 | # Subsector G: Gimgir 25 | # Subsector H: Kiirkandin 26 | # Subsector I: Urunishu 27 | # Subsector J: Uradnim 28 | # Subsector K: Antares 29 | # Subsector L: Mukusi 30 | # Subsector M: Urgusap 31 | # Subsector N: Gaakish 32 | # Subsector O: Sakhag 33 | # Subsector P: Celebes 34 | 35 | # Alleg: CsIm: "Client state, Third Imperium" 36 | # Alleg: ImDa: "Third Imperium, Domain of Antares" 37 | # Alleg: ImLa: "Third Imperium, League of Antares" 38 | # Alleg: JuRu: "Julian Protectorate, Rukadukaz Republic" 39 | # Alleg: NaHu: "Non-Aligned, Human-dominated" 40 | # Alleg: NaVa: "Non-Aligned, Vargr-dominated" 41 | 42 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 43 | ---- -------------------- --------- ------------------------- ------ ------- ------ ---- -- - --- -- ---- -------------- ------------------------------------ 44 | 0823 Oerchgwe E4228A6-7 He Na Po Ph Pi { -2 } (A76-3) [7646] BDe - - 223 13 ImDa G6 V 45 | 0824 Wyshamshen C999657-6 Ni { -2 } (852-2) [6456] B S - 602 13 ImDa K4 V 46 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/xroute_routes_pass_1_2/Core.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2024-09-28T17:11:16-07:00 3 | 4 | # Core 5 | # 0,0 6 | 7 | # Name: Core 8 | # Name: Ukan (vi) 9 | 10 | # Abbreviation: Core 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Core sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). 15 | 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Apge 19 | # Subsector B: Perite 20 | # Subsector C: Ameros 21 | # Subsector D: Shinkan 22 | # Subsector E: Sanches 23 | # Subsector F: Mekee 24 | # Subsector G: Core 25 | # Subsector H: Kaskii 26 | # Subsector I: Bunkeria 27 | # Subsector J: Cemplas 28 | # Subsector K: Chant 29 | # Subsector L: Dingtra 30 | # Subsector M: Cadion 31 | # Subsector N: Ch'naar 32 | # Subsector O: Dunea 33 | # Subsector P: Saregon 34 | 35 | # Alleg: ImDc: "Third Imperium, Domain of Sylea" 36 | # Alleg: ImSy: "Third Imperium, Sylean Worlds" 37 | 38 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 39 | ---- -------------------- --------- ------------------------------------- ------ ------- ------ ----- -- - --- -- ---- --------------- 40 | 2118 Capital A586A98-F Hi Cx (Syleans)5 Ht { 4 } (H9G+4) [AE5F] BEFG NW - 605 15 ImSy G2 V 41 | 2121 Affinity B98A661-B Ni Ri Wa O:2219 { 2 } (C56-2) [2817] BC - - 213 10 ImDc G1 V 42 | 2521 Shashuua E560463-7 De Ni O:2320 { -3 } (631-5) [1124] B - - 201 13 ImDc K4 V 43 | 2922 Ekugush B652A68-A Hi Po O:3021 { 3 } (G9D+3) [AD5A] BE - - 904 11 ImDc M1 V M9 V 44 | -------------------------------------------------------------------------------- /Tests/testUniqueSectors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jan 12, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | import codecs 7 | import os 8 | import tempfile 9 | 10 | from Tests.baseTest import baseTest 11 | from pytest_console_scripts import ScriptRunner 12 | 13 | 14 | class testUniqueSectors(baseTest): 15 | 16 | def testSectorListIsUnique(self) -> None: 17 | fullpath = self.unpack_filename('../PyRoute/unique_sectors.py') 18 | 19 | cases = [ 20 | ("sectorlist.txt", "../sectorlist.txt"), 21 | ("sectorimplist.txt", "../sectorimplist.txt") 22 | ] 23 | 24 | for filename, fullname in cases: 25 | with self.subTest(filename): 26 | with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as outfile: 27 | srcfile = self.unpack_filename(fullname) 28 | 29 | cwd = os.getcwd() 30 | runner = ScriptRunner(launch_mode="subprocess", rootdir=cwd, print_result=False) 31 | 32 | foo = runner.run([fullpath, "--infile", srcfile, "--outfile", outfile.name]) 33 | self.assertEqual(0, foo.returncode, "unique_sectors did not complete successfully: " + foo.stderr) 34 | 35 | inlines = [] 36 | with codecs.open(srcfile, mode="r", encoding="utf-8") as infile: 37 | inlines = [line for line in infile] 38 | 39 | outlines = [] 40 | with codecs.open(outfile.name, mode="r", encoding="utf-8") as outfile_foo: 41 | outlines = [line for line in outfile_foo] 42 | 43 | self.assertEqual(len(outlines), len(inlines), filename + " has at least one duplicate") 44 | self.assertEqual(outlines, inlines, "Lines in " + filename + " not correctly sorted") 45 | -------------------------------------------------------------------------------- /PyRoute/Pathfinding/RouteLandmarkGraph.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Feb 25, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | import functools 7 | import numpy as np 8 | 9 | from PyRoute.Pathfinding.DistanceBase import DistanceBase 10 | 11 | 12 | class RouteLandmarkGraph(DistanceBase): 13 | 14 | def __init__(self, graph): 15 | super().__init__(graph) 16 | self._arcs = [ 17 | (np.array([], dtype=int), np.array([], dtype=float), dict()) 18 | for u in self._nodes 19 | ] 20 | 21 | def __getitem__(self, item): 22 | self._check_index(item) 23 | return self._arcs[item] 24 | 25 | def add_edge(self, u, v, weight) -> None: 26 | self._check_index(u) 27 | self._check_index(v) 28 | self._extend_arc(u, v, weight) 29 | self._extend_arc(v, u, weight) 30 | 31 | def _extend_arc(self, u, v, weight): 32 | arcs = self._arcs[u] 33 | u_dict = arcs[2] 34 | if v not in u_dict: 35 | u_dict[v] = len(u_dict) 36 | u_first = np.append(arcs[0], [v], 0) 37 | u_last = np.append(arcs[1], [weight], 0) 38 | self._arcs[u] = (u_first, u_last, u_dict) 39 | else: 40 | self._lighten_arc(u, v, weight) 41 | 42 | @functools.cache 43 | def _check_index(self, item): 44 | if not isinstance(item, int): 45 | raise IndexError("Index must be integer between 0 and " + str(len(self) - 1)) 46 | if 0 > item: 47 | raise IndexError("Index must be integer between 0 and " + str(len(self) - 1)) 48 | if len(self) <= item: 49 | raise IndexError("Index must be integer between 0 and " + str(len(self) - 1)) 50 | 51 | def lighten_edge(self, u, v, weight) -> None: 52 | self._lighten_arc(u, v, weight) 53 | self._lighten_arc(v, u, weight) 54 | -------------------------------------------------------------------------------- /PyRoute/SystemData/Utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Nov 21, 2023 3 | 4 | Supporting utilities needed by various SystemData components 5 | 6 | @author: CyberiaResurrection 7 | """ 8 | 9 | 10 | class Utilities: 11 | 12 | tax_rate = {'0': 0.50, '1': 0.8, '2': 1.0, '3': 0.9, '4': 0.85, 13 | '5': 0.95, '6': 1.0, '7': 1.0, '8': 1.1, '9': 1.15, 14 | 'A': 1.20, 'B': 1.1, 'C': 1.2, 'D': 0.75, 'E': 0.75, 15 | 'F': 0.75, 16 | # Aslan Government codes 17 | 'G': 1.0, 'H': 1.0, 'J': 1.2, 'K': 1.1, 'L': 1.0, 18 | 'M': 1.1, 'N': 1.2, 19 | # Unknown Gov Codes 20 | 'I': 1.0, 'P': 1.0, 'Q': 1.0, 'R': 1.0, 'S': 1.0, 'T': 1.0, 21 | '': 1.0, 'U': 1.0, 'V': 1.0, 'W': 1.0, 'X': 1.0, '?': 0.0 22 | } 23 | 24 | @staticmethod 25 | def ehex_to_int(value) -> int: 26 | if not isinstance(value, str): 27 | raise ValueError("Value must be string") 28 | val = int(value, 36) if value in '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' else 0 29 | val -= 1 if val > 18 else 0 30 | val -= 1 if val > 22 else 0 31 | return val 32 | 33 | @staticmethod 34 | def int_to_ehex(value: int) -> str: 35 | if not isinstance(value, int): 36 | raise ValueError("Value must be integer") 37 | if 10 > value: 38 | return str(value) 39 | # Ehex doesn't use I, as it's too easily confused with the numeric 1, likewise with 0 and O 40 | if 9 < value < 18: 41 | valstring = 'ABCDEFGH' 42 | return valstring[value - 10] 43 | if 17 < value: 44 | valstring = 'JKLMNOPQRSTUVWXYZ' 45 | value += 1 if 22 < value else 0 46 | return valstring[value - 18] 47 | assert False # pragma: no mutate 48 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/Passes/Dagudashaag-auxiliary-reduction-two-lines.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:21:52-08:00 3 | 4 | # Dagudashaag 5 | # -1,0 6 | 7 | # Name: Dagudashaag 8 | # Name: Dagudashag 9 | 10 | # Abbreviation: Dagu 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Dagudashaag sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Duncan Law-Green, Leighton Piper and Jae Campbell, and appears in Signal GK Magazine. 15 | 16 | # Author: Duncan Law-Green, Leighton Piper and Jae Campbell; Martin J. Dougherty 17 | # Source: Traveller 5 Second Survey 18 | 19 | # Subsector A: Mimu 20 | # Subsector B: Old Suns 21 | # Subsector C: Arnakhish 22 | # Subsector D: Iiradu 23 | # Subsector E: Shallows 24 | # Subsector F: Ushra 25 | # Subsector G: Khandi 26 | # Subsector H: Kuriishe 27 | # Subsector I: Zeda 28 | # Subsector J: Remnants 29 | # Subsector K: Pact 30 | # Subsector L: Gadde 31 | # Subsector M: Bolivar 32 | # Subsector N: Argi 33 | # Subsector O: Sapphyre 34 | # Subsector P: Laraa 35 | 36 | # Alleg: ImAp: "Third Imperium, Amec Protectorate" 37 | # Alleg: ImDv: "Third Imperium, Domain of Vland" 38 | # Alleg: ImLc: "Third Imperium, Lancian Cultural Region" 39 | 40 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 41 | ---- -------------------- --------- ------------------------------------- ------ ------- ------ ---- -- - --- -- ---- --------------- ----------------------------------------- 42 | 2123 Medurma A9D7954-C An Asla1 Cs Di(Miyavine) Hi S'mr0 { 3 } (G8E+1) [7C3A] BEF - - 823 12 ImDv G0 V Xb:1823 Xb:1926 Xb:2223 Xb:2225 Xb:2322 43 | 2123 Kediiga C778411-8 { -2 } (832-5) [1314] - - 100 0 ImDv G6 V 44 | -------------------------------------------------------------------------------- /Tests/Pathfinding/Schemas/testLandmarksExtremesRegression.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 05 Sep, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.AreaItems.Galaxy import Galaxy 7 | from PyRoute.DataClasses.ReadSectorOptions import ReadSectorOptions 8 | from PyRoute.Pathfinding.LandmarkSchemes.LandmarksTriaxialExtremes import LandmarksTriaxialExtremes 9 | from PyRoute.Inputs.ParseStarInput import ParseStarInput 10 | from Tests.baseTest import baseTest 11 | 12 | 13 | class testLandmarksExtremesRegression(baseTest): 14 | 15 | def setUp(self) -> None: 16 | ParseStarInput.deep_space = {} 17 | 18 | def testZeroArgsInTransposeLandmarks(self) -> None: 19 | sourcefile = self.unpack_filename('../DeltaFiles/zero_arg_in_transpose_landmarks/Bar\'kakr.sec') 20 | 21 | args = self._make_args() 22 | args.route_btn = 15 23 | readparms = ReadSectorOptions(sectors=[sourcefile], pop_code=args.pop_code, ru_calc=args.ru_calc, 24 | route_reuse=args.route_reuse, trade_choice=args.routes, route_btn=args.route_btn, 25 | mp_threads=args.mp_threads, debug_flag=args.debug_flag, fix_pop=False, 26 | deep_space={}, map_type=args.map_type) 27 | 28 | galaxy = Galaxy(min_btn=15, max_jump=4) 29 | galaxy.read_sectors(readparms) 30 | galaxy.output_path = args.output 31 | 32 | galaxy.generate_routes() 33 | galaxy.trade.calculate_components() 34 | 35 | btn = [(s, n, d) for (s, n, d) in galaxy.ranges.edges(data=True)] 36 | btn.sort(key=lambda tn: tn[2]['btn'], reverse=True) 37 | 38 | foo = LandmarksTriaxialExtremes(galaxy) 39 | expected = [ 40 | {0: 56}, {0: 95}, {0: 43}, {0: 59}, {0: 0} 41 | ] 42 | actual, _ = foo.get_landmarks(index=True, btn=btn) 43 | self.assertEqual(expected, actual) 44 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | # Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default. 3 | # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or 4 | # McCabe complexity (`C901`) by default. 5 | lint.select = ["E", "F", "TID252", "NPY201", "W", "PLE", "PLW", "B007", "SIM", "ANN201"] 6 | lint.ignore = ["E501", "SIM113", "SIM300"] 7 | 8 | # Allow autofix for all enabled rules (when `--fix`) is provided. 9 | lint.fixable = ["ALL"] 10 | lint.unfixable = [] 11 | 12 | # Specify github format 13 | output-format = "github" 14 | 15 | # Exclude a variety of commonly ignored directories. 16 | exclude = [ 17 | ".bzr", 18 | ".direnv", 19 | ".eggs", 20 | ".git", 21 | ".git-rewrite", 22 | ".hg", 23 | ".mypy_cache", 24 | ".nox", 25 | ".pants.d", 26 | ".pytype", 27 | ".ruff_cache", 28 | ".svn", 29 | ".tox", 30 | ".venv", 31 | "__pypackages__", 32 | "_build", 33 | "buck-out", 34 | "build", 35 | "dist", 36 | "node_modules", 37 | "venv", 38 | ] 39 | lint.per-file-ignores = {'__init__.py' = ['F822'], 'PyRoute/Calculation/TradeMPCalculation.py' = ['PLW0602', 'PLW0603']} 40 | 41 | # Same as Black. 42 | line-length = 120 43 | 44 | # Allow unused variables when underscore-prefixed. 45 | lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 46 | 47 | # Assume Python 3.9 48 | target-version = "py39" 49 | 50 | [tool.mypy] 51 | warn_unused_configs = true 52 | warn_redundant_casts = true 53 | warn_unused_ignores = true 54 | 55 | follow_imports = "silent" 56 | 57 | # Assume Python 3.9 58 | python_version = "3.9" 59 | 60 | 61 | [tool.coverage.run] 62 | source = ["PyRoute/"] 63 | 64 | [tool.mutmut] 65 | paths_to_mutate = ["PyRoute/"] 66 | do_not_mutate = ["PyRoute/downloadsec.py"] 67 | tests_dir = ["Tests/"] 68 | also_copy = ["Tests/", "PyRoute/Pathfinding/*.so", "pytest.ini"] 69 | max_stack_depth = 20 70 | pytest_add_cli_args_test_selection = ['--ignore=Tests/Hypothesis/'] 71 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/dagudashaag-allegiance-pax-balance/Dagudashaag-delta-error.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:21:52-08:00 3 | 4 | # Dagudashaag 5 | # -1,0 6 | 7 | # Name: Dagudashaag 8 | # Name: Dagudashag 9 | 10 | # Abbreviation: Dagu 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Dagudashaag sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Duncan Law-Green, Leighton Piper and Jae Campbell, and appears in Signal GK Magazine. 15 | 16 | # Author: Duncan Law-Green, Leighton Piper and Jae Campbell; Martin J. Dougherty 17 | # Source: Traveller 5 Second Survey 18 | 19 | # Subsector A: Mimu 20 | # Subsector B: Old Suns 21 | # Subsector C: Arnakhish 22 | # Subsector D: Iiradu 23 | # Subsector E: Shallows 24 | # Subsector F: Ushra 25 | # Subsector G: Khandi 26 | # Subsector H: Kuriishe 27 | # Subsector I: Zeda 28 | # Subsector J: Remnants 29 | # Subsector K: Pact 30 | # Subsector L: Gadde 31 | # Subsector M: Bolivar 32 | # Subsector N: Argi 33 | # Subsector O: Sapphyre 34 | # Subsector P: Laraa 35 | 36 | # Alleg: ImAp: "Third Imperium, Amec Protectorate" 37 | # Alleg: ImDv: "Third Imperium, Domain of Vland" 38 | # Alleg: ImLc: "Third Imperium, Lancian Cultural Region" 39 | 40 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 41 | ---- -------------------- --------- ------------------------------------- ------ ------- ------ ---- -- - --- -- ---- --------------- ----------------------------------------- 42 | 3014 Kherse A200536-D Ni Va { 2 } (746+1) [474C] B NS - 500 7 ImDv M0 II Xb:2615 Xb:3111 Xb:3114 Xb:3115 43 | 3015 Akhashmaas D786498-7 Ni Ga Pa { -3 } (631-3) [4157] Bc - - 423 8 ImDv G1 V 44 | 3016 Larmai C797534-8 Ag Ni { -1 } (B43-3) [3436] BC - - 704 9 ImDv M0 V 45 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/xroute_routes_pass_3_4/Dagudashaag.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:21:52-08:00 3 | 4 | # Dagudashaag 5 | # -1,0 6 | 7 | # Name: Dagudashaag 8 | # Name: Dagudashag 9 | 10 | # Abbreviation: Dagu 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Dagudashaag sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Duncan Law-Green, Leighton Piper and Jae Campbell, and appears in Signal GK Magazine. 15 | 16 | # Author: Duncan Law-Green, Leighton Piper and Jae Campbell; Martin J. Dougherty 17 | # Source: Traveller 5 Second Survey 18 | 19 | # Subsector A: Mimu 20 | # Subsector B: Old Suns 21 | # Subsector C: Arnakhish 22 | # Subsector D: Iiradu 23 | # Subsector E: Shallows 24 | # Subsector F: Ushra 25 | # Subsector G: Khandi 26 | # Subsector H: Kuriishe 27 | # Subsector I: Zeda 28 | # Subsector J: Remnants 29 | # Subsector K: Pact 30 | # Subsector L: Gadde 31 | # Subsector M: Bolivar 32 | # Subsector N: Argi 33 | # Subsector O: Sapphyre 34 | # Subsector P: Laraa 35 | 36 | # Alleg: ImAp: "Third Imperium, Amec Protectorate" 37 | # Alleg: ImDv: "Third Imperium, Domain of Vland" 38 | # Alleg: ImLc: "Third Imperium, Lancian Cultural Region" 39 | 40 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 41 | ---- -------------------- --------- ------------------------------------- ------ ------- ------ ---- -- - --- -- ---- --------------- ----------------------------------------- 42 | 1016 Ushra B561766-C Pr Mr Ht { 3 } (E6F+3) [8A4B] BcEF W - 703 13 ImDc M3 V 43 | 2124 Medurma A9D7954-C Hi An Cs Di(Miyavine) Asla1 S'mr0 { 3 } (G8E+1) [7C3A] BEF - - 823 12 ImDv G0 V Xb:1823 Xb:1926 Xb:2223 Xb:2225 Xb:2322 44 | 3121 Depot A31046A-F Ni Da Mr { 2 } (735+4) [667H] B D A 310 13 ImDv M1 V 45 | -------------------------------------------------------------------------------- /Tests/BorderGeneration/Far Frontiers-partial-allymap-border.json: -------------------------------------------------------------------------------- 1 | {"(-175, 138)": ["white", null, null], "(-173, 137)": ["white", "white", "white"], "(-174, 137)": ["white", "white", null], "(-173, 141)": ["white", null, null], "(-173, 138)": ["white", null, null], "(-172, 137)": [null, "white", "white"], "(-175, 142)": ["white", "white", "white"], "(-175, 143)": [null, null, "white"], "(-176, 144)": ["white", "white", null], "(-176, 141)": ["white", "white", null], "(-175, 140)": ["white", null, "white"], "(-174, 140)": [null, "white", "white"], "(-174, 141)": ["white", "white", "white"], "(-176, 138)": ["white", null, "white"], "(-175, 137)": [null, "white", null], "(-177, 138)": ["white", null, "white"], "(-173, 136)": ["white", null, "white"], "(-172, 136)": ["white", null, "white"], "(-171, 136)": [null, "white", "white"], "(-171, 135)": [null, "white", null], "(-177, 142)": ["white", null, null], "(-177, 141)": [null, "white", null], "(-178, 142)": ["white", null, "white"], "(-178, 141)": [null, "white", "white"], "(-178, 140)": [null, "white", "white"], "(-178, 139)": ["white", "white", null], "(-173, 140)": [null, "white", null], "(-172, 140)": ["white", "white", null], "(-170, 140)": ["white", null, "white"], "(-177, 146)": [null, "white", "white"], "(-177, 145)": ["white", "white", "white"], "(-171, 137)": ["white", null, null], "(-170, 137)": [null, "white", "white"], "(-171, 139)": ["white", null, "white"], "(-170, 138)": [null, "white", "white"], "(-170, 139)": [null, "white", "white"], "(-169, 141)": [null, "white", "white"], "(-169, 140)": [null, "white", "white"], "(-169, 142)": [null, "white", "white"], "(-169, 143)": [null, "white", "white"], "(-170, 145)": ["white", "white", null], "(-169, 144)": [null, null, "white"], "(-171, 146)": ["white", null, "white"], "(-172, 147)": ["white", "white", null], "(-173, 148)": ["white", null, "white"], "(-169, 139)": [null, "white", null], "(-174, 149)": ["white", null, "white"], "(-175, 149)": ["white", null, null], "(-175, 148)": [null, "white", null], "(-176, 149)": ["white", null, "white"], "(-177, 149)": ["white", null, null], "(-177, 148)": [null, "white", "white"], "(-177, 147)": [null, "white", "white"]} -------------------------------------------------------------------------------- /PyRoute/Outputs/Cursor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Sep 12, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from typing import Union 7 | from typing_extensions import Self 8 | 9 | 10 | class Cursor(object): 11 | 12 | def __init__(self, x, y): 13 | self._x = x 14 | self._y = y 15 | self.dx = 0 16 | self.dy = 0 17 | 18 | def __eq__(self, other): 19 | return self._x == other._x and self._y == other._y 20 | 21 | def __str__(self): 22 | return f"{self._x}, {self._y}" 23 | 24 | def __hash__(self): 25 | return hash((self.x, self.y)) 26 | 27 | def set_deltas(self, dx=2, dy=2) -> None: 28 | self.dx = dx 29 | self.dy = dy 30 | 31 | @property 32 | def x(self) -> int: 33 | return self._x 34 | 35 | @x.setter 36 | def x(self, value=0) -> None: 37 | # If in left margin, sets to minimum value. 38 | self._x = value 39 | 40 | @property 41 | def y(self) -> int: 42 | return self._y 43 | 44 | @y.setter 45 | def y(self, value=0) -> None: 46 | self._y = value 47 | 48 | # Changes this cursor 49 | def x_plus(self, dx=None) -> None: 50 | """ 51 | Mutable x addition. Defaults to set delta value. 52 | """ 53 | self._x += dx if dx is not None else self.dx 54 | 55 | def y_plus(self, dy=None) -> None: 56 | """ 57 | Mutable y addition. Defaults to set delta value. 58 | """ 59 | self._y += dy if dy is not None else self.dy 60 | 61 | def copy(self) -> Self: 62 | new_cursor = self.__class__(self.x, self.y) 63 | new_cursor.set_deltas(self.dx, self.dy) 64 | return new_cursor 65 | 66 | def as_tuple(self) -> tuple[int, int]: 67 | """ 68 | Express x,y co-ordinates as a 2-element tuple 69 | """ 70 | return self.x, self.y 71 | 72 | def scaled_tuple(self, scale: float, rounding: bool) -> tuple[Union[int, float], Union[int, float]]: 73 | if rounding: 74 | return int(self.x * scale), int(self.y * scale) 75 | return float(self.x * scale), float(self.y * scale) 76 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/xroute_routes_pass_1_2/Diaspora.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2024-09-28T17:12:48-07:00 3 | 4 | # Diaspora 5 | # 0,-2 6 | 7 | # Name: Diaspora 8 | # Name: Nakulakak (vi) 9 | 10 | # Abbreviation: Dias 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Diaspora sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Mark Gelinas Sr. and appears in Astrogators' Guide to Diaspora Sector (GDW, 1992). 15 | 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Narquel 19 | # Subsector B: Libert 20 | # Subsector C: Sufren 21 | # Subsector D: Khavle 22 | # Subsector E: Shadigi 23 | # Subsector F: Kushga 24 | # Subsector G: Alurza 25 | # Subsector H: Pasdaruu 26 | # Subsector I: Ebasha 27 | # Subsector J: Iusea 28 | # Subsector K: The Blight 29 | # Subsector L: Promise 30 | # Subsector M: Hijiri 31 | # Subsector N: Shumisdi 32 | # Subsector O: Madoc 33 | # Subsector P: Khulam 34 | 35 | # Alleg: ImDs: "Third Imperium, Domain of Sol" 36 | 37 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 38 | ---- -------------------- --------- ------------------------------ ------ ------- ------ ----- -- - --- -- ---- -------------- 39 | 1109 Libert A3109BC-F Hi In Na Cs Pz (Liberts) Ht { 4 } (D8G+5) [CD8J] BEF - A 102 6 ImDs K8 III 40 | 1510 Cayene D425338-9 Lo { -2 } (720-2) [3159] B - - 520 10 ImDs K1 V 41 | 1807 Sturgis A544203-E Lo Ht { 1 } (611-2) [132B] B N - 102 9 ImDs K0 V 42 | 2209 Backman B55497B-C Hi Ht { 3 } (E8E+5) [BC7E] BE N - 103 13 ImDs K0 V M0 V 43 | 2609 Shareduu E431574-9 Ni Po Da { -2 } (A42-4) [3337] B - A 121 7 ImDs M2 V 44 | 2807 Blanket C424578-B Ni { 0 } (844+1) [555B] B S - 601 11 ImDs M1 V M3 V 45 | 2906 Dordogne A554578-D Ag Ni Ht { 2 } (B46+2) [575D] BC - - 404 11 ImDs M3 V 46 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/Passes/Dagudashaag-subsector-full-reduce.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:21:52-08:00 3 | 4 | # Dagudashaag 5 | # -1,0 6 | 7 | # Name: Dagudashaag 8 | # Name: Dagudashag 9 | 10 | # Abbreviation: Dagu 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Dagudashaag sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Duncan Law-Green, Leighton Piper and Jae Campbell, and appears in Signal GK Magazine. 15 | 16 | # Author: Duncan Law-Green, Leighton Piper and Jae Campbell; Martin J. Dougherty 17 | # Source: Traveller 5 Second Survey 18 | 19 | # Subsector A: Mimu 20 | # Subsector B: Old Suns 21 | # Subsector C: Arnakhish 22 | # Subsector D: Iiradu 23 | # Subsector E: Shallows 24 | # Subsector F: Ushra 25 | # Subsector G: Khandi 26 | # Subsector H: Kuriishe 27 | # Subsector I: Zeda 28 | # Subsector J: Remnants 29 | # Subsector K: Pact 30 | # Subsector L: Gadde 31 | # Subsector M: Bolivar 32 | # Subsector N: Argi 33 | # Subsector O: Sapphyre 34 | # Subsector P: Laraa 35 | 36 | # Alleg: ImAp: "Third Imperium, Amec Protectorate" 37 | # Alleg: ImDv: "Third Imperium, Domain of Vland" 38 | # Alleg: ImLc: "Third Imperium, Lancian Cultural Region" 39 | 40 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 41 | ---- -------------------- --------- ------------------------------------- ------ ------- ------ ---- -- - --- -- ---- --------------- ----------------------------------------- 42 | 1722 Campbell B99A200-E Lo Wa { 2 } (812-2) [1419] B W - 204 7 ImDv M1 V Xb:1420 Xb:1823 Xb:2020 43 | 2123 Kediiga B778411-8 Ni Pa { -1 } (832-5) [1314] Bc - - 920 9 ImDv G6 V 44 | 2124 Medurma A9D7954-C Hi An Cs Di(Miyavine) Asla1 S'mr0 { 3 } (G8E+1) [7C3A] BEF - - 823 12 ImDv G0 V Xb:1823 Xb:1926 Xb:2223 Xb:2225 Xb:2322 45 | 2123 Medurma A9D7954-C Hi An Cs Di(Miyavine) Asla1 S'mr0 { 3 } (G8E+1) [7C3A] BEF - - 823 12 ImDv G0 V Xb:1823 Xb:1926 Xb:2223 Xb:2225 Xb:2322 46 | -------------------------------------------------------------------------------- /PyRoute/unique_sectors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 3 | Created on Jan 12, 2025 4 | 5 | @author: CyberiaResurrection 6 | """ 7 | 8 | import argparse 9 | import logging 10 | import codecs 11 | 12 | logger = logging.getLogger('PyRoute') 13 | 14 | 15 | def process() -> None: 16 | parser = argparse.ArgumentParser( 17 | description='Traveller trade route sector list deduplicator.' 18 | ) 19 | parser.add_argument( 20 | '--infile', 21 | help='file with list of sector names to process', 22 | default="sectorlist.txt" 23 | ) 24 | parser.add_argument( 25 | '--outfile', 26 | help='file to write deduplicated list of sector names to', 27 | default="sectorlist_uniq.txt" 28 | ) 29 | parser.add_argument( 30 | '--log-level', 31 | default='INFO' 32 | ) 33 | 34 | args = parser.parse_args() 35 | set_logging(args.log_level) 36 | 37 | srcfile = args.infile 38 | outfile = args.outfile 39 | if outfile is None: 40 | outfile = srcfile 41 | 42 | # read srcfile in, line by line: 43 | try: 44 | with codecs.open(srcfile, 'r', encoding="utf-8") as infile: 45 | lines = [line for line in infile] 46 | except FileNotFoundError as e: 47 | logger.error("Sector file " + srcfile + " not found. Cannot continue.") 48 | raise e 49 | 50 | line_set = set(lines) 51 | 52 | lines = list(line_set) 53 | lines.sort() 54 | 55 | try: 56 | with codecs.open(outfile, "w", encoding="utf-8") as targfile: 57 | targfile.writelines(lines) 58 | except Exception as e: 59 | logger.error("Deduplicated sector file " + outfile + " could not be created. Cannot continue") 60 | raise e 61 | 62 | logger.info("process complete") 63 | 64 | 65 | def set_logging(level) -> None: 66 | logger.setLevel(level) 67 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 68 | # create console handler and set level to debug 69 | ch = logging.StreamHandler() 70 | ch.setLevel(level) 71 | ch.setFormatter(formatter) 72 | logger.addHandler(ch) 73 | 74 | 75 | if __name__ == '__main__': 76 | process() 77 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/xroute_routes_pass_3_2/Zarushagar.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:37:26-08:00 3 | 4 | # Zarushagar 5 | # -1,-1 6 | 7 | # Name: Zarushagar 8 | 9 | # Abbreviation: Zaru 10 | 11 | # Milieu: M1105 12 | 13 | # Credits: Zarushagar sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Greg Videll. Portions appear in The Travellers Digest and the MegaTraveller Referee's Gaming Kit (Digest Group Publications, 1989). 14 | 15 | # Author: Greg Videll, Martin J. Dougherty 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Ibaru 19 | # Subsector B: Triton 20 | # Subsector C: Lagaar 21 | # Subsector D: Lode 22 | # Subsector E: Iimii 23 | # Subsector F: Khipge 24 | # Subsector G: Epshu 25 | # Subsector H: Gaussi 26 | # Subsector I: Mizar 27 | # Subsector J: Liasdi 28 | # Subsector K: Wolf 29 | # Subsector L: Oasis 30 | # Subsector M: Void 31 | # Subsector N: Allegro 32 | # Subsector O: Rhamsteppe 33 | # Subsector P: Mongrabi 34 | 35 | # Alleg: CsIm: "Client state, Third Imperium" 36 | # Alleg: ImDi: "Third Imperium, Domain of Ilelish" 37 | # Alleg: NaHu: "Non-Aligned, Human-dominated" 38 | 39 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 40 | ---- -------------------- --------- -------------------------- ------ ------- ------ ----- -- - --- -- ---- -------------- ---------------------------------------------- 41 | 0621 Lenox C855423-9 Ni Ga Pa { 0 } (C33-3) [1426] Bc W - 924 9 ImDi M3 V M9 V Xb:0220 Xb:0724 42 | 0928 Liasdi A887944-C Hi Ga Cs Pr Darm7 { 3 } (F8E+1) [7C3A] BcEF - - 404 12 ImDi F8 V Xb:0628 Xb:1226 43 | 1326 Driuden B6676AA-A Ag Ni Ga Ri Da Darm7 { 3 } (A57+5) [897C] BC N A 911 11 ImDi G7 V M3 V 44 | 1727 Lianma A7329A7-F Hi Na Po Cp Darm8 { 3 } (F8F+3) [9C5F] BEF - - 204 13 ImDi F0 V Xb:1528 45 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/xroute_routes_pass_1_4/Delphi.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2024-09-28T17:12:18-07:00 3 | 4 | # Delphi 5 | # 1,-1 6 | 7 | # Name: Delphi 8 | # Name: Manadish Khurem (vi) 9 | 10 | # Abbreviation: Delp 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Delphi sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). 15 | 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Kirkankhim 19 | # Subsector B: Tsent 20 | # Subsector C: Dighirpi 21 | # Subsector D: Forlorn 22 | # Subsector E: Zukhumi 23 | # Subsector F: Rifts Reach 24 | # Subsector G: Rayoci'Ailr 25 | # Subsector H: Tolanada 26 | # Subsector I: Aklan 27 | # Subsector J: Void 28 | # Subsector K: Anaxias 29 | # Subsector L: Eduum 30 | # Subsector M: Eta-Gu 31 | # Subsector N: Riramla 32 | # Subsector O: Zuma 33 | # Subsector P: Breda 34 | 35 | # Alleg: ImDc: "Third Imperium, Domain of Sylea" 36 | 37 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 38 | ---- -------------------- --------- -------------------- ------ ------- ------ ---- -- - --- -- ---- -------------- 39 | 0129 Walden E687769-4 Ag Ga Ri O:0329 Lt { 0 } (966+1) [8765] BC - - 533 12 ImDc G1 V 40 | 0421 Withern C100963-B Hi In Na Va O:0623 { 3 } (E8E+1) [6C28] BE - - 603 7 ImDc M2 V M9 V 41 | 0528 Valli B562876-6 Ri Ph Pz { 1 } (A78+1) [7945] BCe - A 104 15 ImDc M1 V 42 | 0823 Herodotus A100435-F Ni Va Ht { 1 } (A34-1) [253D] B - - 604 10 ImDc M2 V 43 | 0929 Gabon C10088A-B Na Va Ph Pi { 1 } (E7B+3) [A97D] BDe - - 513 8 ImDc K3 V M4 V 44 | 1024 Toirdealbach B525677-7 Ni { -1 } (853-1) [6557] B - - 403 13 ImDc M0 V M4 V 45 | 1028 Edmonton C431721-A Na Po { 1 } (E6A-3) [3816] B - - 823 8 ImDc K3 V M0 V M0 V 46 | 1422 Barnet C897554-6 Ag Ni { -1 } (743-3) [3434] BC - - 204 10 ImDc K4 V M7 V 47 | 1724 Anaxias A553A85-D Hi Po Cs Ht { 3 } (C9F+1) [8D3B] BEF - - 900 11 ImDc K0 V M3 V 48 | -------------------------------------------------------------------------------- /PyRoute/Inputs/StarlineParser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Dec 27, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | from lark import Lark, Tree, Token 8 | import re 9 | 10 | 11 | def dashrepl(m) -> str: 12 | group = m.group().replace(' ', ' ') 13 | return group 14 | 15 | 16 | class StarlineParser: 17 | 18 | starline_grammar = r""" 19 | starline: position starname trade extensions nbz world_alg residual? 20 | 21 | position: /^((?:0[1-9]|[1-2]\d|3[0-2])(?:0[1-9]|40|[1-3]\d))/ 22 | 23 | starname: /(.{15,}) ([A-HXYa-hxy\?][0-9A-Fa-f\?][\w\?]{2,2}[0-9A-Fa-f\?][0-9A-Xa-x\?][0-9A-Ka-k\?]-[\w\?]) / 24 | 25 | trade: BLANK | TRADECODE* 26 | TRADECODE: MINOR_DIEBACK | BINARY | POPCODE | MINOR_SOPHONT | OWNED_COLONY | MAJOR_SOPHONT | RESIDUAL | SINGLETON 27 | BLANK: / / 28 | BINARY.3: /[A-Z][a-z]/ 29 | POPCODE.3: /[A-Z][a-z!]{1,3}[W\d]{0,1}/ 30 | MINOR_SOPHONT.3: /\([^\)\{\(]{1,}\)[WX\d\?]{0,1}/ 31 | MINOR_DIEBACK.3: /Di\([^\)]{1,}\)[\d]{0,1}/ 32 | MAJOR_SOPHONT.3: /\[[^\]\{]{1,}\][WX\d\?]{0,1}/ 33 | OWNED_COLONY.3: /[OC]:[X\d\?]{0,4}/ | /[OC]:[A-Z][A-Za-z]{3,3}[-\:]{0,1}\d{4,4}/ 34 | RESIDUAL.2: /[0-9A-Za-z?\-+*()\'\{\}\[\]]{2,}/ 35 | SINGLETON: /[0-9AC-Za-z\+\*()?\']/ 36 | 37 | extensions: ix ex cx | /( ) ( ) ( )/ 38 | 39 | ix: /\{ *[ +-]?[0-6] ?\}|-/ 40 | ex: /\([0-9A-Za-z]{3}[+-]\d\)|-/ 41 | cx: /(\[[0-9A-Za-z]{4}[\]\}]|-)/ 42 | 43 | nbz: nobles base zone 44 | 45 | nobles: /([BcCDeEfFGH]{1,5}|-| )/ 46 | 47 | base: /([A-Z]{1,3}|-|\*) / 48 | 49 | zone: /([ARUFGBarufgb]|-| )[ ]{0,}/ 50 | 51 | pbg: /[0-9X?][0-9A-FX?][0-9A-FX?] / 52 | 53 | residual: /(.{1,})/ 54 | 55 | world_alg: pbg worlds allegiance 56 | worlds: /(\d{1,} | |- )/ 57 | 58 | allegiance: /[A-Z0-9?-][A-Za-z0-9?-]{1,3}/ 59 | 60 | %import common.ESCAPED_STRING 61 | %import common.SIGNED_NUMBER 62 | %import common.WS 63 | %ignore WS 64 | 65 | """ 66 | 67 | def __init__(self): 68 | self.parser = Lark(self.starline_grammar, start='starline') 69 | 70 | def parse(self, text: str) -> tuple[Tree[Token], str]: 71 | text = re.sub(r'[\w\-] [\w\-]', dashrepl, text) 72 | 73 | return self.parser.parse(text), text 74 | -------------------------------------------------------------------------------- /PyRoute/Inputs/StarlineStationParser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 20, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | from lark import Lark, Tree, Token 8 | import re 9 | 10 | 11 | def dashrepl(m) -> str: 12 | group = m.group().replace(' ', ' ') 13 | return group 14 | 15 | 16 | class StarlineStationParser: 17 | 18 | starline_grammar = r""" 19 | starline: position starname trade extensions nbz world_alg? residual? 20 | 21 | position: /^((?:0[1-9]|[1-2]\d|3[0-2])(?:0[1-9]|40|[1-3]\d))/ 22 | 23 | starname: /(.{15,}) ([A-HXYa-hxy\?][0-9A-Fa-f\?][\w\?]{2,2}[0-9A-Fa-f\?][0-9A-Xa-x\?][0-9A-Ka-k\?]-[\w\?]) / 24 | 25 | trade: BLANK | TRADECODE* 26 | TRADECODE: MINOR_DIEBACK | BINARY | POPCODE | MINOR_SOPHONT | OWNED_COLONY | MAJOR_SOPHONT | RESIDUAL | SINGLETON 27 | BLANK: / / 28 | BINARY.3: /[A-Z][a-z]/ 29 | POPCODE.3: /[A-Z][a-z!]{1,3}[W\d]{0,1}/ 30 | MINOR_SOPHONT.3: /\([^\)\{\(]{1,}\)[WX\d\?]{0,1}/ 31 | MINOR_DIEBACK.3: /Di\([^\)]{1,}\)[\d]{0,1}/ 32 | MAJOR_SOPHONT.3: /\[[^\]\{]{1,}\][WX\d\?]{0,1}/ 33 | OWNED_COLONY.3: /[OC]:[X\d\?]{0,4}/ | /[OC]:[A-Z][A-Za-z]{3,3}[-\:]{0,1}\d{4,4}/ 34 | RESIDUAL.2: /[0-9A-Za-z?\-+*()\'\{\}\[\]]{2,}/ 35 | SINGLETON: /[0-9AC-Za-z\+\*()?\']/ 36 | 37 | extensions: ix ex cx | /( ) ( ) ( )/ 38 | 39 | ix: /\{ *[ +-]?[0-6] ?\}/ 40 | ex: /\([0-9A-Za-z]{3}[+-]\d\)|-/ 41 | cx: /(\[[0-9A-Za-z]{4}[\]\}]|-)/ 42 | 43 | nbz: nobles base zone 44 | 45 | nobles: /([BcCDeEfFGH]{1,5}|-| )/ 46 | 47 | base: /([A-Z]{1,3}|-|\*) / 48 | 49 | zone: /([ARUFGBarufgb]|-| )[ ]{0,}/ 50 | 51 | pbg: /[0-9X?][0-9A-FX?][0-9A-FX?] / 52 | 53 | residual: /(.{1,})/ 54 | 55 | world_alg: pbg worlds allegiance 56 | worlds: /(\d{1,} | |- )/ 57 | 58 | allegiance: / [A-Z0-9?-][A-Za-z0-9?-]{1,3}/ 59 | 60 | %import common.ESCAPED_STRING 61 | %import common.SIGNED_NUMBER 62 | %import common.WS 63 | %ignore WS 64 | 65 | """ 66 | 67 | def __init__(self): 68 | self.parser = Lark(self.starline_grammar, start='starline') 69 | 70 | def parse(self, text: str) -> tuple[Tree[Token], str]: 71 | text = re.sub(r'[\w\-] [\w\-]', dashrepl, text) 72 | 73 | return self.parser.parse(text), text 74 | -------------------------------------------------------------------------------- /Tests/testAllyGen.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyRoute.Allies.AllyGen import AllyGen 4 | 5 | 6 | class testAllyGen(unittest.TestCase): 7 | def test_is_nonaligned_no_one(self) -> None: 8 | self.assertTrue(AllyGen.is_nonaligned('??')) 9 | 10 | def test_is_nonaligned_nadr(self) -> None: 11 | self.assertTrue(AllyGen.is_nonaligned('NaDr')) 12 | 13 | def test_is_nonaligned_nahu(self) -> None: 14 | self.assertTrue(AllyGen.is_nonaligned('NaHu')) 15 | 16 | def test_are_allies_one_none(self) -> None: 17 | self.assertFalse(AllyGen.are_allies(None, 'ImDd')) 18 | 19 | def test_are_allies_one_no_one(self) -> None: 20 | self.assertFalse(AllyGen.are_allies('--', 'ImDd')) 21 | 22 | def test_are_owned_allies_one_none(self) -> None: 23 | self.assertFalse(AllyGen.are_owned_allies(None, 'ImDd')) 24 | 25 | def test_are_owned_allies_one_no_one(self) -> None: 26 | self.assertFalse(AllyGen.are_owned_allies('--', 'ImDd')) 27 | 28 | def test_are_owned_allies_same_parent(self) -> None: 29 | self.assertTrue(AllyGen.are_owned_allies('ZhAx', 'ZhCa')) 30 | 31 | def test_are_owned_allies_diff_parent(self) -> None: 32 | self.assertFalse(AllyGen.are_owned_allies('ZhAx', 'HvFd')) 33 | 34 | def test_zhodani_in_name_of_population_alignment(self) -> None: 35 | expected = 'Zhod' 36 | actual = AllyGen.population_align('Na', 'Zhodani') 37 | self.assertEqual(expected, actual) 38 | 39 | def test_is_wilds_none(self) -> None: 40 | expected = False 41 | actual = AllyGen.is_wilds(None) 42 | self.assertEqual(expected, actual) 43 | 44 | def test_is_wilds_single_char(self) -> None: 45 | expected = False 46 | actual = AllyGen.is_wilds('a') 47 | self.assertEqual(expected, actual) 48 | 49 | def test_is_client_state_none(self) -> None: 50 | expected = False 51 | actual = AllyGen.is_client_state(None) 52 | self.assertEqual(expected, actual) 53 | 54 | def test_is_client_state_single_char(self) -> None: 55 | expected = False 56 | actual = AllyGen.is_client_state('a') 57 | self.assertEqual(expected, actual) 58 | 59 | 60 | if __name__ == '__main__': 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/xroute_routes_pass_1_2/Fornast.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2024-09-28T17:14:44-07:00 3 | 4 | # Fornast 5 | # 1,0 6 | 7 | # Name: Fornast 8 | # Name: Rishurir (vi) 9 | 10 | # Abbreviation: Forn 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Fornast sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). 15 | 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Cirqa 19 | # Subsector B: Spas 20 | # Subsector C: Madu 21 | # Subsector D: Toza 22 | # Subsector E: AArna 23 | # Subsector F: Akashganar 24 | # Subsector G: Derri 25 | # Subsector H: Sanny 26 | # Subsector I: Imaka 27 | # Subsector J: Irinkuka 28 | # Subsector K: Dikhari 29 | # Subsector L: Nareshakir 30 | # Subsector M: Setten 31 | # Subsector N: Tharrynra 32 | # Subsector O: Shumduur 33 | # Subsector P: Zukhin 34 | 35 | # Alleg: ImDc: "Third Imperium, Domain of Sylea" 36 | 37 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 38 | ---- -------------------- --------- ------------------------- ------ ------- ------ ----- -- - --- -- ---- -------------- 39 | 0124 Hankel C654320-9 Lo { -1 } (720-5) [1214] B - - 502 10 ImDc G2 V M5 V 40 | 0526 Algenib C55466A-5 Ag Ni O:0527 Lt { -1 } (853+1) [8577] BC S - 323 8 ImDc G4 V 41 | 0928 Kikha Dur C100541-C Ni Va Ht { 0 } (A44-4) [1518] B - - 203 11 ImDc M0 V M6 V 42 | 1131 Mathieu A679346-E Lo Ht { 1 } (521+1) [244D] B - - 700 9 ImDc K6 V 43 | 1533 Gemam AA8A558-F Ni Oc Pr Ht { 1 } (745+1) [565F] Bc - - 500 10 ImDc K4 V M5 V 44 | 1933 Ketial A68578A-C Ag Ga Ri (Bhunj) Ht { 4 } (D6E+5) [9B7E] BCf - - 713 14 ImDc G5 V 45 | 2235 Irim A9979AB-E Hi In Pz Ht { 4 } (C8G+5) [BD7G] BEf - A 510 5 ImDc G4 V M1 V M0 V 46 | 2239 Blanchard C100566-B Ni Va O:2438 { 0 } (B44-1) [454A] B - - 122 12 ImDc G2 V 47 | 2334 Shumduur A5459AA-E Hi In Cs Pz Bhun0 Ht { 5 } (C8H+5) [BE7G] BEF W A 610 9 ImDc K3 V M6 V 48 | -------------------------------------------------------------------------------- /Tests/Hypothesis/testTradeBalance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Oct 19, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from hypothesis import given, assume, example, HealthCheck, settings 7 | from hypothesis.strategies import text, composite, sampled_from, lists, dictionaries, integers, tuples, SearchStrategy 8 | 9 | from PyRoute.AreaItems.Galaxy import Galaxy 10 | from PyRoute.AreaItems.Sector import Sector 11 | from PyRoute.TradeBalance import TradeBalance 12 | from Tests.baseTest import baseTest 13 | 14 | 15 | @composite 16 | def multi_dict(draw) -> SearchStrategy: 17 | sec_names = text(min_size=3, max_size=36, alphabet='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWYXZ -{}()[]?\'+*') 18 | sec_strat = lists(sec_names, min_size=2, unique=True) 19 | choices = draw(sec_strat) 20 | key_strat = tuples(sampled_from(choices), sampled_from(choices)) 21 | 22 | multi_strat = dictionaries(keys=key_strat, values=integers(min_value=0, max_value=2000), min_size=2, 23 | max_size=len(choices) * (len(choices) - 1)) 24 | multidict = draw(multi_strat) 25 | 26 | return multidict 27 | 28 | 29 | class testTradeBalance(baseTest): 30 | 31 | @given(multi_dict()) 32 | @example({('000', '00'): 0, ('000', '0000'): 0}) 33 | @example({('000', '001'): 0, ('000', '002'): 2}) 34 | @example({('8cVH', 'idhC2-TNs'): 1312, ('hnjMin', 'idhC2-TNs'): 1397, ('idhC2-TNs', 'hnjMin'): 376}) 35 | @settings(suppress_health_check=[HealthCheck(10)], deadline=1000) 36 | def test_multilateral_balance(self, s: dict) -> None: 37 | galaxy = Galaxy(8) 38 | for item in s: 39 | first = item[0] 40 | second = item[1] 41 | assume(first != second) 42 | if first not in galaxy.sectors: 43 | galaxy.sectors[first] = Sector('# ' + first, '# 0, 0') 44 | if second not in galaxy.sectors: 45 | galaxy.sectors[second] = Sector('# ' + second, '# 0, 0') 46 | 47 | foo = TradeBalance(stat_field='tradeExt', region=galaxy) 48 | foo.update(s) 49 | foo.multilateral_balance() 50 | 51 | act_dict = dict(foo) 52 | self.assertTrue(0 <= min(act_dict.values()), "Unexpected minimum value") 53 | self.assertTrue(1 >= max(act_dict.values()), "Unexpected maximum value") 54 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/Canonicalisation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jun 13, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaStar import DeltaStar 7 | 8 | 9 | class Canonicalisation(object): 10 | 11 | def __init__(self, reducer): 12 | self.reducer = reducer 13 | 14 | def preflight(self) -> bool: 15 | return self.reducer is not None and self.reducer.sectors is not None and 0 < len(self.reducer.sectors.lines) 16 | 17 | def run(self) -> None: 18 | # build substitution list - canonicalise _everything_ 19 | subs_list = [] 20 | for line in self.reducer.sectors.lines: 21 | canon = DeltaStar.reduce(line, canonicalise=True) 22 | assert isinstance(canon, str), "Candidate line " + line + " was not reduced to a string. Got " + canon + " instead." 23 | canon = DeltaStar.reduce(canon, canonicalise=True) 24 | subs_list.append((line, canon)) 25 | 26 | if 0 == len(subs_list): 27 | # nothing to do, bail out early 28 | return 29 | 30 | temp_sectors = self.reducer.sectors.switch_lines(subs_list) 31 | 32 | interesting, msg, _ = self.reducer._check_interesting(self.reducer.args, temp_sectors) 33 | 34 | if interesting: 35 | new_hex = 'Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes \n' 36 | new_dash = '---- -------------------- --------- ------------------------------------- ------ ------- ------ ---- -- - --- -- ---- --------------- -----------------------------------------\n' 37 | for sec_name in temp_sectors: 38 | num_headers = len(temp_sectors[sec_name].headers) 39 | for i in range(0, num_headers): 40 | raw_line = temp_sectors[sec_name].headers[i] 41 | if raw_line.startswith('Hex '): 42 | temp_sectors[sec_name].headers[i] = new_hex 43 | elif raw_line.startswith('---- --'): 44 | temp_sectors[sec_name].headers[i] = new_dash 45 | 46 | self.reducer.sectors = temp_sectors 47 | msg = "Reduction found with full canonicalisation" 48 | self.reducer.logger.error(msg) 49 | return 50 | -------------------------------------------------------------------------------- /PyRoute/Outputs/FontLayer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Dec 04, 2021 3 | 4 | @author: CyberiaResurrection 5 | 6 | A dirt-simple adapter layer that used to abstract the differences in where Linux distros (currently, Ubuntu and Fedora) 7 | store their font files away from the rest of the project. Now, it still serves as a SPOT for font locations, even 8 | though they're now bundled with the project. 9 | """ 10 | 11 | import os 12 | import functools 13 | 14 | from PyRoute.Utilities.UnpackFilename import UnpackFilename 15 | 16 | 17 | class FontLayer(object): 18 | fontdict: dict[str, list[str]] = {} 19 | 20 | def __init__(self): 21 | self.fontdict['DejaVuSerifCondensed.ttf'] = [ 22 | UnpackFilename.unpack_filename('../PyRoute/Fonts/DejaVuSerifCondensed.ttf') 23 | ] 24 | self.fontdict['DejaVuSerifCondensed-Bold.ttf'] = [ 25 | UnpackFilename.unpack_filename('../PyRoute/Fonts/DejaVuSerifCondensed-Bold.ttf') 26 | ] 27 | self.fontdict['LiberationMono-Bold.ttf'] = [ 28 | UnpackFilename.unpack_filename('../PyRoute/Fonts/LiberationMono-Bold.ttf') 29 | ] 30 | self.fontdict['FreeMono.ttf'] = [ 31 | UnpackFilename.unpack_filename('../PyRoute/Fonts/FreeMono.ttf') 32 | ] 33 | self.fontdict['Symbola-hint.ttf'] = [ 34 | UnpackFilename.unpack_filename('../PyRoute/Fonts/Symbola-hint.ttf') 35 | ] 36 | 37 | self.fontdict['ZapfDingbats_Regular.ttf'] = [ 38 | UnpackFilename.unpack_filename('../PyRoute/Fonts/ZapfDingbats-Regular.ttf') 39 | ] 40 | 41 | self.fontdict['ZapfDingbats-Regular.ttf'] = [ 42 | UnpackFilename.unpack_filename('../PyRoute/Fonts/ZapfDingbats-Regular.ttf') 43 | ] 44 | 45 | @functools.cache 46 | def getpath(self, filename) -> str: 47 | # Deliberately lean on dictionaries blowing up on accessing absent keys to make it obvious 48 | # that we don't know how to handle what's being asked for 49 | filelist = self.fontdict[filename] 50 | 51 | for item in filelist: 52 | # for moment, don't care whether is a hard or softlinked file 53 | if os.path.exists(item) and not os.path.isdir(item): 54 | return item 55 | 56 | assert False, "Font mapping for " + filename + " not found. Tried " + " ".join(filelist) 57 | -------------------------------------------------------------------------------- /Tests/Outputs/testCursor.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | from PyRoute.Outputs.Cursor import Cursor 5 | 6 | 7 | class TestCursor(unittest.TestCase): 8 | 9 | def testSimple(self) -> None: 10 | cursor = Cursor(0, 0) 11 | self.assertEqual(cursor.x, 0) 12 | self.assertEqual(cursor.y, 0) 13 | 14 | def testdx(self) -> None: 15 | cursor = Cursor(1, 1) 16 | cursor.x_plus() 17 | self.assertEqual(cursor.x, 1) 18 | cursor.x_plus(1) 19 | self.assertEqual(cursor.x, 2) 20 | 21 | def testdy(self) -> None: 22 | cursor = Cursor(1, 1) 23 | cursor.y_plus() 24 | self.assertEqual(cursor.y, 1) 25 | cursor.y_plus(1) 26 | self.assertEqual(cursor.y, 2) 27 | 28 | def testSetDeltas(self) -> None: 29 | cursor = Cursor(1, 1) 30 | cursor.set_deltas() 31 | cursor.x_plus() 32 | self.assertEqual(cursor.x, 3) 33 | self.assertEqual(cursor.y, 1) 34 | cursor.y_plus() 35 | self.assertEqual(cursor.x, 3) 36 | self.assertEqual(cursor.y, 3) 37 | 38 | def testSetDeltaOverride(self) -> None: 39 | cursor = Cursor(1, 1) 40 | cursor.set_deltas(4, 4) 41 | cursor.x_plus() 42 | self.assertEqual(cursor.x, 5) 43 | self.assertEqual(cursor.y, 1) 44 | cursor.y_plus() 45 | self.assertEqual(cursor.x, 5) 46 | self.assertEqual(cursor.y, 5) 47 | 48 | def testString(self) -> None: 49 | cursor = Cursor(1, 1) 50 | self.assertEqual(str(cursor), "1, 1") 51 | 52 | def testEquals(self) -> None: 53 | cursor1 = Cursor(1, 1) 54 | cursor2 = Cursor(3, 3) 55 | self.assertFalse(cursor2 == cursor1) 56 | cursor1.x_plus(2) 57 | cursor1.y_plus(2) 58 | self.assertTrue(cursor2 == cursor1) 59 | 60 | def testAssignValues(self) -> None: 61 | cursor = Cursor(1, 1) 62 | cursor.x = -1 63 | self.assertEqual(cursor.x, -1) 64 | self.assertEqual(cursor.y, 1) 65 | cursor.y = -3 66 | self.assertEqual(cursor.x, -1) 67 | self.assertEqual(cursor.y, -3) 68 | 69 | def testCopy(self) -> None: 70 | cursor = Cursor(1, 1) 71 | cursor_new = cursor.copy() 72 | 73 | self.assertTrue(cursor == cursor_new) 74 | 75 | 76 | if __name__ == "__main__": 77 | unittest.main() 78 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/dijkstra_restart_blowup/Lishun.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:30:02-08:00 3 | 4 | # Lishun 5 | # 0,1 6 | 7 | # Name: Lishun 8 | 9 | # Abbreviation: Lish 10 | 11 | # Milieu: M1105 12 | 13 | # Credits: Lishun sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Joe D. Fugate Sr., and portions appear in The Travellers Digest, The Flaming Eye (DGP, 1991) and Challenge Magazine. 14 | 15 | # Source: Traveller 5 Second Survey 16 | 17 | # Subsector A: Vakkuun 18 | # Subsector B: Adawi 19 | # Subsector C: Sotri 20 | # Subsector D: Criideu 21 | # Subsector E: Pryden 22 | # Subsector F: Masionia 23 | # Subsector G: Gama 24 | # Subsector H: Tephany 25 | # Subsector I: Shuna 26 | # Subsector J: Taccis 27 | # Subsector K: Simen 28 | # Subsector L: Ot Zell 29 | # Subsector M: Shuun 30 | # Subsector N: Welling 31 | # Subsector O: Strashna 32 | # Subsector P: Mirmida 33 | 34 | # Alleg: CsIm: "Client state, Third Imperium" 35 | # Alleg: ImDa: "Third Imperium, Domain of Antares" 36 | # Alleg: NaHu: "Non-Aligned, Human-dominated" 37 | # Alleg: NaVa: "Non-Aligned, Vargr-dominated" 38 | # Alleg: NaXX: "Non-Aligned, unclaimed" 39 | 40 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 41 | ---- -------------------- --------- ------------------------ ------ ------- ------ ---- -- - --- -- ---- -------------- ------------------------------------ 42 | 1134 Ushipaale A422264-C He Lo Po O:1033 { 1 } (611-1) [133A] B N - 920 10 ImDa M1 V Xb:1033 Xb:1037 Xb:1332 43 | 1136 Khusila B95A436-9 Ni Wa Da { 0 } (A33-1) [3448] B N A 304 11 ImDa M1 V 44 | 1232 Siidugam B654632-A Ag Ni { 2 } (856-2) [2816] BC - - 800 12 ImDa M3 V 45 | 1233 Ushmuur C100766-A Na Va Pi O:1033 { 1 } (96A+1) [6849] BD - - 200 7 ImDa K4 IV M0 V 46 | 1332 Welling C420973-C De He Hi In Na Po { 3 } (B8E+1) [6C29] BE S - 200 11 ImDa M1 V M3 V Xb:1134 Xb:1631 47 | 1333 Iigduur C574546-9 Ag Ni { 0 } (A44-1) [4548] BC S - 321 14 ImDa F6 V 48 | 1433 Chayn C637653-A Ni { 0 } (D54-3) [3627] B S - 505 15 ImDa M3 V 49 | 50 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/xroute_calculation_blowups/Antares-Sakhag-Celebes.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:20:28-08:00 3 | 4 | # Antares 5 | # 1,1 6 | 7 | # Name: Antares 8 | # Name: Mikasirka (vi) 9 | 10 | # Abbreviation: Anta 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Antares sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). 15 | 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Pelusium 19 | # Subsector B: Ninik 20 | # Subsector C: Dartho 21 | # Subsector D: Oulduktak 22 | # Subsector E: Shurlarlem 23 | # Subsector F: Sarar 24 | # Subsector G: Gimgir 25 | # Subsector H: Kiirkandin 26 | # Subsector I: Urunishu 27 | # Subsector J: Uradnim 28 | # Subsector K: Antares 29 | # Subsector L: Mukusi 30 | # Subsector M: Urgusap 31 | # Subsector N: Gaakish 32 | # Subsector O: Sakhag 33 | # Subsector P: Celebes 34 | 35 | # Alleg: CsIm: "Client state, Third Imperium" 36 | # Alleg: ImDa: "Third Imperium, Domain of Antares" 37 | # Alleg: ImLa: "Third Imperium, League of Antares" 38 | # Alleg: JuRu: "Julian Protectorate, Rukadukaz Republic" 39 | # Alleg: NaHu: "Non-Aligned, Human-dominated" 40 | # Alleg: NaVa: "Non-Aligned, Vargr-dominated" 41 | 42 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 43 | ---- -------------------- --------- ------------------------- ------ ------- ------ ---- -- - --- -- ---- -------------- ------------------------------------ 44 | 2330 Pijasysley E572577-7 He Ni { -3 } (741-3) [5257] B - - 303 12 ImDa F4 V 45 | 2421 Antares A000ADA-D As Hi In Na Va Cs Pz { 5 } (C9H+5) [CF7F] BEFG NS A 800 3 ImDa M1 Ib B3 V Xb:2122 Xb:2220 Xb:2324 46 | 2425 Ansenz A547ABB-E Hi In Pz Varg1 Di(Kaski) { 4 } (F9G+5) [CE7G] BEf - A 403 11 ImLa G2 V M9 V 47 | 2427 Vat C550533-9 De Ni Po { -1 } (B43-4) [2426] B - - 522 12 ImDa G2 V Xb:2324 Xb:2730 48 | 2631 Laupla C564410-8 Ni Pa { -2 } (D31-5) [1213] Bc S - 725 15 ImDa G8 V 49 | 2634 Rinotawha A99A8BE-E Wa Ph Pi Pz { 2 } (C7D+5) [CA9J] BDe - A 420 9 ImDa K1 V M8 V Xb:2235 Xb:2638 Xb:2935 50 | 2638 Likarudig A89A7BD-E Wa Cp Pi Pz { 3 } (E6E+5) [BA9J] BDF W A 205 14 ImDa K0 V Xb:2634 Xb:2940 51 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.9", "3.13"] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install freefonts & symbola 20 | run: | 21 | sudo apt-get -y install fonts-freefont-ttf fonts-symbola 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install ruff pytest 26 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 27 | - name: Lint with ruff - syntax errors & undefined names 28 | run: | 29 | # stop the build if there are Python syntax errors or undefined names 30 | ruff check --select=E9,F63,F7,F82 --preview PyRoute Tests 31 | - name: Lint with ruff - configured options 32 | run: | 33 | # now run configured options 34 | ruff check --preview PyRoute Tests 35 | - name: Check types with mypy 36 | run: | 37 | mypy PyRoute/Allies/*.py PyRoute/AreaItems/*.py PyRoute/Calculation/*.py PyRoute/DeltaDebug/*.py PyRoute/DeltaPasses/*.py PyRoute/Inputs/*.py PyRoute/Position/*.py PyRoute/StatCalculation/*.py PyRoute/SystemData/*.py PyRoute/*.py 38 | - name: Run non-cython tests 39 | run: | 40 | pytest "Tests/Pathfinding/testAstarNumpy.py::testAStarNumpy::testAStarOverSubsector" "Tests/Pathfinding/testTradeCalculation.py::testTradeCalculation::test_route_update_simple" 41 | - name: Set up cython build 42 | run: | 43 | cd $LD_LIBRARY_PATH/python${{ matrix.python-version }}/site-packages/numpy/ 44 | ls -al 45 | cp __init__.cython-30.pxd numpy.pxd 46 | - name: Build cython modules 47 | run: | 48 | cd PyRoute/Pathfinding 49 | python3 setup.py build_ext --inplace 50 | - name: Test with pytest 51 | run: | 52 | pytest 53 | - name: TradeMP smoke test 54 | run: | 55 | echo "$PWD" 56 | mkdir ./Tests/maps 57 | PYTHONPATH="." python ./PyRoute/route.py @Tests/TradeMPFiles/run_trademp_parameters 58 | -------------------------------------------------------------------------------- /PyRoute/DeltaDebug/DeltaGalaxy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on May 21, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | import copy 7 | 8 | from PyRoute.AreaItems.Allegiance import Allegiance 9 | from PyRoute.AreaItems.Galaxy import Galaxy 10 | from PyRoute.AreaItems.Subsector import Subsector 11 | from PyRoute.AreaItems.Sector import Sector 12 | from PyRoute.DeltaDebug.DeltaDictionary import SectorDictionary, SubsectorDictionary 13 | from PyRoute.Star import Star 14 | 15 | 16 | class DeltaGalaxy(Galaxy): 17 | 18 | def read_sectors(self, sectors, pop_code, ru_calc, # type: ignore 19 | route_reuse, trade_choice, route_btn, mp_threads, debug_flag) -> None: 20 | self._set_trade_object(route_reuse, trade_choice, route_btn, mp_threads, debug_flag) 21 | star_counter = 0 22 | sector: SectorDictionary 23 | 24 | sectors.skip_void_subsectors() 25 | for sector_name in sectors: 26 | sector = sectors[sector_name] 27 | sec = Sector("# " + sector.name, sector.position) 28 | sec.filename = sector.filename 29 | 30 | # load up subsectors 31 | for subsector_name in sector: 32 | subsector: SubsectorDictionary = sector[subsector_name] 33 | subsec = Subsector(subsector.name, subsector.position, sec) 34 | sec.subsectors[subsector.position] = subsec 35 | 36 | # load up allegiances 37 | for allegiance_name in sector.allegiances: 38 | allegiance: Allegiance = copy.deepcopy(sector.allegiances[allegiance_name]) 39 | 40 | if allegiance_name not in self.alg: 41 | self.alg[allegiance_name] = allegiance 42 | 43 | # once all that's done, load up the stars 44 | for line in sector.lines: 45 | if line.startswith('#') or len(line) < 20: 46 | continue 47 | star = Star.parse_line_into_star(line, sec, pop_code, ru_calc) 48 | if star: 49 | star_counter = self.add_star_to_galaxy(star, star_counter, sec) 50 | 51 | self.sectors[sec.name] = sec 52 | self.logger.info("Sector {} loaded {} worlds".format(sec, len(sec.worlds))) 53 | 54 | self.set_bounding_sectors() 55 | self.set_bounding_subsectors() 56 | self.set_positions() 57 | self.logger.debug("Allegiances: {}".format(self.alg)) 58 | -------------------------------------------------------------------------------- /Tests/Pathfinding/testTradeMPCalculation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Aug 08, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.AreaItems.Galaxy import Galaxy 7 | from PyRoute.DataClasses.ReadSectorOptions import ReadSectorOptions 8 | from Tests.baseTest import baseTest 9 | 10 | 11 | class testTradeMPCalculation(baseTest): 12 | def test_direct_route_doesnt_blow_up(self) -> None: 13 | sourcefile = self.unpack_filename('DeltaFiles/direct_routes_on_trade_mp/Lishun.sec') 14 | 15 | args = self._make_args() 16 | args.route_reuse = 30 17 | args.route_btn = 8 18 | args.mp_threads = 2 19 | args.routes = "trade-mp" 20 | 21 | readparms = ReadSectorOptions(sectors=[sourcefile], pop_code=args.pop_code, ru_calc=args.ru_calc, 22 | route_reuse=args.route_reuse, trade_choice=args.routes, route_btn=args.route_btn, 23 | mp_threads=args.mp_threads, debug_flag=args.debug_flag, fix_pop=False, 24 | deep_space={}, map_type=args.map_type) 25 | 26 | galaxy = Galaxy(min_btn=15, max_jump=4) 27 | galaxy.read_sectors(readparms) 28 | galaxy.output_path = args.output 29 | 30 | galaxy.generate_routes() 31 | galaxy.set_borders(args.borders, args.ally_match) 32 | galaxy.trade.calculate_routes() 33 | 34 | def test_direct_route_doesnt_blow_up_2(self) -> None: 35 | sourcefile = self.unpack_filename('DeltaFiles/direct_routes_on_trade_mp/Lishun.sec') 36 | 37 | args = self._make_args() 38 | args.route_reuse = 30 39 | args.min_btn = 15 40 | args.mp_threads = 2 41 | args.routes = "trade-mp" 42 | 43 | readparms = ReadSectorOptions(sectors=[sourcefile], pop_code=args.pop_code, ru_calc=args.ru_calc, 44 | route_reuse=args.route_reuse, trade_choice=args.routes, route_btn=args.route_btn, 45 | mp_threads=args.mp_threads, debug_flag=args.debug_flag, fix_pop=False, 46 | deep_space={}, map_type=args.map_type) 47 | 48 | galaxy = Galaxy(min_btn=15, max_jump=4) 49 | galaxy.read_sectors(readparms) 50 | galaxy.output_path = args.output 51 | 52 | galaxy.generate_routes() 53 | galaxy.set_borders(args.borders, args.ally_match) 54 | galaxy.trade.calculate_routes() 55 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/WidenHoleReducer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Oct 04, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from typing import Optional 7 | 8 | from PyRoute.DeltaDebug.DeltaDictionary import DeltaDictionary 9 | 10 | 11 | class WidenHoleReducer(object): 12 | 13 | def __init__(self, reducer): 14 | from PyRoute.DeltaDebug.DeltaReduce import DeltaReduce 15 | self.reducer: DeltaReduce = reducer 16 | 17 | def preflight(self) -> bool: 18 | return self.reducer is not None and self.reducer.sectors is not None and 0 < len(self.reducer.sectors.lines) 19 | 20 | def run(self, start_pos, reverse=False, best_sectors=None) -> Optional[DeltaDictionary]: 21 | found_reduction = True 22 | chunk_size = 1 23 | 24 | supplied = best_sectors is not None 25 | 26 | best_sectors = self.reducer.sectors if not supplied else best_sectors 27 | 28 | msg = "# of lines: " + str(len(best_sectors.lines)) 29 | self.reducer.logger.error(msg) 30 | 31 | while found_reduction: 32 | segment = best_sectors.lines 33 | segment_len = len(segment) 34 | if reverse: 35 | if 0 < start_pos: 36 | start_pos = min(start_pos, len(best_sectors.lines)) 37 | 38 | raw_start_pos = start_pos + 1 if 0 < start_pos else segment_len + start_pos 39 | raw_fin_pos = raw_start_pos - chunk_size 40 | lines_to_drop = segment[min(raw_start_pos, raw_fin_pos):max(raw_start_pos, raw_fin_pos)] 41 | else: 42 | lines_to_drop = segment[start_pos:start_pos + chunk_size] 43 | 44 | if 1 < chunk_size and 0 == len(lines_to_drop): 45 | found_reduction = False 46 | continue 47 | 48 | temp_sectors = best_sectors.drop_lines(lines_to_drop) 49 | 50 | interesting, msg, _ = self.reducer._check_interesting(self.reducer.args, temp_sectors) 51 | 52 | if not interesting: 53 | found_reduction = False 54 | continue 55 | best_sectors = temp_sectors 56 | chunk_size *= 2 57 | 58 | msg = "Reduction found: new input has " + str(len(best_sectors.lines)) + " lines" 59 | self.reducer.logger.error(msg) 60 | 61 | if supplied: 62 | return best_sectors 63 | 64 | self.reducer.sectors = best_sectors 65 | return None 66 | -------------------------------------------------------------------------------- /Tests/baseTest.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import os 4 | import tempfile 5 | import unittest 6 | 7 | from PyRoute.Utilities.UnpackFilename import UnpackFilename 8 | 9 | 10 | class baseTest(unittest.TestCase): 11 | 12 | # Set up logging information 13 | logger = logging.getLogger(__name__) 14 | logging.basicConfig(level=logging.DEBUG) 15 | 16 | def unpack_filename(self, filename: str) -> str: 17 | return UnpackFilename.unpack_filename(filename) 18 | 19 | def unpack_workdir(self, dirname) -> str: 20 | # try unpacked directory directly 21 | workdir = os.path.abspath(dirname) 22 | 23 | cwd = os.getcwd() 24 | if not os.path.isdir(workdir): 25 | workdir = os.path.abspath(cwd + dirname) 26 | 27 | return workdir 28 | 29 | def reset_logging(self) -> None: 30 | loggers = [logging.getLogger(name) for name in logging.root.manager.loggerDict] 31 | loggers.append(logging.getLogger()) 32 | for logger in loggers: 33 | handlers = logger.handlers[:] 34 | for handler in handlers: 35 | logger.removeHandler(handler) 36 | handler.close() 37 | logger.setLevel(logging.NOTSET) 38 | logger.propagate = True 39 | 40 | def _make_args(self) -> argparse.ArgumentParser: 41 | args = argparse.ArgumentParser(description='PyRoute input minimiser.') 42 | args.btn = 8 43 | args.max_jump = 2 44 | args.route_btn = 13 45 | args.pop_code = 'scaled' 46 | args.ru_calc = 'scaled' 47 | args.routes = 'trade' 48 | args.route_reuse = 10 49 | args.interestingline = "Weight of edge" 50 | args.interestingtype = None 51 | args.maps = None 52 | args.borders = 'range' 53 | args.ally_match = 'collapse' 54 | args.owned = False 55 | args.trade = True 56 | args.speculative_version = 'CT' 57 | args.ally_count = 10 58 | args.json_data = False 59 | args.output = tempfile.gettempdir() 60 | args.mp_threads = 1 61 | args.debug_flag = False 62 | args.map_type = "classic" 63 | return args 64 | 65 | def _make_args_no_line(self) -> argparse.ArgumentParser: 66 | args = self._make_args() 67 | args.interestingline = None 68 | 69 | return args 70 | 71 | 72 | if __name__ == '__main__': 73 | unittest.main() 74 | -------------------------------------------------------------------------------- /PyRoute/Pathfinding/LandmarkSchemes/LandmarkAvoidHelper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Mar 06, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | import numpy as np 7 | 8 | 9 | class LandmarkAvoidHelper: 10 | 11 | TREE_ROOT = -1 12 | TREE_NONE = -100 13 | 14 | @staticmethod 15 | def calc_weights(distances, lobound): 16 | return distances - lobound 17 | 18 | @staticmethod 19 | def calc_sizes(weights, tree, landmarks): 20 | active_nodes = np.array(range(len(weights))) 21 | sizes = np.zeros(len(weights)) 22 | # filter out nodes who aren't part of a tree 23 | keep = tree[active_nodes] != LandmarkAvoidHelper.TREE_NONE 24 | active_nodes = active_nodes[keep] 25 | 26 | # spin through nodes in bulk, propagating weights upwards 27 | active_weights = weights[active_nodes] 28 | while 0 < len(active_nodes): 29 | # Directly iterating in python worked out faster than using nditer 30 | for index in range(len(active_nodes)): 31 | sizes[active_nodes[index]] += active_weights[index] 32 | keep = tree[active_nodes] != LandmarkAvoidHelper.TREE_ROOT 33 | active_nodes = active_nodes[keep] 34 | active_weights = active_weights[keep] 35 | active_nodes = tree[active_nodes] # Move up to immediate parents 36 | 37 | active_nodes = np.array(list(landmarks)) 38 | while 0 < len(active_nodes): 39 | sizes[active_nodes] = 0 40 | keep = tree[active_nodes] != LandmarkAvoidHelper.TREE_ROOT 41 | active_nodes = active_nodes[keep] 42 | active_nodes = tree[active_nodes] 43 | 44 | return sizes 45 | 46 | @staticmethod 47 | def traverse_sizes(sizes, rootnode, tree): 48 | active_nodes = np.array(range(len(sizes))) 49 | if LandmarkAvoidHelper.TREE_ROOT != tree[rootnode]: 50 | raise AssertionError("Selected root node " + str(rootnode) + " not marked as a tree root") 51 | choice = active_nodes[np.argmax(sizes)] 52 | kidvec = np.where(choice == tree) 53 | kidsizes = sizes[kidvec] 54 | maxkid = np.argmax(kidsizes) 55 | while 0 < len(kidsizes): 56 | choice = active_nodes[kidvec][maxkid] 57 | kidvec = np.where(choice == tree) 58 | kidsizes = sizes[kidvec] 59 | if 0 < len(kidsizes): 60 | maxkid = np.argmax(kidsizes) 61 | 62 | return choice 63 | -------------------------------------------------------------------------------- /PyRoute/Nobles.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Nov 29, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | 7 | 8 | class Nobles(object): 9 | __slots__ = 'nobles' 10 | 11 | codes = {'B': 'Knights', 12 | 'c': 'Baronets', 13 | 'C': 'Barons', 14 | 'D': 'Marquis', 15 | 'e': 'Viscounts', 16 | 'E': 'Counts', 17 | 'f': 'Dukes', 18 | 'F': 'Sector Dukes', 19 | 'G': 'Archdukes', 20 | 'H': 'Emperor'} 21 | 22 | key_list = list(codes.keys()) 23 | value_list = list(codes.values()) 24 | 25 | def __init__(self): 26 | self.nobles = {'Knights': 0, 27 | 'Baronets': 0, 28 | 'Barons': 0, 29 | 'Marquis': 0, 30 | 'Viscounts': 0, 31 | 'Counts': 0, 32 | 'Dukes': 0, 33 | 'Sector Dukes': 0, 34 | 'Archdukes': 0, 35 | 'Emperor': 0} 36 | 37 | def __str__(self): 38 | # If there's absolutely no nobles, return '-': 39 | if 0 == self.max_value: 40 | return '-' 41 | 42 | nobility = "" 43 | for rank, count in self.nobles.items(): 44 | if count > 0: 45 | nobility += Nobles.key_list[Nobles.value_list.index(rank)] 46 | return ''.join(sorted(nobility, key=lambda v: (v.lower(), v[0].isupper()))) 47 | 48 | def __getstate__(self): 49 | state = self.__dict__.copy() 50 | del state['codes'] 51 | return state 52 | 53 | def count(self, nobility) -> None: 54 | for code, rank in Nobles.codes.items(): 55 | if code in nobility: 56 | self.nobles[rank] += 1 57 | 58 | def accumulate(self, nobles) -> None: 59 | for rank, count in nobles.nobles.items(): 60 | self.nobles[rank] += count 61 | 62 | @property 63 | def max_value(self) -> int: 64 | return max(self.nobles.values()) 65 | 66 | @property 67 | def min_value(self) -> int: 68 | return min(self.nobles.values()) 69 | 70 | @property 71 | def sum_value(self) -> int: 72 | return sum(self.nobles.values()) 73 | 74 | def is_well_formed(self) -> tuple[bool, str]: 75 | msg = '' 76 | if 0 > self.max_value: 77 | msg = 'Noble count values cannot be negative' 78 | return False, msg 79 | 80 | return True, msg 81 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/Dagudashaag-subsector-reduced.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:21:52-08:00 3 | 4 | # Dagudashaag 5 | # -1,0 6 | 7 | # Name: Dagudashaag 8 | # Name: Dagudashag 9 | 10 | # Abbreviation: Dagu 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Dagudashaag sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Duncan Law-Green, Leighton Piper and Jae Campbell, and appears in Signal GK Magazine. 15 | 16 | # Author: Duncan Law-Green, Leighton Piper and Jae Campbell; Martin J. Dougherty 17 | # Source: Traveller 5 Second Survey 18 | 19 | # Subsector A: Mimu 20 | # Subsector B: Old Suns 21 | # Subsector C: Arnakhish 22 | # Subsector D: Iiradu 23 | # Subsector E: Shallows 24 | # Subsector F: Ushra 25 | # Subsector G: Khandi 26 | # Subsector H: Kuriishe 27 | # Subsector I: Zeda 28 | # Subsector J: Remnants 29 | # Subsector K: Pact 30 | # Subsector L: Gadde 31 | # Subsector M: Bolivar 32 | # Subsector N: Argi 33 | # Subsector O: Sapphyre 34 | # Subsector P: Laraa 35 | 36 | # Alleg: ImAp: "Third Imperium, Amec Protectorate" 37 | # Alleg: ImDv: "Third Imperium, Domain of Vland" 38 | # Alleg: ImLc: "Third Imperium, Lancian Cultural Region" 39 | 40 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 41 | ---- -------------------- --------- ------------------------------------- ------ ------- ------ ---- -- - --- -- ---- --------------- ----------------------------------------- 42 | 1728 Ekhugush A63698A-D Hi { 3 } (E8F+5) [BC7F] BE N - 312 8 ImDv M2 V M9 V 43 | 1729 Ushmegili D86A204-A Lo Wa { -1 } (810-3) [1138] B S - 722 14 ImDv G1 V 44 | 2123 Medurma A9D7954-C Hi An Cs Di(Miyavine) Asla1 S'mr0 { 3 } (G8E+1) [7C3A] BEF - - 823 12 ImDv G0 V Xb:1823 Xb:1926 Xb:2223 Xb:2225 Xb:2322 45 | 2122 Chandra's World C9966AA-7 Ag Ni Da { -1 } (853+1) [8579] BC - A 803 8 ImDv K4 V 46 | 2123 Kediiga B778411-8 Ni Pa { -1 } (832-5) [1314] Bc - - 920 9 ImDv G6 V 47 | 2127 Thalassa D56A557-9 Ni Wa Pr { -2 } (C42-2) [5359] Bc - - 423 11 ImDv K8 V 48 | 2130 Maiden B43069B-C De Na Ni Po Da { 1 } (D55+3) [877E] B N A 814 13 ImDv M0 V M7 V Xb:2028 Xb:2531 49 | -------------------------------------------------------------------------------- /sectorlist.txt: -------------------------------------------------------------------------------- 1 | Afawahisa 2 | Aktifao 3 | Aldebaran 4 | Alpha Crucis 5 | Amderstun 6 | Amdukan 7 | Angfutsag 8 | Antares 9 | Astron 10 | Ataurre 11 | Avereguar 12 | Banners 13 | Bar'kakr 14 | Blaskon 15 | Canopus 16 | Centrax 17 | Chit Botshti 18 | Core 19 | Corridor 20 | Crucis Margin 21 | Dagudashaag 22 | Daibei 23 | Dark Nebula 24 | Darret 25 | Datsatl 26 | Delphi 27 | Deneb 28 | Dfotseth 29 | Dhuerorrg 30 | Diaspora 31 | Drakken 32 | Ealiyasiyw 33 | Eiaplial 34 | Empty Quarter 35 | Esai'yo 36 | Extolian 37 | Fa Dzaets 38 | Faoheiroi'iyhao 39 | Far Frontiers 40 | Finggvakhou 41 | Folgore 42 | Fornast 43 | Ftaoiyekyu 44 | Fulani 45 | Gashikan 46 | Gateway 47 | Genfert 48 | Gh!hken 49 | Ghoekhnael 50 | Glimmerdrift Reaches 51 | Gn'hk'r 52 | Grikr!ng 53 | Gur 54 | Gushemege 55 | Gvurrdon 56 | Gzaekfueg 57 | Gzirr!k'l 58 | Hadji 59 | Hanstone 60 | Harea 61 | Hinterworlds 62 | Hkakhaeaw 63 | Hlakhoi 64 | Ianshaplzdier 65 | Ilelish 66 | Ingukrax 67 | Iphigenaia 68 | Irugangog 69 | Itvikiastaf 70 | Iwahfuah 71 | K'trekreer 72 | Kaa G!'kul 73 | Karleaya 74 | Katoonah 75 | Kfazz Ghik 76 | Khaeaw 77 | Kharrthon 78 | Kidunal 79 | Kilong 80 | Knaeleng 81 | Knoellighz 82 | Kolire 83 | Kring Noor 84 | Kruse 85 | Ksinanirz 86 | Ktiin'gzat 87 | Langere 88 | Leonidae 89 | Ley Sector 90 | Lishun 91 | Listanaya 92 | Lorspane 93 | Lubbock 94 | Luretiir!girr 95 | Magyar 96 | Malorn 97 | Massilia 98 | Mbil!!gh 99 | Mendan 100 | Meshan 101 | Mikhail 102 | Mugheen't 103 | Neworld 104 | Ngathksirz 105 | Nooq 106 | Numbis 107 | Nuughe 108 | Oeghz Vaerrghr 109 | Old Expanses 110 | Phlange 111 | Phlask 112 | Porlock 113 | Provence 114 | Raakaan 115 | Reaver's Deep 116 | Reft Sector 117 | Rfigh 118 | Ricenden 119 | Riftspan Reaches 120 | RimReach 121 | Ruthless Veil 122 | Ruupiin 123 | Rzakki 124 | Sidiadl 125 | Solomani Rim 126 | Spangele 127 | Spica 128 | Spinward Marches 129 | Staihaia'yo 130 | Star's End 131 | Stiatlchepr 132 | Storr 133 | Tar'G'kell'p 134 | The Beyond 135 | Theron 136 | Theta Borealis 137 | Tienspevnekr 138 | Touchstone 139 | Treece 140 | Trenchans 141 | Trojan Reach 142 | Tsadra 143 | Tuglikki 144 | Uistilrao 145 | Ukaarriit!!b 146 | Un'k!!k'ng 147 | Ustral Quadrant 148 | Uuk 149 | Vanguard Reaches 150 | Veg Fergakh 151 | Verge 152 | Vland 153 | Waroatahe 154 | Windhorn 155 | Wrenton 156 | X'kug 157 | Xaagr 158 | Yiklerzdanzh 159 | Zao Kfeng Ig Grilokh 160 | Zarushagar 161 | Zdiedeiant 162 | Zhdant 163 | Zhiaqrqiats 164 | Ziafrplians 165 | Zortakh 166 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/Zarushagar-imbalanced-routes.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:37:26-08:00 3 | 4 | # Zarushagar 5 | # -1,-1 6 | 7 | # Name: Zarushagar 8 | 9 | # Abbreviation: Zaru 10 | 11 | # Milieu: M1105 12 | 13 | # Credits: Zarushagar sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Greg Videll. Portions appear in The Travellers Digest and the MegaTraveller Referee's Gaming Kit (Digest Group Publications, 1989). 14 | 15 | # Author: Greg Videll, Martin J. Dougherty 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Ibaru 19 | # Subsector B: Triton 20 | # Subsector C: Lagaar 21 | # Subsector D: Lode 22 | # Subsector E: Iimii 23 | # Subsector F: Khipge 24 | # Subsector G: Epshu 25 | # Subsector H: Gaussi 26 | # Subsector I: Mizar 27 | # Subsector J: Liasdi 28 | # Subsector K: Wolf 29 | # Subsector L: Oasis 30 | # Subsector M: Void 31 | # Subsector N: Allegro 32 | # Subsector O: Rhamsteppe 33 | # Subsector P: Mongrabi 34 | 35 | # Alleg: CsIm: "Client state, Third Imperium" 36 | # Alleg: ImDi: "Third Imperium, Domain of Ilelish" 37 | # Alleg: NaHu: "Non-Aligned, Human-dominated" 38 | 39 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 40 | ---- -------------------- --------- -------------------------- ------ ------- ------ ----- -- - --- -- ---- -------------- ---------------------------------------------- 41 | 3037 Ralik C765220-9 Lo Ga { -1 } (910-5) [1114] B - - 205 12 ImDi G7 V M7 V 42 | 3038 Noire D000556-7 As Ni Va { -3 } (741-4) [4246] B - - 715 13 ImDi K3 V 43 | 3040 Borderline B664756-B Ag Ri { 4 } (B6E+3) [6B4A] BCf N - 711 10 ImDi K3 V M7 V 44 | 3134 Pylon D556652-7 Ag Ni { -2 } (852-5) [2413] BC - - 403 9 ImDi M0 V 45 | 3135 Aquivda C565730-8 Ag Ri { 1 } (B69-3) [2813] BC S - 902 8 ImDi M2 V 46 | 3136 Gaidraa B6859A9-C Hi Ga Pr Darm6 { 4 } (B8F+5) [AD6D] BcEf W - 400 13 ImDi K3 V M5 V Xb:2937 Xb:3133 47 | -------------------------------------------------------------------------------- /Tests/Pyfakefs/baseTest.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import os 4 | import tempfile 5 | from pyfakefs.fake_filesystem_unittest import TestCase 6 | 7 | from PyRoute.Inputs.ParseStarInput import ParseStarInput 8 | from PyRoute.Utilities.UnpackFilename import UnpackFilename 9 | 10 | 11 | class baseTest(TestCase): 12 | 13 | # Set up logging information 14 | logger = logging.getLogger(__name__) 15 | logging.basicConfig(level=logging.DEBUG) 16 | 17 | def setUp(self) -> None: 18 | ParseStarInput.deep_space = {} 19 | 20 | def unpack_filename(self, filename: str) -> str: 21 | return UnpackFilename.unpack_filename(filename) 22 | 23 | def unpack_workdir(self, dirname: str) -> str: 24 | # try unpacked directory directly 25 | workdir = os.path.abspath(dirname) 26 | 27 | cwd = os.getcwd() 28 | if not os.path.isdir(workdir): 29 | workdir = os.path.abspath(cwd + dirname) 30 | 31 | return workdir 32 | 33 | def reset_logging(self) -> None: 34 | loggers = [logging.getLogger(name) for name in logging.root.manager.loggerDict] 35 | loggers.append(logging.getLogger()) 36 | for logger in loggers: 37 | handlers = logger.handlers[:] 38 | for handler in handlers: 39 | logger.removeHandler(handler) 40 | handler.close() 41 | logger.setLevel(logging.NOTSET) 42 | logger.propagate = True 43 | 44 | def _make_args(self) -> argparse.ArgumentParser: 45 | args = argparse.ArgumentParser(description='PyRoute input minimiser.') 46 | args.btn = 8 47 | args.max_jump = 2 48 | args.route_btn = 13 49 | args.pop_code = 'scaled' 50 | args.ru_calc = 'scaled' 51 | args.routes = 'trade' 52 | args.route_reuse = 10 53 | args.interestingline = "Weight of edge" 54 | args.interestingtype = None 55 | args.maps = None 56 | args.borders = 'range' 57 | args.ally_match = 'collapse' 58 | args.owned = False 59 | args.trade = True 60 | args.speculative_version = 'CT' 61 | args.ally_count = 10 62 | args.json_data = False 63 | args.output = tempfile.gettempdir() 64 | args.mp_threads = 1 65 | args.debug_flag = False 66 | args.map_type = "classic" 67 | return args 68 | 69 | def _make_args_no_line(self) -> argparse.ArgumentParser: 70 | args = self._make_args() 71 | args.interestingline = None 72 | 73 | return args 74 | -------------------------------------------------------------------------------- /PyRoute/Pathfinding/DistanceGraph.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Sep 23, 2023 3 | 4 | @author: CyberiaResurrection 5 | 6 | Thanks to @GamesByDavidE for original prototype and design discussions 7 | """ 8 | import copy 9 | 10 | import numpy as np 11 | from networkx.classes import Graph 12 | 13 | from PyRoute.Pathfinding.DistanceBase import DistanceBase 14 | 15 | 16 | class DistanceGraph(DistanceBase): 17 | 18 | __slots__ = '_arcs', '_positions', '_nodes', '_indexes', '_min_cost', '_min_indirect' 19 | 20 | def __init__(self, graph: Graph): 21 | super().__init__(graph) 22 | self._arcs = [ 23 | ( 24 | np.array(graph.adj[u], dtype=int), 25 | np.array([data['weight'] for data in list(graph.adj[u].values())], dtype=float), 26 | {v: k for (k, v) in enumerate(list(graph.adj[u].keys()))} 27 | ) 28 | for u in self._nodes 29 | ] 30 | self._min_cost = np.zeros(len(self._nodes)) 31 | self._min_indirect = np.zeros(len(self._nodes)) 32 | for i in range(0, len(self._nodes)): 33 | node_edges = self._arcs[i][1] 34 | if 0 < len(node_edges): 35 | self._min_cost[i] = min(node_edges) 36 | for i in range(0, len(self._nodes)): 37 | node_neighbours = self._arcs[i][0] 38 | if 0 < len(node_neighbours): 39 | self._min_indirect[i] = min(self._min_cost[node_neighbours]) 40 | 41 | def min_cost(self, target: int, indirect: bool = False) -> np.ndarray: 42 | min_cost = copy.deepcopy(self._min_cost) 43 | min_cost[target] = 0 44 | 45 | if indirect is not True: 46 | return min_cost 47 | 48 | min_indirect = copy.deepcopy(self._min_indirect) 49 | neighbours = self._arcs[target][0] # Skip neighbours of target and target itself in the indirect leg 50 | min_indirect[target] = 0 51 | min_indirect[neighbours] = 0 52 | 53 | min_cost += min_indirect 54 | 55 | return min_cost 56 | 57 | def lighten_edge(self, u: int, v: int, weight: float) -> None: 58 | if v not in self._arcs[u][2]: 59 | assert False 60 | self._lighten_arc(u, v, weight) 61 | self._lighten_arc(v, u, weight) 62 | if weight < self._min_cost[u]: 63 | self._min_cost[u] = weight 64 | if weight < self._min_cost[v]: 65 | self._min_cost[v] = weight 66 | 67 | self._min_indirect[self._arcs[u][0]] = np.fmin(self._min_indirect[self._arcs[u][0]], weight) 68 | self._min_indirect[self._arcs[v][0]] = np.fmin(self._min_indirect[self._arcs[v][0]], weight) 69 | -------------------------------------------------------------------------------- /Tests/BorderGeneration/Far Frontiers-partial-allymap-allymap.json: -------------------------------------------------------------------------------- 1 | {"(-176, 144)": "Na", "(-175, 141)": "Na", "(-175, 139)": "4Wor", "(-175, 137)": "Na", "(-174, 144)": "Prot", "(-174, 139)": "4Wor", "(-174, 138)": "4Wor", "(-174, 137)": "Na", "(-173, 145)": "Prot", "(-173, 142)": "Prot", "(-173, 139)": "4Wor", "(-173, 138)": "UnGa", "(-172, 144)": "Prot", "(-177, 144)": "Na", "(-176, 143)": "Na", "(-175, 143)": "Prot", "(-175, 144)": "Prot", "(-176, 145)": "Prot", "(-177, 145)": "Na", "(-176, 141)": "4Wor", "(-175, 140)": "4Wor", "(-174, 140)": "4Wor", "(-174, 141)": "4Wor", "(-175, 142)": "Na", "(-176, 142)": "Na", "(-176, 139)": "4Wor", "(-175, 138)": "Na", "(-176, 140)": "4Wor", "(-177, 139)": "4Wor", "(-176, 138)": "Na", "(-173, 137)": "4Wor", "(-177, 141)": "4Wor", "(-177, 140)": "4Wor", "(-178, 139)": null, "(-177, 138)": null, "(-176, 137)": "Na", "(-175, 136)": null, "(-174, 136)": null, "(-173, 136)": null, "(-172, 136)": null, "(-172, 137)": "4Wor", "(-172, 138)": "4Wor", "(-172, 139)": "4Wor", "(-173, 140)": "4Wor", "(-177, 142)": "4Wor", "(-178, 142)": "4Wor", "(-178, 141)": "4Wor", "(-178, 140)": "4Wor", "(-174, 143)": "Prot", "(-173, 143)": "Prot", "(-173, 144)": "Prot", "(-174, 145)": "Prot", "(-175, 145)": "Prot", "(-174, 142)": "Prot", "(-172, 142)": "Prot", "(-172, 143)": "Prot", "(-174, 146)": "Prot", "(-175, 146)": "Prot", "(-176, 146)": "Prot", "(-173, 141)": "4Wor", "(-172, 141)": "Prot", "(-171, 141)": "Prot", "(-171, 142)": "Prot", "(-171, 143)": "Prot", "(-171, 144)": "Prot", "(-172, 145)": "Prot", "(-173, 146)": "Prot", "(-174, 147)": "Prot", "(-175, 147)": "Prot", "(-176, 147)": "Prot", "(-177, 147)": "Prot", "(-177, 146)": "Prot", "(-171, 136)": null, "(-171, 137)": null, "(-171, 138)": "4Wor", "(-171, 139)": "4Wor", "(-172, 140)": "4Wor", "(-174, 135)": null, "(-173, 135)": null, "(-172, 135)": null, "(-171, 135)": null, "(-170, 135)": null, "(-170, 136)": null, "(-170, 137)": null, "(-170, 138)": null, "(-170, 139)": null, "(-171, 140)": "Prot", "(-177, 143)": "Na", "(-178, 143)": null, "(-171, 145)": "Prot", "(-172, 146)": "Prot", "(-173, 147)": "Prot", "(-170, 142)": "Prot", "(-170, 143)": "Prot", "(-170, 144)": "Prot", "(-170, 145)": "Prot", "(-171, 146)": "Prot", "(-172, 147)": "Prot", "(-173, 148)": "Prot", "(-174, 148)": "Prot", "(-175, 148)": "Prot", "(-176, 148)": "Prot", "(-170, 141)": "Prot", "(-169, 141)": null, "(-169, 142)": null, "(-169, 143)": null, "(-169, 144)": null, "(-169, 145)": null, "(-170, 146)": null, "(-171, 147)": null, "(-172, 148)": null, "(-173, 149)": null, "(-174, 149)": "Prot", "(-175, 149)": "Prot", "(-176, 149)": "Prot", "(-177, 149)": "Prot", "(-177, 148)": "Prot", "(-170, 140)": null, "(-169, 138)": null, "(-169, 139)": null, "(-169, 140)": null} -------------------------------------------------------------------------------- /Tests/DeltaFiles/xroute_routes_pass_2_4/Dagudashaag.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2021-11-28T10:21:52-08:00 3 | 4 | # Dagudashaag 5 | # -1,0 6 | 7 | # Name: Dagudashaag 8 | # Name: Dagudashag 9 | 10 | # Abbreviation: Dagu 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Dagudashaag sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Duncan Law-Green, Leighton Piper and Jae Campbell, and appears in Signal GK Magazine. 15 | 16 | # Author: Duncan Law-Green, Leighton Piper and Jae Campbell; Martin J. Dougherty 17 | # Source: Traveller 5 Second Survey 18 | 19 | # Subsector A: Mimu 20 | # Subsector B: Old Suns 21 | # Subsector C: Arnakhish 22 | # Subsector D: Iiradu 23 | # Subsector E: Shallows 24 | # Subsector F: Ushra 25 | # Subsector G: Khandi 26 | # Subsector H: Kuriishe 27 | # Subsector I: Zeda 28 | # Subsector J: Remnants 29 | # Subsector K: Pact 30 | # Subsector L: Gadde 31 | # Subsector M: Bolivar 32 | # Subsector N: Argi 33 | # Subsector O: Sapphyre 34 | # Subsector P: Laraa 35 | 36 | # Alleg: ImDv: "Third Imperium, Domain of Vland" 37 | 38 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar Routes 39 | ---- -------------------- --------- ------------------------------------- ------ ------- ------ ---- -- - --- -- ---- --------------- ----------------------------------------- 40 | 2124 Medurma A9D7954-C Hi An Cs Di(Miyavine) Asla1 S'mr0 { 3 } (G8E+1) [7C3A] BEF - - 823 12 ImDv G0 V Xb:1823 Xb:1926 Xb:2223 Xb:2225 Xb:2322 41 | 2526 Rraeghzoez B775452-C Ni Pa VargW { 1 } (934-3) [1518] Bc N - 212 7 ImDv K3 V M3 V 42 | 2927 Shea E651976-8 Hi Po { -1 } (E88-2) [8847] BE - - 303 6 ImDv M0 V M5 V 43 | 3228 Ishmurish D000200-B As Lo Va { -1 } (410-5) [1116] B S - 700 7 ImDv F6 V M1 V 44 | 3231 Mershemu D580411-7 De Ni { -3 } (631-5) [1113] B - - 903 12 ImDv G2 V 45 | 3235 Edraconis C594550-7 Ag Ni { -1 } (743-5) [1412] BC - - 903 12 ImDv M1 V M5 V 46 | 3239 Tutrii DA7A266-8 Lo Oc Mr { -3 } (610-4) [1147] B S - 102 7 ImDv G9 V M4 V 47 | -------------------------------------------------------------------------------- /PyRoute/Pathfinding/single_source_dijkstra.py: -------------------------------------------------------------------------------- 1 | """ 2 | Restarted single-source all-target Dijkstra shortest paths. 3 | 4 | Lifted from networkx repo (https://github.com/networkx/networkx/blob/main/networkx/algorithms/shortest_paths/weighted.py) 5 | as at Add note about using latex formatting in docstring in the contributor… (commit a63c8bd). 6 | 7 | Major modifications here are ripping out gubbins unrelated to the single-source all-targets case, handling cases 8 | where less than the entire shortest path tree needs to be regenerated due to a link weight change, and specialising to 9 | the specific input format used in PyRoute. 10 | 11 | """ 12 | import numpy as np 13 | 14 | try: 15 | from PyRoute.Pathfinding.single_source_dijkstra_core import dijkstra_core 16 | except ModuleNotFoundError: 17 | from PyRoute.Pathfinding.single_source_dijkstra_core_fallback import dijkstra_core 18 | except ImportError: 19 | from PyRoute.Pathfinding.single_source_dijkstra_core_fallback import dijkstra_core 20 | except AttributeError: 21 | from PyRoute.Pathfinding.single_source_dijkstra_core_fallback import dijkstra_core 22 | 23 | 24 | def implicit_shortest_path_dijkstra_distance_graph(graph, source, distance_labels, seeds=None, divisor=1, min_cost=None, max_labels=None) -> tuple: 25 | # return only distance_labels from the explicit version 26 | distance_labels, _, max_neighbour_labels = explicit_shortest_path_dijkstra_distance_graph(graph, source, 27 | distance_labels, seeds, 28 | divisor, 29 | min_cost=min_cost, 30 | max_labels=max_labels) 31 | return distance_labels, max_neighbour_labels 32 | 33 | 34 | def explicit_shortest_path_dijkstra_distance_graph(graph, source, distance_labels, seeds=None, divisor=1, min_cost=None, max_labels=None) -> tuple: 35 | if not isinstance(distance_labels, np.ndarray): 36 | raise ValueError("distance labels must be ndarray") 37 | 38 | # assumes that distance_labels is already setup 39 | if seeds is None: 40 | seeds = {source} 41 | 42 | seeds = list(seeds) 43 | min_cost = np.zeros(len(graph), dtype=float) if min_cost is None else min_cost 44 | max_neighbour_labels = max_labels if max_labels is not None else np.ones(len(graph), dtype=float) * float('+inf') 45 | 46 | arcs = graph._arcs 47 | 48 | return dijkstra_core(arcs, distance_labels, divisor, seeds, max_neighbour_labels, min_cost) 49 | -------------------------------------------------------------------------------- /RunSuite.txt: -------------------------------------------------------------------------------- 1 | # download the sector files 2 | python PyRoute/downloadsec.py --routes sectorlist.txt ../sectors 3 | 4 | # generate the maps and data 5 | nohup pypy PyRoute/route.py --borders erode --min-btn 15 --pop-code fixed --routes trade --max-jump=5 --speculative-version None --output /root/maps --input /root/sectors --sectors sectorlist.txt > route.log 2>&1 & 6 | 7 | # Upload the sector summaries. 8 | python PyRoute/WikiUploadPDF.py --input Runs/runs_map14 9 | 10 | # Update the xml files for TravellerMap 11 | python PyRoute/map_remap.py ../maps/stars.txt ../sectors/ ../maps 12 | 13 | #pypy PyRoute/route.py --borders erode --min-btn 15 --pop-code fixed --routes comm --max-jump 4 --route-reuse 3 --output sectors/review --input sectors/review --sectors sectors/review/reviewlist.txt 14 | 15 | #python PyRoute/route.py --borders erode --min-btn 13 --pop-code fixed --routes comm --no-subsector-maps --max-jump 6 --route-reuse 9 --output Runs/imperial_xroute --input sectors/Imperial --sectors sectorimplist.txt 16 | #python PyRoute/map_remap.py Runs/imperial_xroute/stars.txt sectors/Imperial Runs/imperial_xroute 17 | #python PyRoute/route.py --borders erode --min-btn 13 --pop-code fixed --routes trade --max-jump 10 --output maps_m1900 18 | 19 | # 20 | 21 | #python PyRoute/WikiUploadPDF.py --no-maps --no-secs --input test_data/full 22 | 23 | 24 | # Zarushagar update 25 | python PyRoute/WikiCreateWorlds.py @test_data/zarushagar/parameters > Zarushagar.data 26 | 27 | python PyRoute/WikiCreateWorlds.py sectors/full/Zarushagar.sec --search-disambig "(Za " --skip-list skip_processing.txt \ 28 | --category test_data/zarushagar/Darmine_Corporate_worlds.txt --category test_data/zarushagar/Independent_Duchy_of_Oasis_worlds.txt \ 29 | --category test_data/zarushagar/Zaimen,_Clammath_and_Hollings_worlds.txt \ 30 | --category test_data/zarushagar/Gushemege_Main_worlds.txt --category test_data/zarushagar/Massilia_Spur_worlds.txt \ 31 | --category test_data/zarushagar/Rimward_Link_worlds.txt --category test_data/zarushagar/Sylean_Main_worlds.txt \ 32 | --category test_data/zarushagar/Vilani_Run_worlds.txt \ 33 | --source test_data/zarushagar/Atlas-source.txt \ 34 | > Zarushagar.data 35 | 36 | 37 | # python PyRoute/route.py --borders erode --min-btn 13 --pop-code fixed --routes trade --max-jump=5 --output test_data 38 | 39 | python PyRoute/route.py --borders erode --min-btn 13 --pop-code fixed --routes trade --max-jump 10 --min-ally-count 0 --ally-match separate --output maps_m1900 sectors/1900/spinward\ marches.sec sectors/1900/gvurrdon.sec 40 | 41 | python3 PyRoute/route.py --borders erode --min-btn 13 --pop-code fixed --routes trade --max-jump 5 --min-ally-count 0 --ally-match separate --no-subsector-maps --owned-worlds \ 42 | --output maps_federation --input sectors/Federation --sectors sectors/Federation/Federation.txt -------------------------------------------------------------------------------- /Tests/DeltaFiles/xroute_routes_pass_1_2/Massilia.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2024-09-28T17:21:53-07:00 3 | 4 | # Massilia 5 | # 0,-1 6 | 7 | # Name: Massilia 8 | # Name: Massila 9 | # Name: Masilaa (vi) 10 | 11 | # Abbreviation: Mass 12 | 13 | # Milieu: M1105 14 | 15 | # Credits: Massilia sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Joe D. Fugate Sr. and appears The Travellers Digest and in Knightfall (GDW, 1990). 16 | 17 | # Source: Traveller 5 Second Survey 18 | 19 | # Subsector A: Kerr 20 | # Subsector B: Arar 21 | # Subsector C: Khisham 22 | # Subsector D: Zalucha 23 | # Subsector E: Keum 24 | # Subsector F: Vaait 25 | # Subsector G: Forquee 26 | # Subsector H: Palasha 27 | # Subsector I: Ten Suns 28 | # Subsector J: Shiwonee 29 | # Subsector K: Nes'Vra 30 | # Subsector L: Tooka 31 | # Subsector M: Annari 32 | # Subsector N: Shokee 33 | # Subsector O: Shimmer 34 | # Subsector P: Oreo 35 | 36 | # Alleg: ImDc: "Third Imperium, Domain of Sylea" 37 | 38 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 39 | ---- -------------------- --------- ----------------------------- ------ ------- ------ ---- -- - --- -- ---- -------------- 40 | 0904 Transvaal B557584-B Ag Ni { 2 } (B46+1) [3739] BC - - 404 16 ImDc M3 V M5 V 41 | 0908 Sargashad A994878-C Pa Ph Pi Pz Px Ht { 2 } (A7C+2) [8A5C] BcDe N A 400 13 ImDc G2 V M5 V 42 | 1203 Datrumna A562686-C Ni Ri Ht { 2 } (B56+1) [584B] BC - - 403 16 ImDc M1 V 43 | 1601 Suttas Belts A000200-F As Lo Va Ht { 1 } (411-3) [131A] B - - 100 6 ImDc M2 V 44 | 2001 Marfa B20079D-C Na Va Pi Pz Ht { 2 } (C6C+5) [B99G] BD - A 503 13 ImDc G3 V M4 V 45 | 2302 Elan B66889D-A Ri Pa Ph Pz { 3 } (F7C+5) [CB9E] BcCe N A 805 9 ImDc F7 V M6 V M7 V 46 | 2503 Euteneier B4227B8-B He Na Po Pi { 2 } (E6C+2) [795B] BD - - 414 9 ImDc M2 V M8 V 47 | 2902 Watia B545776-A Ag Pi { 3 } (F6C+2) [6A49] BCD - - 824 12 ImDc K1 V 48 | 0312 Rebin A88A436-F Ni Wa Ht { 1 } (834+1) [354E] B - - 202 8 ImDc F9 V M5 V 49 | 0414 Laisarlo B100758-E Na Va Pi Ht { 2 } (A6D+2) [795E] BD N - 101 14 ImDc M2 V M7 V 50 | 0711 Newport A427303-F Lo Ht { 1 } (B21-2) [142C] B - - 624 10 ImDc K4 V M9 V 51 | 0815 Keum A67198A-E He Hi In Cs Ht { 4 } (F8G+5) [BD7G] BEF - - 704 16 ImDc K0 V M4 V 52 | -------------------------------------------------------------------------------- /Tests/Outputs/testClipping.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from PyRoute.Outputs.Map import Map 3 | from PyRoute.Outputs.Cursor import Cursor 4 | 5 | 6 | class testClipping(unittest.TestCase): 7 | def setUp(self) -> None: 8 | self.min_pos = Cursor(25, 25) 9 | self.max_pos = Cursor(100, 100) 10 | 11 | def test_internal(self) -> None: 12 | start = Cursor(30, 30) 13 | end = Cursor(75, 75) 14 | clip_start, clip_end = Map.clipping(self.min_pos, self.max_pos, start, end) 15 | 16 | self.assertEqual(start, clip_start) 17 | self.assertEqual(end, clip_end) 18 | 19 | def test_external(self) -> None: 20 | start = Cursor(0, 0) 21 | end = Cursor(10, 10) 22 | clip_start, clip_end = Map.clipping(self.min_pos, self.max_pos, start, end) 23 | 24 | self.assertIsNone(clip_start) 25 | self.assertIsNone(clip_end) 26 | 27 | def test_top_line(self) -> None: 28 | start = Cursor(30, 25) 29 | end = Cursor(50, 25) 30 | clip_start, clip_end = Map.clipping(self.min_pos, self.max_pos, start, end) 31 | self.assertEqual(start, clip_start) 32 | self.assertEqual(end, clip_end) 33 | 34 | def test_top_clip(self) -> None: 35 | start = Cursor(30, 20) 36 | end = Cursor(50, 50) 37 | clip_start, clip_end = Map.clipping(self.min_pos, self.max_pos, start, end) 38 | self.assertEqual(int(clip_start.x), 33) 39 | self.assertEqual(clip_start.y, 25) 40 | self.assertEqual(clip_end.x, 50) 41 | self.assertEqual(clip_end.y, 50) 42 | 43 | def test_right_clip(self) -> None: 44 | start = Cursor(150, 40) 45 | end = Cursor(50, 50) 46 | clip_start, clip_end = Map.clipping(self.min_pos, self.max_pos, start, end) 47 | self.assertEqual(int(clip_start.x), 100) 48 | self.assertEqual(clip_start.y, 45) 49 | self.assertEqual(clip_end.x, 50) 50 | self.assertEqual(clip_end.y, 50) 51 | 52 | def test_left_clip(self) -> None: 53 | start = Cursor(15, 40) 54 | end = Cursor(50, 50) 55 | clip_start, clip_end = Map.clipping(self.min_pos, self.max_pos, start, end) 56 | self.assertEqual(int(clip_start.x), 25) 57 | self.assertEqual(int(clip_start.y), 42) 58 | self.assertEqual(clip_end.x, 50) 59 | self.assertEqual(clip_end.y, 50) 60 | 61 | def test_bottom_clip(self) -> None: 62 | start = Cursor(45, 150) 63 | end = Cursor(50, 50) 64 | clip_start, clip_end = Map.clipping(self.min_pos, self.max_pos, start, end) 65 | self.assertEqual(int(clip_start.x), 47) 66 | self.assertEqual(int(clip_start.y), 100) 67 | self.assertEqual(clip_end.x, 50) 68 | self.assertEqual(clip_end.y, 50) 69 | 70 | 71 | if __name__ == '__main__': 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /PyRoute/DeltaPasses/TwoLineReducer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Oct 03, 2023 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.DeltaPasses.BeyondLineReducer import BeyondLineReducer 7 | 8 | 9 | class TwoLineReducer(BeyondLineReducer): 10 | 11 | def run(self, singleton_only=False, first_segment=True) -> None: 12 | segment = self.reducer.sectors.lines 13 | 14 | # An interesting less-than-4-element list is 2-minimal by definition 15 | if 4 > len(segment): 16 | return 17 | 18 | best_sectors = self.reducer.sectors 19 | old_length = len(segment) 20 | sqrt = max(2, round(old_length ** 0.5)) 21 | gap = 1 if first_segment else sqrt 22 | maxgap = sqrt + 1 if first_segment else len(segment) 23 | 24 | while gap < min(maxgap, len(segment)): 25 | msg = "# of lines: " + str(len(best_sectors.lines)) + f", gap {gap}" 26 | self.reducer.logger.error(msg) 27 | interim_write = max(10, int((len(segment) - gap) ** 0.5)) 28 | interim_counter = 0 29 | 30 | i = 0 31 | j = i + gap 32 | while j < len(segment): 33 | lines_to_remove = [segment[i], segment[j]] 34 | temp_sectors = best_sectors.drop_lines(lines_to_remove) 35 | 36 | interesting, msg, _ = self.reducer._check_interesting(self.reducer.args, temp_sectors) 37 | 38 | # We've found a chunk of input and have _demonstrated_ its irrelevance, 39 | # empty that chunk, update best so far, and continue 40 | if interesting: 41 | best_sectors = temp_sectors 42 | segment = best_sectors.lines 43 | msg = "Reduction found: new input has " + str(len(best_sectors.lines)) + " lines" 44 | self.reducer.logger.error(msg) 45 | interim_counter += 1 46 | if interim_write == interim_counter: 47 | interim_counter = 0 48 | best_sectors.trim_empty_allegiances() 49 | self.write_files(best_sectors) 50 | else: 51 | i += 1 52 | j = i + gap 53 | 54 | if old_length > len(segment): 55 | best_sectors.trim_empty_allegiances() 56 | self.write_files(best_sectors) 57 | old_length = len(segment) 58 | 59 | gap += 1 60 | 61 | # now that the pass is done, update self.sectors with best reduction found 62 | self.reducer.sectors = best_sectors 63 | 64 | # At least one sector was shown to be irrelevant, write out the intermediate result 65 | if old_length > len(segment): 66 | self.reducer.sectors.trim_empty_allegiances() 67 | self.write_files() 68 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/xroute_routes_pass_1_4/Core.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2024-09-28T17:11:16-07:00 3 | 4 | # Core 5 | # 0,0 6 | 7 | # Name: Core 8 | # Name: Ukan (vi) 9 | 10 | # Abbreviation: Core 11 | 12 | # Milieu: M1105 13 | 14 | # Credits: Core sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). 15 | 16 | # Source: Traveller 5 Second Survey 17 | 18 | # Subsector A: Apge 19 | # Subsector B: Perite 20 | # Subsector C: Ameros 21 | # Subsector D: Shinkan 22 | # Subsector E: Sanches 23 | # Subsector F: Mekee 24 | # Subsector G: Core 25 | # Subsector H: Kaskii 26 | # Subsector I: Bunkeria 27 | # Subsector J: Cemplas 28 | # Subsector K: Chant 29 | # Subsector L: Dingtra 30 | # Subsector M: Cadion 31 | # Subsector N: Ch'naar 32 | # Subsector O: Dunea 33 | # Subsector P: Saregon 34 | 35 | # Alleg: ImDc: "Third Imperium, Domain of Sylea" 36 | # Alleg: ImSy: "Third Imperium, Sylean Worlds" 37 | 38 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 39 | ---- -------------------- --------- ------------------------------------- ------ ------- ------ ----- -- - --- -- ---- --------------- 40 | 2118 Capital A586A98-F Hi Cx (Syleans)5 Ht { 4 } (H9G+4) [AE5F] BEFG NW - 605 15 ImSy G2 V 41 | 2519 Riid Irman D000330-A As Lo Va { -1 } (920-5) [1215] B S - 604 7 ImDc G3 V 42 | 2821 Kuggar B552997-9 Hi Po { 2 } (F8C+2) [9B59] BE - - 304 7 ImDc M2 V 43 | 3123 Kankuup Ir B4247A8-B Pi { 3 } (E6D+3) [7A5B] BD NS - 814 10 ImDc M3 V 44 | 3226 Duuka B686576-B Ag Ni Ga Pr { 2 } (746+1) [474A] BcC N - 600 13 ImDc M2 V 45 | 3227 Ashash As B758677-A Ag Ni { 2 } (B56+2) [685A] BC - - 903 12 ImDc M2 V M6 V 46 | 2532 Mie Duur B310320-D Lo Da Ht { 2 } (822-2) [1518] B NS A 803 13 ImDc K6 V 47 | 2536 Duunpigamuur C511637-9 Ic Na Ni { -1 } (E53-1) [6559] B S - 624 9 ImDc K9 V M2 V 48 | 2639 Suurashuur B100403-E Ni Va Ht { 1 } (934-2) [152B] B S - 203 10 ImDc G9 V 49 | 2731 Milpa B6315AF-B Ni Po Da { 1 } (B45+5) [A6AG] B N A 404 14 ImDc G0 V 50 | 3040 Unlakhar B629997-E Hi In Ht { 4 } (E8G+4) [9D5E] BEf N - 603 10 ImDc K3 V 51 | 3131 Ashga A7C3874-D Fl Ph Ht { 2 } (E7D+1) [6A3B] Be - - 513 12 ImDc K2 V M6 V 52 | -------------------------------------------------------------------------------- /PyRoute/downloadsec.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Jun 3, 2014 3 | 4 | @author: tjoneslo 5 | """ 6 | import argparse 7 | import codecs 8 | import os 9 | import time 10 | import urllib.error 11 | import urllib.parse 12 | import urllib.request 13 | 14 | 15 | def get_url(url, sector, suffix, output_dir) -> bool: 16 | try: 17 | f = urllib.request.urlopen(url) 18 | except urllib.error.HTTPError as ex: 19 | print("get URL failed: {} -> {}".format(url, ex)) 20 | return False 21 | except urllib.error.URLError as ex: 22 | print("get URL failed: {} -> {}".format(url, ex)) 23 | return False 24 | 25 | encoding = f.headers['content-type'].split('charset=')[-1] 26 | content = f.read() 27 | if encoding == 'text/xml' or encoding == 'text/html': 28 | ucontent = str(content, 'utf-8').replace('\r\n', '\n') 29 | else: 30 | ucontent = str(content, encoding).replace('\r\n', '\n') 31 | 32 | path = os.path.join(output_dir, '%s.%s' % (sector, suffix)) 33 | 34 | with codecs.open(path, 'wb', 'utf-8') as out: 35 | out.write(ucontent) 36 | f.close() 37 | return True 38 | 39 | 40 | if __name__ == '__main__': 41 | 42 | parser = argparse.ArgumentParser(description='Download sector/metadata from TravellerMap') 43 | parser.add_argument('--routes', dest='routes', default=False, action='store_true', 44 | help='Include route information in the sector downloads') 45 | parser.add_argument('sector_list', help='List of sectors to download') 46 | parser.add_argument('output_dir', help='output directory for sector data and xml metadata') 47 | parser.add_argument('--milieu', default="M1105", help="Milieu of data to download") 48 | args = parser.parse_args() 49 | 50 | with codecs.open(args.sector_list, 'r', encoding="utf-8") as f: 51 | sectorsList = [line for line in f] 52 | 53 | for raw_sector in sectorsList: 54 | sector = raw_sector.rstrip() 55 | print('Downloading %s' % sector) 56 | params = {'sector': sector, 'type': 'SecondSurvey', "milieu": args.milieu} 57 | if args.routes: 58 | params['routes'] = '1' 59 | url_params = urllib.parse.urlencode(params) 60 | url = 'http://www.travellermap.com/api/sec?%s' % url_params 61 | 62 | success = get_url(url, sector, 'sec', args.output_dir) 63 | if not success: 64 | print("Retrying " + sector) 65 | get_url(url, sector, 'sec', args.output_dir) 66 | 67 | url_params = urllib.parse.urlencode({'sector': sector, 'accept': 'text/xml'}) 68 | url = 'http://travellermap.com/api/metadata?%s' % url_params 69 | success = get_url(url, sector, 'xml', args.output_dir) 70 | if not success: 71 | print("Retrying XML for " + sector) 72 | get_url(url, sector, 'sec', args.output_dir) 73 | 74 | time.sleep(5) 75 | -------------------------------------------------------------------------------- /Tests/Pyfakefs/AreaItems/testGalaxy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Nov 09, 2025 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | import os 7 | 8 | from PyRoute.AreaItems.Galaxy import Galaxy 9 | from PyRoute.DataClasses.ReadSectorOptions import ReadSectorOptions 10 | from Tests.Pyfakefs.baseTest import baseTest 11 | 12 | 13 | class testGalaxy(baseTest): 14 | 15 | def test_process_owned_worlds_1(self) -> None: 16 | sourcefile = self.unpack_filename('DeltaFiles/Zarushagar-Ibara.sec') 17 | 18 | args = self._make_args() 19 | args.routes = 'trade' 20 | args.route_btn = 15 21 | readparms = ReadSectorOptions(sectors=[sourcefile], pop_code=args.pop_code, ru_calc=args.ru_calc, 22 | route_reuse=args.route_reuse, trade_choice=args.routes, route_btn=args.route_btn, 23 | mp_threads=args.mp_threads, debug_flag=args.debug_flag, fix_pop=False, 24 | deep_space={}, map_type=args.map_type) 25 | 26 | galaxy = Galaxy(min_btn=15, max_jump=4) 27 | galaxy.read_sectors(readparms) 28 | galaxy.generate_routes() 29 | galaxy.output_path = args.output 30 | galaxy.set_borders(args.borders, args.ally_match) 31 | 32 | foostar = galaxy.star_mapping[24] 33 | foostar.ownedBy = 'Dagu-0' 34 | 35 | self.setUpPyfakefs(allow_root_user=False) 36 | namepath = args.output + '/owned-worlds-names.csv' 37 | listpath = args.output + '/owned-worlds-list.csv' 38 | 39 | galaxy.process_owned_worlds() 40 | 41 | self.assertTrue(os.path.exists(namepath)) 42 | self.assertTrue(os.path.exists(listpath)) 43 | 44 | expected_name = [ 45 | '"Woden (Zarushagar 0306)", "Mr", "Strela (Zarushagar 0407)", "Imled ' 46 | '(Zarushagar 0406)", "Miller\'s World (Zarushagar 0607)", "Makkus (Zarushagar 0304)"\n', 47 | '"Aslungi (Zarushagar 0605)", "None", "Gishin (Zarushagar 0804)", "Miller\'s ' 48 | 'World (Zarushagar 0607)", "Strela (Zarushagar 0407)", "Imled (Zarushagar 0406)"\n', 49 | '"Airvae (Zarushagar 0801)", "Mr", "Gishin (Zarushagar 0804)", "Nedadzia (Zarushagar 0701)"\n', 50 | '"Ginshe (Zarushagar 0805)", "Gishin (Zarushagar 0804)", "Gishin (Zarushagar ' 51 | '0804)", "Miller\'s World (Zarushagar 0607)", "Strela (Zarushagar 0407)", "Imled (Zarushagar 0406)"\n' 52 | ] 53 | expected_list = [ 54 | '"Zaru", "0306", "Mr", "O:0407", "O:0406", "O:0607", "O:0304"\n', 55 | '"Zaru", "0605", "None", "O:0804", "O:0607", "O:0407", "O:0406"\n', 56 | '"Zaru", "0801", "Mr", "O:0804", "O:0701"\n', 57 | '"Zaru", "0805", "Gishin (Zarushagar 0804)", "O:0804", "O:0607", "O:0407", "O:0406"\n'] 58 | with open(namepath, 'r', encoding='utf-8') as f: 59 | name_contents = f.readlines() 60 | self.assertEqual(expected_name, name_contents) 61 | with open(listpath, 'r', encoding='utf-8') as f: 62 | list_contents = f.readlines() 63 | self.assertEqual(expected_list, list_contents) 64 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/duplicate_node_blowup/Verge.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2023-12-23T00:59:47-08:00 3 | 4 | # Verge 5 | # -3,-1 6 | 7 | # Name: Verge 8 | # Name: Khtiyhkokaeiw (as) 9 | # Name: Shaakasi (vi) 10 | 11 | # Abbreviation: Verg 12 | 13 | # Milieu: M1105 14 | 15 | # Credits: Verge sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). 16 | 17 | # Source: Traveller 5 Second Survey 18 | 19 | # Subsector A: Piimigeka 20 | # Subsector B: Draco Abyss 21 | # Subsector C: Mimiuudlika 22 | # Subsector D: Nuzuu 23 | # Subsector E: Shukhuarmiushruu 24 | # Subsector F: Dark Chasm 25 | # Subsector G: Biika 26 | # Subsector H: Miruu 27 | # Subsector I: Arkhukamier 28 | # Subsector J: Khukaruu 29 | # Subsector K: Likagemika 30 | # Subsector L: Khulikhu 31 | # Subsector M: Khuug 32 | # Subsector N: Inugzuu 33 | # Subsector O: Imkhuruuka 34 | # Subsector P: Naakhulige 35 | 36 | # Alleg: AsMw: "Aslan Hierate, single multiple-world clan dominates" 37 | # Alleg: AsTv: "Aslan Hierate, Tlaukhu vassal clan dominates" 38 | # Alleg: AsWc: "Aslan Hierate, single one-world clan dominates" 39 | # Alleg: CsIm: "Client state, Third Imperium" 40 | # Alleg: ImDi: "Third Imperium, Domain of Ilelish" 41 | # Alleg: NaHu: "Non-Aligned, Human-dominated" 42 | 43 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 44 | ---- -------------------- --------- -------------------- ------ ------- ------ ---- -- - --- -- ---- -------------- 45 | 2805 Lagi B436999-E Hi { 3 } (F8F+4) [AC6F] - - - 100 1 ImDi K9 V 46 | 2902 Joe's World E681105-8 Lo { -3 } (600-5) [1136] - - - 100 1 ImDi M1 V 47 | 2903 Serafim C538230-A Lo { 0 } (810-4) [1215] - - - 100 1 ImDi K5 V 48 | 2908 Tlaq B54A9DD-C Hi In Pz Wa { 4 } (C8F+5) [DD9G] - - - 100 1 ImDi K2 V 49 | 3210 Inkha C5769AA-A Hi In Pz { 3 } (H8D+5) [BC7C] - - - 100 1 ImDi K3 V 50 | 2519 Beraugaus B4338BC-C Na Ph Po Pz { 2 } (F7C+5) [BA8F] - - - 100 1 ImDi G2 V 51 | 2612 Tripolis B885A98-E Ga Hi { 4 } (H9G+4) [AE5E] - NW - 100 1 ImDi K9 V 52 | 2811 Sarlee A310746-C Na Pi { 2 } (C6C+1) [694B] - N - 100 1 ImDi G1 IV 53 | 2820 Ukuna C77A342-A Lo Wa { 0 } (920-4) [1316] - - - 100 1 ImDi M1 V 54 | 2911 Ksandi B432856-D Na Ph Po { 2 } (E7D+1) [7A4C] - - - 100 1 ImDi M1 V 55 | 2916 Uranion D4319BD-A Hi Na Po Pz { 1 } (D8B+5) [DA9E] - S - 100 1 ImDi M1 V 56 | 2919 Pippin A6377BB-D Pz { 2 } (D6D+4) [997F] - - - 100 1 ImDi M0 V 57 | 3113 Ellas B876355-C Lo { 1 } (521-1) [143A] - - - 100 1 ImDi K4 V 58 | -------------------------------------------------------------------------------- /Tests/DeltaFiles/xroute_routes_pass_1_4/Massilia.sec: -------------------------------------------------------------------------------- 1 | # Generated by https://travellermap.com 2 | # 2024-09-28T17:21:53-07:00 3 | 4 | # Massilia 5 | # 0,-1 6 | 7 | # Name: Massilia 8 | # Name: Massila 9 | # Name: Masilaa (vi) 10 | 11 | # Abbreviation: Mass 12 | 13 | # Milieu: M1105 14 | 15 | # Credits: Massilia sector was designed by Marc W. Miller and appears in Atlas of the Imperium (GDW, 1984). It was refined by Joe D. Fugate Sr. and appears The Travellers Digest and in Knightfall (GDW, 1990). 16 | 17 | # Source: Traveller 5 Second Survey 18 | 19 | # Subsector A: Kerr 20 | # Subsector B: Arar 21 | # Subsector C: Khisham 22 | # Subsector D: Zalucha 23 | # Subsector E: Keum 24 | # Subsector F: Vaait 25 | # Subsector G: Forquee 26 | # Subsector H: Palasha 27 | # Subsector I: Ten Suns 28 | # Subsector J: Shiwonee 29 | # Subsector K: Nes'Vra 30 | # Subsector L: Tooka 31 | # Subsector M: Annari 32 | # Subsector N: Shokee 33 | # Subsector O: Shimmer 34 | # Subsector P: Oreo 35 | 36 | # Alleg: ImDc: "Third Imperium, Domain of Sylea" 37 | 38 | Hex Name UWP Remarks {Ix} (Ex) [Cx] N B Z PBG W A Stellar 39 | ---- -------------------- --------- ----------------------------- ------ ------- ------ ---- -- - --- -- ---- -------------- 40 | 3203 Freelund E792743-6 He Pi { -2 } (965-5) [4523] BD - - 302 10 ImDc K3 V 41 | 3207 Hamagami B300245-D Lo Va Ht { 1 } (711-1) [133B] B - - 503 8 ImDc M3 V 42 | 0720 Amaya A100321-F Lo Va Ht { 1 } (B21-3) [141B] B - - 924 9 ImDc K9 III D 43 | 0815 Keum A67198A-E He Hi In Cs Ht { 4 } (F8G+5) [BD7G] BEF - - 704 16 ImDc K0 V M4 V 44 | 0816 Capson B551459-C Ni Po Ht { 1 } (C34+2) [556D] B - - 233 13 ImDc F7 V 45 | 3211 Porto B556AEC-E Hi Pz Ht { 3 } (H9F+5) [DD8H] BE - A 114 12 ImDc M1 V 46 | 3215 Remundo B889468-C Ni Mr Ht { 1 } (D34+1) [455C] B N - 325 10 ImDc G1 V 47 | 3219 Letycia B684113-B Lo Da { 1 } (701-2) [1228] B - A 904 10 ImDc F5 V 48 | 1122 Elemza E10046B-9 Ni Va O:1019 { -2 } (931+1) [627B] B - - 303 7 ImDc M1 III 49 | 1424 Mag' Lphr B100435-E Ni Va Ht { 2 } (935+1) [263C] B W - 703 8 ImDc M0 V 50 | 1428 Nindaro B9896BB-A Ni Ri Da { 2 } (856+4) [887C] BC - A 300 6 ImDc G0 V 51 | 1731 Myimpia B784202-B Lo { 2 } (812-2) [1417] B NS - 222 13 ImDc G2 V 52 | 2132 Honatty C545851-9 Pa Ph Pi { 0 } (E79-4) [4815] BcDe - - 104 10 ImDc F8 V 53 | 2531 Luong A500596-D Ni Va Da Ht { 1 } (C45+1) [464C] B - A 214 16 ImDc M3 V 54 | 2931 Amanita B100587-D Ni Va Ht { 1 } (A45+1) [565D] B - - 603 13 ImDc G1 V 55 | -------------------------------------------------------------------------------- /PyRoute/AreaItems/Subsector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 21 Jul, 2024 3 | 4 | @author: CyberiaResurrection 5 | """ 6 | from PyRoute.AreaItems.AreaItem import AreaItem 7 | 8 | 9 | class Subsector(AreaItem): 10 | def __init__(self, name, position, sector): 11 | super(Subsector, self).__init__(name) 12 | self.positions = ["ABCD", "EFGH", "IJKL", "MNOP"] 13 | self.sector = sector 14 | self.position = position 15 | self.spinward = None 16 | self.trailing = None 17 | self.coreward = None 18 | self.rimward = None 19 | self.dx = sector.dx 20 | self.dy = sector.dy 21 | self._wiki_name = Subsector.set_wiki_name(name, sector.name, position) 22 | 23 | # For the JSONPickel work 24 | def __getstate__(self): 25 | state = self.__dict__.copy() 26 | del state['sector'] 27 | del state['spinward'] 28 | del state['trailing'] 29 | del state['coreward'] 30 | del state['rimward'] 31 | del state['alg_sorted'] 32 | del state['positions'] 33 | return state 34 | 35 | @staticmethod 36 | def set_wiki_name(name, sector_name, position) -> str: 37 | if len(name) == 0: 38 | return "{0} location {1}".format(sector_name, position) 39 | else: 40 | if "(" in name: 41 | return '[[{0} Subsector|{1}]]'.format(name, name[:-7]) 42 | else: 43 | return '[[{0} Subsector|{0}]]'.format(name) 44 | 45 | def wiki_title(self) -> str: 46 | return '{0} - {1}'.format(self.wiki_name(), self.sector.wiki_name()) 47 | 48 | def subsector_name(self) -> str: 49 | if len(self.name) == 0: 50 | return "Location {}".format(self.position) 51 | else: 52 | return self.name[:-9].strip() if self.name.endswith('Subsector') else self.name 53 | 54 | def set_bounding_subsectors(self) -> None: 55 | posrow = 0 56 | for row in self.positions: 57 | if self.position in row: 58 | pos = self.positions[posrow].index(self.position) # pragma: no mutate 59 | break 60 | posrow += 1 61 | 62 | if posrow == 0: 63 | self.coreward = self.sector.coreward.subsectors[self.positions[3][pos]] if self.sector.coreward else None 64 | else: 65 | self.coreward = self.sector.subsectors[self.positions[posrow - 1][pos]] 66 | 67 | if pos == 0: 68 | self.spinward = self.sector.spinward.subsectors[self.positions[posrow][3]] if self.sector.spinward else None 69 | else: 70 | self.spinward = self.sector.subsectors[self.positions[posrow][pos - 1]] 71 | 72 | if posrow == 3: 73 | self.rimward = self.sector.rimward.subsectors[self.positions[0][pos]] if self.sector.rimward else None 74 | else: 75 | self.rimward = self.sector.subsectors[self.positions[posrow + 1][pos]] 76 | 77 | if pos == 3: 78 | self.trailing = self.sector.trailing.subsectors[self.positions[posrow][0]] if self.sector.trailing else None 79 | else: 80 | self.trailing = self.sector.subsectors[self.positions[posrow][pos + 1]] 81 | --------------------------------------------------------------------------------