sctmgtool 0.3.1__tar.gz
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-0.3.1/LICENSE +21 -0
- sctmgtool-0.3.1/PKG-INFO +85 -0
- sctmgtool-0.3.1/README.md +63 -0
- sctmgtool-0.3.1/pyproject.toml +38 -0
- sctmgtool-0.3.1/setup.cfg +4 -0
- sctmgtool-0.3.1/src/sctmgtool/__init__.py +4 -0
- sctmgtool-0.3.1/src/sctmgtool/__main__.py +15 -0
- sctmgtool-0.3.1/src/sctmgtool/base.py +257 -0
- sctmgtool-0.3.1/src/sctmgtool/cache.py +80 -0
- sctmgtool-0.3.1/src/sctmgtool/hooks.py +17 -0
- sctmgtool-0.3.1/src/sctmgtool/tk.py +404 -0
- sctmgtool-0.3.1/src/sctmgtool/tools.py +381 -0
- sctmgtool-0.3.1/src/sctmgtool/units/NOTICE.txt +17 -0
- sctmgtool-0.3.1/src/sctmgtool/units/__init__.py +9 -0
- sctmgtool-0.3.1/src/sctmgtool/units/protoss.py +65 -0
- sctmgtool-0.3.1/src/sctmgtool/units/terran.py +58 -0
- sctmgtool-0.3.1/src/sctmgtool/units/zerg.py +36 -0
- sctmgtool-0.3.1/src/sctmgtool.egg-info/PKG-INFO +85 -0
- sctmgtool-0.3.1/src/sctmgtool.egg-info/SOURCES.txt +23 -0
- sctmgtool-0.3.1/src/sctmgtool.egg-info/dependency_links.txt +1 -0
- sctmgtool-0.3.1/src/sctmgtool.egg-info/requires.txt +10 -0
- sctmgtool-0.3.1/src/sctmgtool.egg-info/top_level.txt +1 -0
- sctmgtool-0.3.1/tests/test_base.py +86 -0
- sctmgtool-0.3.1/tests/test_roll.py +249 -0
- sctmgtool-0.3.1/tests/test_tools.py +181 -0
sctmgtool-0.3.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Łukasz Gieryk
|
|
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.
|
sctmgtool-0.3.1/PKG-INFO
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sctmgtool
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: A combat analysis tool for StarCraft: Tabletop Miniatures Game
|
|
5
|
+
Author-email: Łukasz Gieryk <108897150+lgieryk@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lgieryk/sctmgtool
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: src/sctmgtool/units/NOTICE.txt
|
|
12
|
+
Requires-Dist: matplotlib
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: platformdirs
|
|
15
|
+
Requires-Dist: customtkinter
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff; extra == "dev"
|
|
20
|
+
Requires-Dist: pyinstaller; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# SC:TMG Tool
|
|
24
|
+
|
|
25
|
+
A combat analysis tool for StarCraft: Tabletop Miniatures Game. It calculates
|
|
26
|
+
the expected outcome of clashes between units to help players with list-building
|
|
27
|
+
decisions.
|
|
28
|
+
|
|
29
|
+
**Disclaimer:** This is an unofficial fan-made project. It is not affiliated
|
|
30
|
+
with, sponsored by, or endorsed by the owners of the StarCraft or StarCraft:
|
|
31
|
+
Tabletop Miniatures Game trademarks.
|
|
32
|
+
|
|
33
|
+
Any references to unit names, factions, stats, or other game data are used
|
|
34
|
+
solely for identification and analytical purposes under fair use — see
|
|
35
|
+
[NOTICE.txt](src/sctmgtool/units/NOTICE.txt) for details.
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
## A word from the author
|
|
40
|
+
Is that UpgradeX, which gives TAG-Y, worth Z minerals? I wanted to know, so I
|
|
41
|
+
created SC:TMG Tool - an unofficial, poorly written, and _slow_ application
|
|
42
|
+
written in my spare time.
|
|
43
|
+
|
|
44
|
+
The main idea was to visualize how an upgrade affects the outcome of a clash
|
|
45
|
+
between two units. So if an upgrade does not directly translate to a lost (or
|
|
46
|
+
saved) hit point, it's not included.
|
|
47
|
+
|
|
48
|
+
While the rules of SC:TMG are not that complex, they do not translate easily
|
|
49
|
+
into a computer program. I applied multiple shortcuts to achieve the
|
|
50
|
+
functionality I needed most. And - obviously - I might have gotten a more
|
|
51
|
+
complex rule wrong. Typos (and a wrong stat here and there) are also to be
|
|
52
|
+
expected.
|
|
53
|
+
|
|
54
|
+
The tool is slow. It happens so as the result of every clash is simulated 10000
|
|
55
|
+
times. The results are saved, though, so returning to an already simulated clash
|
|
56
|
+
is slightly faster.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
### From PyPI
|
|
60
|
+
python3 -m venv env
|
|
61
|
+
source env/bin/activate
|
|
62
|
+
pip install --upgrade pip
|
|
63
|
+
pip install sctmgtool
|
|
64
|
+
python3 -m sctmgtool
|
|
65
|
+
|
|
66
|
+
### Binary package for Windows
|
|
67
|
+
Download a precompiled binary for Windows from
|
|
68
|
+
https://github.com/lgieryk/sctmgtool/releases
|
|
69
|
+
|
|
70
|
+
### Installation from sources
|
|
71
|
+
git clone https://github.com/lgieryk/sctmgtool.git
|
|
72
|
+
cd sctmgtool
|
|
73
|
+
python3 -m venv env
|
|
74
|
+
source env/bin/activate
|
|
75
|
+
pip install -e .
|
|
76
|
+
python3 -m sctmgtool
|
|
77
|
+
|
|
78
|
+
You may need to install some additional system packages:
|
|
79
|
+
|
|
80
|
+
apt-get update && apt-get install python3-tk
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
- Source code: [MIT License](LICENSE)
|
|
84
|
+
- Data files in `src/sctmgtool/units/`: distributed under separate terms, see
|
|
85
|
+
[NOTICE.txt](src/sctmgtool/units/NOTICE.txt).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# SC:TMG Tool
|
|
2
|
+
|
|
3
|
+
A combat analysis tool for StarCraft: Tabletop Miniatures Game. It calculates
|
|
4
|
+
the expected outcome of clashes between units to help players with list-building
|
|
5
|
+
decisions.
|
|
6
|
+
|
|
7
|
+
**Disclaimer:** This is an unofficial fan-made project. It is not affiliated
|
|
8
|
+
with, sponsored by, or endorsed by the owners of the StarCraft or StarCraft:
|
|
9
|
+
Tabletop Miniatures Game trademarks.
|
|
10
|
+
|
|
11
|
+
Any references to unit names, factions, stats, or other game data are used
|
|
12
|
+
solely for identification and analytical purposes under fair use — see
|
|
13
|
+
[NOTICE.txt](src/sctmgtool/units/NOTICE.txt) for details.
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
## A word from the author
|
|
18
|
+
Is that UpgradeX, which gives TAG-Y, worth Z minerals? I wanted to know, so I
|
|
19
|
+
created SC:TMG Tool - an unofficial, poorly written, and _slow_ application
|
|
20
|
+
written in my spare time.
|
|
21
|
+
|
|
22
|
+
The main idea was to visualize how an upgrade affects the outcome of a clash
|
|
23
|
+
between two units. So if an upgrade does not directly translate to a lost (or
|
|
24
|
+
saved) hit point, it's not included.
|
|
25
|
+
|
|
26
|
+
While the rules of SC:TMG are not that complex, they do not translate easily
|
|
27
|
+
into a computer program. I applied multiple shortcuts to achieve the
|
|
28
|
+
functionality I needed most. And - obviously - I might have gotten a more
|
|
29
|
+
complex rule wrong. Typos (and a wrong stat here and there) are also to be
|
|
30
|
+
expected.
|
|
31
|
+
|
|
32
|
+
The tool is slow. It happens so as the result of every clash is simulated 10000
|
|
33
|
+
times. The results are saved, though, so returning to an already simulated clash
|
|
34
|
+
is slightly faster.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
### From PyPI
|
|
38
|
+
python3 -m venv env
|
|
39
|
+
source env/bin/activate
|
|
40
|
+
pip install --upgrade pip
|
|
41
|
+
pip install sctmgtool
|
|
42
|
+
python3 -m sctmgtool
|
|
43
|
+
|
|
44
|
+
### Binary package for Windows
|
|
45
|
+
Download a precompiled binary for Windows from
|
|
46
|
+
https://github.com/lgieryk/sctmgtool/releases
|
|
47
|
+
|
|
48
|
+
### Installation from sources
|
|
49
|
+
git clone https://github.com/lgieryk/sctmgtool.git
|
|
50
|
+
cd sctmgtool
|
|
51
|
+
python3 -m venv env
|
|
52
|
+
source env/bin/activate
|
|
53
|
+
pip install -e .
|
|
54
|
+
python3 -m sctmgtool
|
|
55
|
+
|
|
56
|
+
You may need to install some additional system packages:
|
|
57
|
+
|
|
58
|
+
apt-get update && apt-get install python3-tk
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
- Source code: [MIT License](LICENSE)
|
|
62
|
+
- Data files in `src/sctmgtool/units/`: distributed under separate terms, see
|
|
63
|
+
[NOTICE.txt](src/sctmgtool/units/NOTICE.txt).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools.dynamic]
|
|
6
|
+
version = {attr = "sctmgtool.__version__"}
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "sctmgtool"
|
|
10
|
+
description = "A combat analysis tool for StarCraft: Tabletop Miniatures Game"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name="Łukasz Gieryk", email="108897150+lgieryk@users.noreply.github.com" },
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICENSE", "src/sctmgtool/units/NOTICE.txt"]
|
|
16
|
+
dynamic = ["version"]
|
|
17
|
+
requires-python = ">=3.12"
|
|
18
|
+
readme = "README.md"
|
|
19
|
+
dependencies = [
|
|
20
|
+
"matplotlib",
|
|
21
|
+
"numpy",
|
|
22
|
+
"platformdirs",
|
|
23
|
+
"customtkinter",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest",
|
|
29
|
+
"pytest-cov",
|
|
30
|
+
"ruff",
|
|
31
|
+
"pyinstaller",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/lgieryk/sctmgtool"
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
line-length = 160
|
|
@@ -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)
|
|
@@ -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}"
|
|
@@ -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()
|
|
@@ -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
|