sctmgtool 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sctmgtool/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 Łukasz Gieryk
3
+
4
+ __version__ = "0.3.1"
sctmgtool/__main__.py ADDED
@@ -0,0 +1,15 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 Łukasz Gieryk
3
+
4
+ import logging
5
+ from multiprocessing import freeze_support
6
+ from sctmgtool.tools import process_unit_list
7
+ from sctmgtool.tk import tkinter_main
8
+ from sctmgtool.units import ALL_UNITS
9
+
10
+
11
+ if __name__ == "__main__":
12
+ freeze_support()
13
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
14
+ process_unit_list(ALL_UNITS)
15
+ tkinter_main(ALL_UNITS)
sctmgtool/base.py ADDED
@@ -0,0 +1,257 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 Łukasz Gieryk
3
+
4
+ from enum import Flag, Enum, auto
5
+ from typing import NamedTuple, Callable
6
+ from dataclasses import dataclass
7
+ import enum
8
+
9
+ # pylint: disable=invalid-name
10
+
11
+
12
+ class Faction(Flag):
13
+ Zerg = auto()
14
+ Terran = auto()
15
+ Protoss = auto()
16
+
17
+
18
+ # fmt: off
19
+ class Tag(Flag):
20
+ _None = 0b0000000000000000000000000000000000000000000000
21
+ Biological = 0b0000000000000000000000000000000000000000000001
22
+ Mechanical = 0b0000000000000000000000000000000000000000000010
23
+ Flying = 0b0000000000000000000000000000000000000000000100
24
+ Ground = 0b0000000000000000000000000000000000000000001000
25
+ Light = 0b0000000000000000000000000000000000000000010000
26
+ Armoured = 0b0000000000000000000000000000000000000000100000
27
+ Psionic = 0b0000000000000000000000000000000000000001000000
28
+ Sidearm = 0b0000000000000000000000000000000000000010000000
29
+ Specialist = 0b0000000000000000000000000000000000000100000000
30
+ _AntiEvade = 0b0000000000000000000000000000000000001000000000
31
+ AntiEvade1 = 0b0000000000000000000000000000000000011000000000
32
+ AntiEvade2 = 0b0000000000000000000000000000000000101000000000
33
+ AntiEvade3 = 0b0000000000000000000000000000000001001000000000
34
+ _Precision = 0b0000000000000000000000000000000010000000000000
35
+ Precision1 = 0b0000000000000000000000000000000110000000000000
36
+ Precision2 = 0b0000000000000000000000000000001010000000000000
37
+ Precision3 = 0b0000000000000000000000000000010010000000000000
38
+ Pinpoint = 0b0000000000000000000000000000100000000000000000
39
+ _LockedIn = 0b0000000000000000000000000001000000000000000000
40
+ LockedIn6 = 0b0000000000000000000001000001000000000000000000
41
+ _PierceArmoured = 0b0000000000000000000010000000000000000000000000
42
+ PierceArmoured2 = 0b0000000000000000001010000000000000000000000000
43
+ PierceArmoured3 = 0b0000000000000000010010000000000000000000000000
44
+ _PierceLight = 0b0000000000000000100000000000000000000000000000
45
+ PierceLight2 = 0b0000000000000010100000000000000000000000000000
46
+ _Tough = 0b0000000000001000000000000000000000000000000000
47
+ Tough1 = 0b0000000000011000000000000000000000000000000000
48
+ Tough2 = 0b0000000000101000000000000000000000000000000000
49
+ Unique = 0b0000000001000000000000000000000000000000000000
50
+ IndirectFire = 0b0000000010000000000000000000000000000000000000
51
+ Bulky = 0b0000000100000000000000000000000000000000000000
52
+ BurstFire = 0b0000001000000000000000000000000000000000000000
53
+ _CriticalHit = 0b0000010000000000000000000000000000000000000000
54
+ CriticalHit2 = 0b0001010000000000000000000000000000000000000000
55
+ Instant = 0b0010000000000000000000000000000000000000000000
56
+ # fmt: on
57
+
58
+ def __str__(self):
59
+ masked_value = self.value & ~Tag(Tag._AntiEvade | Tag._Precision | Tag._PierceArmoured | Tag._Tough).value
60
+ first = True
61
+ ret = ""
62
+ for name, member in Tag.__members__.items():
63
+ if not name.startswith("_") and member.value & masked_value:
64
+ if not first:
65
+ ret += "|"
66
+ first = False
67
+ ret += name
68
+ return ret
69
+
70
+ @staticmethod
71
+ def _lsb(val):
72
+ return (val & -val).bit_length() - 1
73
+
74
+ def sibling_bits(self):
75
+ siblings = 0
76
+ for _name, member in Tag.__members__.items():
77
+ if member.value & self.value:
78
+ siblings |= member.value
79
+ return siblings
80
+
81
+ def _extract(self, tag):
82
+ if tag not in self:
83
+ return 0
84
+ masked = ~((tag.value << 1) - 1) & self.value
85
+ return Tag._lsb(masked) - Tag._lsb(tag.value)
86
+
87
+ def anti_evade(self):
88
+ return self._extract(Tag._AntiEvade)
89
+
90
+ def precision(self):
91
+ return self._extract(Tag._Precision)
92
+
93
+ def pierce_armoured(self):
94
+ return self._extract(Tag._PierceArmoured)
95
+
96
+ def pierce_light(self):
97
+ return self._extract(Tag._PierceLight)
98
+
99
+ def tough(self):
100
+ return self._extract(Tag._Tough)
101
+
102
+ def critical_hit(self):
103
+ return self._extract(Tag._CriticalHit)
104
+
105
+
106
+ class SurgeDie(Enum):
107
+ @enum.nonmember
108
+ class _Inner(NamedTuple):
109
+ d3:int
110
+ d6:int
111
+ add:int
112
+
113
+ D3 = _Inner(1, 0, 0)
114
+ D6 = _Inner(0, 1, 0)
115
+ D3P1 = _Inner(1, 0, 1)
116
+
117
+ def __str__(self):
118
+ return self.name
119
+
120
+
121
+ @dataclass
122
+ class Weapon():
123
+ name: str
124
+ range: (int | str)
125
+ target: Tag
126
+ rate_of_attack: int
127
+ hit: int
128
+ surge: Tag
129
+ surge_die: SurgeDie
130
+ damage: int
131
+ exchange_for:str = ""
132
+ # pylint: disable-next=protected-access
133
+ tags:Tag = Tag._None
134
+
135
+ def add_tag(self, tag:Tag):
136
+ if tag & self.tags: # replace the existing value-tag with a new value, e.g., Pierce1 -> Pierce2
137
+ new = Tag(self.tags.value & ~tag.sibling_bits())
138
+ self.tags = new
139
+ self.tags |= tag
140
+
141
+ def add_surge_tag(self, tag:Tag):
142
+ if self.surge is None:
143
+ self.surge = tag
144
+ else:
145
+ self.surge |= tag
146
+
147
+ def set_surge_die(self, die:SurgeDie):
148
+ self.surge_die = die
149
+
150
+ def buff_roa(self, val:int):
151
+ self.rate_of_attack += val
152
+
153
+ def buff_hit(self, val:int):
154
+ self.hit = max(2, self.hit - val)
155
+
156
+ def set_damage(self, val:int):
157
+ self.damage = val
158
+
159
+ def type_letter(self):
160
+ return self.range if self.range in ("E", "C") else "R"
161
+
162
+ def __str__(self):
163
+ ef = f" FOR:{self.exchange_for}" if self.exchange_for != "" else ""
164
+ surge = f" {str(self.surge or ""):<16} {str(self.surge_die or ""):<4}"
165
+
166
+ if Tag.Flying in self.target and Tag.Ground in self.target:
167
+ tgt = "GF"
168
+ else:
169
+ tgt = "G " if Tag.Ground in self.target else "F "
170
+
171
+ return f"{self.name:<30} {self.range:>2} {tgt} {self.rate_of_attack}D6 {self.hit}+ {surge} DMG:{self.damage} {str(self.tags):<20}{ef}"
172
+
173
+
174
+ class Range(NamedTuple):
175
+ start: int
176
+ stop: int
177
+
178
+
179
+ class Squad(NamedTuple):
180
+ models: Range
181
+ supply: int
182
+ points: int
183
+
184
+
185
+ @dataclass
186
+ class Speed():
187
+ unit: int
188
+ model: int
189
+
190
+ def __str__(self):
191
+ return f"{self.unit}/{self.model}"
192
+
193
+
194
+ @dataclass
195
+ class Cost():
196
+ small: int|None = None
197
+ large: int|None = None
198
+ points: int|None = None
199
+
200
+ def __str__(self):
201
+ points = f"/{self.points}P" if self.points else ""
202
+ return f"{self.small or '-'}/{self.large or '-'}{points}"
203
+
204
+
205
+ class Hook(Enum):
206
+ ModifyOwner = auto()
207
+ RollPoolsInitiated = auto()
208
+
209
+
210
+ class Upgrade():
211
+ class Type(Flag):
212
+ Other = auto()
213
+ Defensive = auto()
214
+ Offensive = auto()
215
+
216
+ def __init__(self, name:str, /, apply:Callable|dict=None, *, message="", cost=None, upgrade_type=None):
217
+ if apply is Upgrade.activate_weapon:
218
+ def wrapper(unit):
219
+ return Upgrade.activate_weapon(unit, name)
220
+ apply = wrapper
221
+ message = "Upgrade weapon"
222
+
223
+ if callable(apply):
224
+ apply = {Hook.ModifyOwner: apply}
225
+ else:
226
+ assert isinstance(apply, dict)
227
+
228
+ self.name = name
229
+ self.apply = apply
230
+ self.upgrade_type = upgrade_type
231
+ self.message = message
232
+ self.cost = cost
233
+
234
+ def __str__(self):
235
+ return f"{self.name} {self.cost or "-"} {self.message}"
236
+
237
+ @staticmethod
238
+ def activate_weapon(unit, weapon_name:str):
239
+ unit.activate_weapon(weapon_name)
240
+
241
+
242
+ @dataclass
243
+ class Unit():
244
+ faction: Faction
245
+ name: str
246
+ shield: int
247
+ speed: Speed
248
+ evade: int
249
+ armour: int
250
+ hit_points: int
251
+ weapons: tuple[Weapon]
252
+ squad: tuple[Squad]
253
+ tags: Tag
254
+ upgrades: tuple[Upgrade]
255
+
256
+ def __str__(self):
257
+ return f"{self.name} SHLD:{self.shield} EVA:{self.evade}+ ARM:{self.armour}+ HP:{self.hit_points} {self.tags}"
sctmgtool/cache.py ADDED
@@ -0,0 +1,80 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 Łukasz Gieryk
3
+
4
+ import pickle
5
+ from multiprocessing import Process, Manager, Lock, Event, Value
6
+ from pathlib import Path
7
+ import hashlib
8
+ import logging
9
+ import sctmgtool
10
+
11
+
12
+ class Cache:
13
+ MAJOR_REVISION = hashlib.sha256(sctmgtool.__version__.encode("utf-8")).hexdigest()
14
+ FILENAME = "cache.pkl"
15
+
16
+ def __init__(self, /, minor_revision: int, save_path: Path):
17
+ manager = Manager()
18
+ self.revision = (self.MAJOR_REVISION, minor_revision)
19
+ self.save_file_path = save_path / Cache.FILENAME
20
+
21
+ try:
22
+ logging.info("Cache path: %s", self.save_file_path)
23
+ with open(self.save_file_path, "rb") as file:
24
+ rev, data = pickle.load(file)
25
+ if rev == self.revision:
26
+ self.data = manager.dict(data)
27
+ else:
28
+ logging.warning("Cache version error; starting a new one from scratch")
29
+ self.data = manager.dict()
30
+ except (FileNotFoundError, AttributeError):
31
+ logging.warning("Cache access error; starting a new one from scratch")
32
+ self.data = manager.dict()
33
+
34
+ self.lock = Lock()
35
+ self.stop_event = Event()
36
+ self.workers = []
37
+ self.new_inserts = Value("i", 0)
38
+
39
+ def start_worker_func(self, worker_func: callable, arguments=None):
40
+ if arguments is None:
41
+ worker = Process(target=worker_func)
42
+ else:
43
+ worker = Process(target=worker_func, args=arguments)
44
+ worker.start()
45
+ self.workers.append(worker)
46
+
47
+ def stop_and_save(self):
48
+ self.stop_event.set()
49
+
50
+ for worker in self.workers:
51
+ worker.join()
52
+
53
+ if self.new_inserts.value == 0:
54
+ return
55
+
56
+ with self.lock:
57
+ with open(self.save_file_path, "wb") as file:
58
+ pickle.dump((self.revision, dict(self.data)), file)
59
+
60
+ def __getitem__(self, obj):
61
+ key = self.obj_to_key(obj)
62
+
63
+ if key in self.data:
64
+ return self.data[key]
65
+
66
+ value = self.compute_func(obj)
67
+
68
+ with self.lock:
69
+ self.data.setdefault(key, value)
70
+
71
+ with self.new_inserts.get_lock():
72
+ self.new_inserts.value += 1
73
+
74
+ return value
75
+
76
+ def obj_to_key(self, obj):
77
+ return obj
78
+
79
+ def compute_func(self, _obj):
80
+ raise NotImplementedError()
sctmgtool/hooks.py ADDED
@@ -0,0 +1,17 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 Łukasz Gieryk
3
+
4
+ from typing import NamedTuple, Any
5
+
6
+
7
+ class RollHookArgs(NamedTuple):
8
+ attacker: Any
9
+ weapon_batch: Any
10
+ defender: Any
11
+
12
+
13
+ class PoolHookArgs(NamedTuple):
14
+ attack_pool: Any
15
+ armour_pool: Any
16
+ damage_pool: Any
17
+ discard_pool: Any