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 +4 -0
- sctmgtool/__main__.py +15 -0
- sctmgtool/base.py +257 -0
- sctmgtool/cache.py +80 -0
- sctmgtool/hooks.py +17 -0
- sctmgtool/tk.py +404 -0
- sctmgtool/tools.py +381 -0
- sctmgtool/units/NOTICE.txt +17 -0
- sctmgtool/units/__init__.py +9 -0
- sctmgtool/units/protoss.py +65 -0
- sctmgtool/units/terran.py +58 -0
- sctmgtool/units/zerg.py +36 -0
- sctmgtool-0.3.1.dist-info/METADATA +85 -0
- sctmgtool-0.3.1.dist-info/RECORD +18 -0
- sctmgtool-0.3.1.dist-info/WHEEL +5 -0
- sctmgtool-0.3.1.dist-info/licenses/LICENSE +21 -0
- sctmgtool-0.3.1.dist-info/licenses/src/sctmgtool/units/NOTICE.txt +17 -0
- sctmgtool-0.3.1.dist-info/top_level.txt +1 -0
sctmgtool/__init__.py
ADDED
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
|