entscheidomat-decision-kit 1.0.0__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.
- entscheidomat_decision_kit-1.0.0/PKG-INFO +116 -0
- entscheidomat_decision_kit-1.0.0/README.md +95 -0
- entscheidomat_decision_kit-1.0.0/decision_kit/__init__.py +21 -0
- entscheidomat_decision_kit-1.0.0/decision_kit/alias_method.py +65 -0
- entscheidomat_decision_kit-1.0.0/decision_kit/cli.py +64 -0
- entscheidomat_decision_kit-1.0.0/decision_kit/coin_flip.py +17 -0
- entscheidomat_decision_kit-1.0.0/decision_kit/dice_engine.py +92 -0
- entscheidomat_decision_kit-1.0.0/decision_kit/magic_8_ball.py +33 -0
- entscheidomat_decision_kit-1.0.0/decision_kit/team_balancer.py +25 -0
- entscheidomat_decision_kit-1.0.0/decision_kit/wheel_physics.py +38 -0
- entscheidomat_decision_kit-1.0.0/entscheidomat_decision_kit.egg-info/PKG-INFO +116 -0
- entscheidomat_decision_kit-1.0.0/entscheidomat_decision_kit.egg-info/SOURCES.txt +16 -0
- entscheidomat_decision_kit-1.0.0/entscheidomat_decision_kit.egg-info/dependency_links.txt +1 -0
- entscheidomat_decision_kit-1.0.0/entscheidomat_decision_kit.egg-info/entry_points.txt +2 -0
- entscheidomat_decision_kit-1.0.0/entscheidomat_decision_kit.egg-info/top_level.txt +1 -0
- entscheidomat_decision_kit-1.0.0/pyproject.toml +34 -0
- entscheidomat_decision_kit-1.0.0/setup.cfg +4 -0
- entscheidomat_decision_kit-1.0.0/tests/test_decision_kit.py +47 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: entscheidomat-decision-kit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The ultimate Python library for decision-making algorithms, spinner wheels, weighted sampling, dice notation, and team generators.
|
|
5
|
+
Author-email: Entscheidomat <info@entscheidomat.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://entscheidomat.com
|
|
8
|
+
Project-URL: Documentation, https://entscheidomat.com
|
|
9
|
+
Project-URL: Repository, https://github.com/knuthtimo-lab/decision-kit-py
|
|
10
|
+
Project-URL: Glücksrad / Wheel, https://entscheidomat.com/gluecksrad
|
|
11
|
+
Project-URL: Münzwurf / Coin Flip, https://entscheidomat.com/muenzwurf
|
|
12
|
+
Project-URL: Würfel / Dice Roller, https://entscheidomat.com/wuerfel
|
|
13
|
+
Project-URL: Ja/Nein Generator, https://entscheidomat.com/ja-nein-generator
|
|
14
|
+
Keywords: decision-maker,random-picker,wheel-of-fortune,spinner-wheel,weighted-random,dice-roller,team-generator,entscheidomat
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
|
|
24
|
+
# 🎲 decision-kit (Python)
|
|
25
|
+
|
|
26
|
+
**The ultimate zero-dependency Python library for decision-making algorithms, spinner wheels, weighted sampling, dice notation, and team generators.**
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/decision-kit/)
|
|
29
|
+
[](https://pypi.org/project/decision-kit/)
|
|
30
|
+
[](https://opensource.org/licenses/MIT)
|
|
31
|
+
[](https://entscheidomat.com)
|
|
32
|
+
|
|
33
|
+
[Interactive Web Tools](https://entscheidomat.com) • [Documentation](#usage) • [CLI Usage](#cli-tool)
|
|
34
|
+
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🚀 Overview
|
|
40
|
+
|
|
41
|
+
`decision-kit` provides fast, battle-tested algorithms and data structures to build decision-making applications, tabletop RPG tools, and random selection systems in Python.
|
|
42
|
+
|
|
43
|
+
Maintained and sponsored by **[Entscheidomat.com](https://entscheidomat.com)** — the free online decision suite.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 🌐 Interactive Web Demos
|
|
48
|
+
|
|
49
|
+
Try these online decision tools powered by **[Entscheidomat.com](https://entscheidomat.com)**:
|
|
50
|
+
|
|
51
|
+
- 🎡 **[Glücksrad / Wheel of Fortune](https://entscheidomat.com/gluecksrad)**
|
|
52
|
+
- ❓ **[Ja / Nein Generator](https://entscheidomat.com/ja-nein-generator)**
|
|
53
|
+
- 🎲 **[3D Würfel Simulator](https://entscheidomat.com/wuerfel)**
|
|
54
|
+
- 🪙 **[Münzwurf Simulation](https://entscheidomat.com/muenzwurf)**
|
|
55
|
+
- 🎱 **[Magic 8-Ball Orakel](https://entscheidomat.com/magic-8-ball)**
|
|
56
|
+
- 🏷️ **[Namen Auslosung & Team Generator](https://entscheidomat.com/namen-auslosung)**
|
|
57
|
+
- 🔢 **[Zufallszahl Generator](https://entscheidomat.com/zufallszahl-generator)**
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 📦 Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install decision-kit
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 💡 Usage
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from decision_kit import AliasMethod, DiceEngine, TeamBalancer, CoinFlipEngine
|
|
73
|
+
|
|
74
|
+
# 1. Weighted Random Selection (O(1) Alias Algorithm)
|
|
75
|
+
items = [
|
|
76
|
+
{"id": "A", "label": "Common Item", "weight": 70},
|
|
77
|
+
{"id": "B", "label": "Rare Item", "weight": 25},
|
|
78
|
+
{"id": "C", "label": "Legendary Item", "weight": 5},
|
|
79
|
+
]
|
|
80
|
+
alias = AliasMethod(items)
|
|
81
|
+
winner = alias.next()
|
|
82
|
+
print("Won:", winner["label"])
|
|
83
|
+
|
|
84
|
+
# 2. Tabletop RPG Dice Parser
|
|
85
|
+
roll = DiceEngine.roll("3d6+2")
|
|
86
|
+
print(roll["total"], roll["breakdown"])
|
|
87
|
+
|
|
88
|
+
# 3. Fair Team Generator
|
|
89
|
+
teams = TeamBalancer.divide_teams(["Anna", "Ben", "Clara", "Dan"], team_count=2)
|
|
90
|
+
print(teams)
|
|
91
|
+
|
|
92
|
+
# 4. Coin Flip
|
|
93
|
+
coin = CoinFlipEngine.flip("de")
|
|
94
|
+
print(coin["side"])
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 🖥️ CLI Tool
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Pick random winner
|
|
103
|
+
decision-kit pick "Pizza" "Sushi" "Burger"
|
|
104
|
+
|
|
105
|
+
# Roll dice
|
|
106
|
+
decision-kit roll 2d6+3
|
|
107
|
+
|
|
108
|
+
# Flip coin
|
|
109
|
+
decision-kit flip
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 📄 License
|
|
115
|
+
|
|
116
|
+
MIT © **[Entscheidomat](https://entscheidomat.com)**
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🎲 decision-kit (Python)
|
|
4
|
+
|
|
5
|
+
**The ultimate zero-dependency Python library for decision-making algorithms, spinner wheels, weighted sampling, dice notation, and team generators.**
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/decision-kit/)
|
|
8
|
+
[](https://pypi.org/project/decision-kit/)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](https://entscheidomat.com)
|
|
11
|
+
|
|
12
|
+
[Interactive Web Tools](https://entscheidomat.com) • [Documentation](#usage) • [CLI Usage](#cli-tool)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 🚀 Overview
|
|
19
|
+
|
|
20
|
+
`decision-kit` provides fast, battle-tested algorithms and data structures to build decision-making applications, tabletop RPG tools, and random selection systems in Python.
|
|
21
|
+
|
|
22
|
+
Maintained and sponsored by **[Entscheidomat.com](https://entscheidomat.com)** — the free online decision suite.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🌐 Interactive Web Demos
|
|
27
|
+
|
|
28
|
+
Try these online decision tools powered by **[Entscheidomat.com](https://entscheidomat.com)**:
|
|
29
|
+
|
|
30
|
+
- 🎡 **[Glücksrad / Wheel of Fortune](https://entscheidomat.com/gluecksrad)**
|
|
31
|
+
- ❓ **[Ja / Nein Generator](https://entscheidomat.com/ja-nein-generator)**
|
|
32
|
+
- 🎲 **[3D Würfel Simulator](https://entscheidomat.com/wuerfel)**
|
|
33
|
+
- 🪙 **[Münzwurf Simulation](https://entscheidomat.com/muenzwurf)**
|
|
34
|
+
- 🎱 **[Magic 8-Ball Orakel](https://entscheidomat.com/magic-8-ball)**
|
|
35
|
+
- 🏷️ **[Namen Auslosung & Team Generator](https://entscheidomat.com/namen-auslosung)**
|
|
36
|
+
- 🔢 **[Zufallszahl Generator](https://entscheidomat.com/zufallszahl-generator)**
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 📦 Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install decision-kit
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 💡 Usage
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from decision_kit import AliasMethod, DiceEngine, TeamBalancer, CoinFlipEngine
|
|
52
|
+
|
|
53
|
+
# 1. Weighted Random Selection (O(1) Alias Algorithm)
|
|
54
|
+
items = [
|
|
55
|
+
{"id": "A", "label": "Common Item", "weight": 70},
|
|
56
|
+
{"id": "B", "label": "Rare Item", "weight": 25},
|
|
57
|
+
{"id": "C", "label": "Legendary Item", "weight": 5},
|
|
58
|
+
]
|
|
59
|
+
alias = AliasMethod(items)
|
|
60
|
+
winner = alias.next()
|
|
61
|
+
print("Won:", winner["label"])
|
|
62
|
+
|
|
63
|
+
# 2. Tabletop RPG Dice Parser
|
|
64
|
+
roll = DiceEngine.roll("3d6+2")
|
|
65
|
+
print(roll["total"], roll["breakdown"])
|
|
66
|
+
|
|
67
|
+
# 3. Fair Team Generator
|
|
68
|
+
teams = TeamBalancer.divide_teams(["Anna", "Ben", "Clara", "Dan"], team_count=2)
|
|
69
|
+
print(teams)
|
|
70
|
+
|
|
71
|
+
# 4. Coin Flip
|
|
72
|
+
coin = CoinFlipEngine.flip("de")
|
|
73
|
+
print(coin["side"])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 🖥️ CLI Tool
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Pick random winner
|
|
82
|
+
decision-kit pick "Pizza" "Sushi" "Burger"
|
|
83
|
+
|
|
84
|
+
# Roll dice
|
|
85
|
+
decision-kit roll 2d6+3
|
|
86
|
+
|
|
87
|
+
# Flip coin
|
|
88
|
+
decision-kit flip
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 📄 License
|
|
94
|
+
|
|
95
|
+
MIT © **[Entscheidomat](https://entscheidomat.com)**
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
decision-kit: Python library for decision-making algorithms, random selection, weighted sampling, and dice notation.
|
|
3
|
+
Powered by https://entscheidomat.com
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .alias_method import AliasMethod
|
|
7
|
+
from .dice_engine import DiceEngine
|
|
8
|
+
from .wheel_physics import WheelPhysics
|
|
9
|
+
from .team_balancer import TeamBalancer
|
|
10
|
+
from .coin_flip import CoinFlipEngine
|
|
11
|
+
from .magic_8_ball import Magic8BallEngine
|
|
12
|
+
|
|
13
|
+
__version__ = "1.0.0"
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AliasMethod",
|
|
16
|
+
"DiceEngine",
|
|
17
|
+
"WheelPhysics",
|
|
18
|
+
"TeamBalancer",
|
|
19
|
+
"CoinFlipEngine",
|
|
20
|
+
"Magic8BallEngine",
|
|
21
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from typing import List, Dict, Any, TypeVar
|
|
3
|
+
|
|
4
|
+
T = TypeVar("T")
|
|
5
|
+
|
|
6
|
+
class AliasMethod:
|
|
7
|
+
"""
|
|
8
|
+
Vose's Alias Method implementation for O(1) weighted random sampling in Python.
|
|
9
|
+
Preprocessing time: O(N)
|
|
10
|
+
Sampling time: O(1)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, items: List[Dict[str, Any]]):
|
|
14
|
+
if not items:
|
|
15
|
+
raise ValueError("AliasMethod requires at least one item.")
|
|
16
|
+
|
|
17
|
+
self.items = items
|
|
18
|
+
n = len(items)
|
|
19
|
+
self.probabilities = [0.0] * n
|
|
20
|
+
self.alias = [0] * n
|
|
21
|
+
|
|
22
|
+
total_weight = sum(max(0.0, float(item.get("weight", 1.0))) for item in items)
|
|
23
|
+
if total_weight <= 0:
|
|
24
|
+
raise ValueError("Total weight must be greater than zero.")
|
|
25
|
+
|
|
26
|
+
scaled_probs = [(max(0.0, float(item.get("weight", 1.0))) / total_weight) * n for item in items]
|
|
27
|
+
small = []
|
|
28
|
+
large = []
|
|
29
|
+
|
|
30
|
+
for i, prob in enumerate(scaled_probs):
|
|
31
|
+
if prob < 1.0:
|
|
32
|
+
small.append(i)
|
|
33
|
+
else:
|
|
34
|
+
large.append(i)
|
|
35
|
+
|
|
36
|
+
while small and large:
|
|
37
|
+
s = small.pop()
|
|
38
|
+
l = large.pop()
|
|
39
|
+
|
|
40
|
+
self.probabilities[s] = scaled_probs[s]
|
|
41
|
+
self.alias[s] = l
|
|
42
|
+
|
|
43
|
+
scaled_probs[l] = scaled_probs[l] + scaled_probs[s] - 1.0
|
|
44
|
+
|
|
45
|
+
if scaled_probs[l] < 1.0:
|
|
46
|
+
small.append(l)
|
|
47
|
+
else:
|
|
48
|
+
large.append(l)
|
|
49
|
+
|
|
50
|
+
while large:
|
|
51
|
+
self.probabilities[large.pop()] = 1.0
|
|
52
|
+
|
|
53
|
+
while small:
|
|
54
|
+
self.probabilities[small.pop()] = 1.0
|
|
55
|
+
|
|
56
|
+
def next(self) -> Dict[str, Any]:
|
|
57
|
+
"""Sample an item in O(1) time."""
|
|
58
|
+
n = len(self.items)
|
|
59
|
+
column = random.randint(0, n - 1)
|
|
60
|
+
coin_toss = random.random()
|
|
61
|
+
|
|
62
|
+
if coin_toss < self.probabilities[column]:
|
|
63
|
+
return self.items[column]
|
|
64
|
+
else:
|
|
65
|
+
return self.items[self.alias[column]]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .alias_method import AliasMethod
|
|
3
|
+
from .dice_engine import DiceEngine
|
|
4
|
+
from .team_balancer import TeamBalancer
|
|
5
|
+
from .coin_flip import CoinFlipEngine
|
|
6
|
+
from .magic_8_ball import Magic8BallEngine
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
args = sys.argv[1:]
|
|
10
|
+
if not args or args[0] in ("--help", "-h"):
|
|
11
|
+
print("=================================================")
|
|
12
|
+
print(" 🎲 decision-kit CLI (Powered by entscheidomat.com)")
|
|
13
|
+
print("=================================================\n")
|
|
14
|
+
print("Usage:")
|
|
15
|
+
print(" decision-kit pick <item1> <item2> ...")
|
|
16
|
+
print(" decision-kit roll <notation> (e.g. 2d6+3, 1d20)")
|
|
17
|
+
print(" decision-kit flip")
|
|
18
|
+
print(" decision-kit 8ball [de|en]")
|
|
19
|
+
print(" decision-kit team --names \"Alice,Bob,Charlie,Dan\" --teams 2")
|
|
20
|
+
print("\nExplore interactive decision tools online:")
|
|
21
|
+
print(" 🔗 https://entscheidomat.com\n")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
cmd = args[0].lower()
|
|
25
|
+
|
|
26
|
+
if cmd == "pick":
|
|
27
|
+
items = args[1:]
|
|
28
|
+
if not items:
|
|
29
|
+
print("❌ Please provide items to pick from.")
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
opts = [{"id": str(i), "label": label, "weight": 1.0} for i, label in enumerate(items)]
|
|
32
|
+
alias = AliasMethod(opts)
|
|
33
|
+
winner = alias.next()
|
|
34
|
+
print(f"\n🎯 Chosen Winner: {winner['label']}\n")
|
|
35
|
+
|
|
36
|
+
elif cmd == "roll":
|
|
37
|
+
notation = args[1] if len(args) > 1 else "1d20"
|
|
38
|
+
res = DiceEngine.roll(notation)
|
|
39
|
+
print(f"\n🎲 Roll Result: {res['total']} ({res['breakdown']})\n")
|
|
40
|
+
|
|
41
|
+
elif cmd == "flip":
|
|
42
|
+
res = CoinFlipEngine.flip("de")
|
|
43
|
+
print(f"\n🪙 Coin Flip Result: {res['side']}\n")
|
|
44
|
+
|
|
45
|
+
elif cmd == "8ball":
|
|
46
|
+
lang = args[1] if len(args) > 1 else "de"
|
|
47
|
+
res = Magic8BallEngine.ask(lang)
|
|
48
|
+
print(f"\n🎱 Magic 8-Ball says: \"{res['answer']}\"\n")
|
|
49
|
+
|
|
50
|
+
elif cmd == "team":
|
|
51
|
+
if "--names" in args:
|
|
52
|
+
idx = args.index("--names")
|
|
53
|
+
names = [s.strip() for s in args[idx + 1].split(",") if s.strip()]
|
|
54
|
+
teams_count = int(args[args.index("--teams") + 1]) if "--teams" in args else 2
|
|
55
|
+
teams = TeamBalancer.divide_teams(names, teams_count)
|
|
56
|
+
print(f"\n👥 Teams:")
|
|
57
|
+
for t in teams:
|
|
58
|
+
print(f" {t['name']}: {', '.join(t['members'])}")
|
|
59
|
+
print()
|
|
60
|
+
else:
|
|
61
|
+
print("❌ Please specify --names \"Name1,Name2\"")
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from typing import Dict, Any
|
|
3
|
+
|
|
4
|
+
class CoinFlipEngine:
|
|
5
|
+
@classmethod
|
|
6
|
+
def flip(cls, language: str = "de") -> Dict[str, Any]:
|
|
7
|
+
outcome = "heads" if random.random() < 0.5 else "tails"
|
|
8
|
+
if language == "de":
|
|
9
|
+
side = "Kopf" if outcome == "heads" else "Zahl"
|
|
10
|
+
else:
|
|
11
|
+
side = "Heads" if outcome == "heads" else "Tails"
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
"outcome": outcome,
|
|
15
|
+
"side": side,
|
|
16
|
+
"language": language
|
|
17
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import random
|
|
3
|
+
from typing import Dict, Any, List
|
|
4
|
+
|
|
5
|
+
class DiceEngine:
|
|
6
|
+
"""
|
|
7
|
+
Parser and evaluator for tabletop RPG dice expressions (e.g., "3d6+2", "1d20 advantage", "4d6kh3").
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def roll(cls, expression: str) -> Dict[str, Any]:
|
|
12
|
+
clean_expr = expression.strip().lower()
|
|
13
|
+
|
|
14
|
+
# Advantage / Disadvantage
|
|
15
|
+
if "adv" in clean_expr or "advantage" in clean_expr:
|
|
16
|
+
r1 = random.randint(1, 20)
|
|
17
|
+
r2 = random.randint(1, 20)
|
|
18
|
+
best = max(r1, r2)
|
|
19
|
+
return {
|
|
20
|
+
"notation": expression,
|
|
21
|
+
"rolls": [r1, r2],
|
|
22
|
+
"modifier": 0,
|
|
23
|
+
"total": best,
|
|
24
|
+
"critical_hit": best == 20,
|
|
25
|
+
"critical_fumble": best == 1,
|
|
26
|
+
"breakdown": f"Advantage [{r1}, {r2}] -> {best}",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if "dis" in clean_expr or "disadvantage" in clean_expr:
|
|
30
|
+
r1 = random.randint(1, 20)
|
|
31
|
+
r2 = random.randint(1, 20)
|
|
32
|
+
worst = min(r1, r2)
|
|
33
|
+
return {
|
|
34
|
+
"notation": expression,
|
|
35
|
+
"rolls": [r1, r2],
|
|
36
|
+
"modifier": 0,
|
|
37
|
+
"total": worst,
|
|
38
|
+
"critical_hit": worst == 20,
|
|
39
|
+
"critical_fumble": worst == 1,
|
|
40
|
+
"breakdown": f"Disadvantage [{r1}, {r2}] -> {worst}",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pattern = r"^(\d+)?d(\d+)(kh\d+|dl\d+)?([+-]\d+)?$"
|
|
44
|
+
match = re.match(pattern, clean_expr)
|
|
45
|
+
|
|
46
|
+
if not match:
|
|
47
|
+
if clean_expr.isdigit():
|
|
48
|
+
sides = int(clean_expr)
|
|
49
|
+
r = random.randint(1, sides)
|
|
50
|
+
return {
|
|
51
|
+
"notation": f"1d{sides}",
|
|
52
|
+
"rolls": [r],
|
|
53
|
+
"modifier": 0,
|
|
54
|
+
"total": r,
|
|
55
|
+
"breakdown": f"1d{sides} ({r})",
|
|
56
|
+
}
|
|
57
|
+
raise ValueError(f'Invalid dice notation: "{expression}". Format like "2d6", "1d20+3", "4d6kh3"')
|
|
58
|
+
|
|
59
|
+
count = int(match.group(1) or 1)
|
|
60
|
+
sides = int(match.group(2))
|
|
61
|
+
keep_drop = match.group(3)
|
|
62
|
+
static_mod = int(match.group(4) or 0)
|
|
63
|
+
|
|
64
|
+
if count <= 0 or sides <= 0:
|
|
65
|
+
raise ValueError("Dice count and sides must be positive integers.")
|
|
66
|
+
|
|
67
|
+
raw_rolls = [random.randint(1, sides) for _ in range(count)]
|
|
68
|
+
kept_rolls = list(raw_rolls)
|
|
69
|
+
kd_text = ""
|
|
70
|
+
|
|
71
|
+
if keep_drop:
|
|
72
|
+
if keep_drop.startswith("kh"):
|
|
73
|
+
k = int(keep_drop.replace("kh", ""))
|
|
74
|
+
kept_rolls = sorted(raw_rolls, reverse=True)[:k]
|
|
75
|
+
kd_text = f" (keep highest {k})"
|
|
76
|
+
elif keep_drop.startswith("dl"):
|
|
77
|
+
d = int(keep_drop.replace("dl", ""))
|
|
78
|
+
kept_rolls = sorted(raw_rolls)[d:]
|
|
79
|
+
kd_text = f" (drop lowest {d})"
|
|
80
|
+
|
|
81
|
+
total = sum(kept_rolls) + static_mod
|
|
82
|
+
mod_text = f"+{static_mod}" if static_mod > 0 else (f"{static_mod}" if static_mod < 0 else "")
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"notation": expression,
|
|
86
|
+
"rolls": raw_rolls,
|
|
87
|
+
"modifier": static_mod,
|
|
88
|
+
"total": total,
|
|
89
|
+
"critical_hit": count == 1 and sides == 20 and raw_rolls[0] == 20,
|
|
90
|
+
"critical_fumble": count == 1 and sides == 20 and raw_rolls[0] == 1,
|
|
91
|
+
"breakdown": f"{raw_rolls}{kd_text}{mod_text} = {total}",
|
|
92
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from typing import Dict, Any
|
|
3
|
+
|
|
4
|
+
EN_MAGIC_ANSWERS = [
|
|
5
|
+
{"text": "It is certain.", "category": "positive"},
|
|
6
|
+
{"text": "Without a doubt.", "category": "positive"},
|
|
7
|
+
{"text": "Yes definitely.", "category": "positive"},
|
|
8
|
+
{"text": "Reply hazy, try again.", "category": "neutral"},
|
|
9
|
+
{"text": "Ask again later.", "category": "neutral"},
|
|
10
|
+
{"text": "Don't count on it.", "category": "negative"},
|
|
11
|
+
{"text": "My reply is no.", "category": "negative"},
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
DE_MAGIC_ANSWERS = [
|
|
15
|
+
{"text": "Es ist ganz sicher so.", "category": "positive"},
|
|
16
|
+
{"text": "Zweifellos ja.", "category": "positive"},
|
|
17
|
+
{"text": "Ja, definitiv.", "category": "positive"},
|
|
18
|
+
{"text": "Antwort verschwommen, versuche es nochmal.", "category": "neutral"},
|
|
19
|
+
{"text": "Frage später nochmal.", "category": "neutral"},
|
|
20
|
+
{"text": "Verlass dich nicht darauf.", "category": "negative"},
|
|
21
|
+
{"text": "Meine Antwort ist Nein.", "category": "negative"},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
class Magic8BallEngine:
|
|
25
|
+
@classmethod
|
|
26
|
+
def ask(cls, language: str = "en") -> Dict[str, Any]:
|
|
27
|
+
pool = DE_MAGIC_ANSWERS if language == "de" else EN_MAGIC_ANSWERS
|
|
28
|
+
selected = random.choice(pool)
|
|
29
|
+
return {
|
|
30
|
+
"answer": selected["text"],
|
|
31
|
+
"category": selected["category"],
|
|
32
|
+
"language": language
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from typing import List, Dict, Any, TypeVar
|
|
3
|
+
|
|
4
|
+
T = TypeVar("T")
|
|
5
|
+
|
|
6
|
+
class TeamBalancer:
|
|
7
|
+
"""Fair team and group partition algorithms in Python."""
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def divide_teams(cls, members: List[T], team_count: int) -> List[Dict[str, Any]]:
|
|
11
|
+
if not members:
|
|
12
|
+
raise ValueError("Members list cannot be empty.")
|
|
13
|
+
if team_count <= 0:
|
|
14
|
+
raise ValueError("Team count must be greater than zero.")
|
|
15
|
+
|
|
16
|
+
shuffled = list(members)
|
|
17
|
+
random.shuffle(shuffled)
|
|
18
|
+
|
|
19
|
+
effective_count = min(team_count, len(members))
|
|
20
|
+
teams = [{"id": i + 1, "name": f"Team {i + 1}", "members": []} for i in range(effective_count)]
|
|
21
|
+
|
|
22
|
+
for idx, member in enumerate(shuffled):
|
|
23
|
+
teams[idx % effective_count]["members"].append(member)
|
|
24
|
+
|
|
25
|
+
return teams
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from typing import List, Dict, Any
|
|
3
|
+
from .alias_method import AliasMethod
|
|
4
|
+
|
|
5
|
+
class WheelPhysics:
|
|
6
|
+
"""Calculates spinner wheel stopping angle, winner, and sector alignment."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, min_rotations: int = 5, max_rotations: int = 8):
|
|
9
|
+
self.min_rotations = min_rotations
|
|
10
|
+
self.max_rotations = max_rotations
|
|
11
|
+
|
|
12
|
+
def calculate_spin(self, items: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
13
|
+
if not items:
|
|
14
|
+
raise ValueError("Cannot calculate spin for an empty list.")
|
|
15
|
+
|
|
16
|
+
alias = AliasMethod(items)
|
|
17
|
+
winner = alias.next()
|
|
18
|
+
winning_index = items.index(winner)
|
|
19
|
+
|
|
20
|
+
segment_angle = 360.0 / len(items)
|
|
21
|
+
sector_center = winning_index * segment_angle + segment_angle / 2.0
|
|
22
|
+
|
|
23
|
+
padding = segment_angle * 0.1
|
|
24
|
+
variance = (random.random() - 0.5) * (segment_angle - 2 * padding)
|
|
25
|
+
stop_angle_on_wheel = (sector_center + variance + 360.0) % 360.0
|
|
26
|
+
|
|
27
|
+
pointer_offset = 270.0
|
|
28
|
+
final_angle_offset = (pointer_offset - stop_angle_on_wheel + 360.0) % 360.0
|
|
29
|
+
|
|
30
|
+
full_rotations = random.randint(self.min_rotations, self.max_rotations)
|
|
31
|
+
total_target_rotation = full_rotations * 360.0 + final_angle_offset
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"winner": winner,
|
|
35
|
+
"winning_index": winning_index,
|
|
36
|
+
"target_angle": total_target_rotation,
|
|
37
|
+
"rotation_count": full_rotations
|
|
38
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: entscheidomat-decision-kit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The ultimate Python library for decision-making algorithms, spinner wheels, weighted sampling, dice notation, and team generators.
|
|
5
|
+
Author-email: Entscheidomat <info@entscheidomat.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://entscheidomat.com
|
|
8
|
+
Project-URL: Documentation, https://entscheidomat.com
|
|
9
|
+
Project-URL: Repository, https://github.com/knuthtimo-lab/decision-kit-py
|
|
10
|
+
Project-URL: Glücksrad / Wheel, https://entscheidomat.com/gluecksrad
|
|
11
|
+
Project-URL: Münzwurf / Coin Flip, https://entscheidomat.com/muenzwurf
|
|
12
|
+
Project-URL: Würfel / Dice Roller, https://entscheidomat.com/wuerfel
|
|
13
|
+
Project-URL: Ja/Nein Generator, https://entscheidomat.com/ja-nein-generator
|
|
14
|
+
Keywords: decision-maker,random-picker,wheel-of-fortune,spinner-wheel,weighted-random,dice-roller,team-generator,entscheidomat
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
|
|
24
|
+
# 🎲 decision-kit (Python)
|
|
25
|
+
|
|
26
|
+
**The ultimate zero-dependency Python library for decision-making algorithms, spinner wheels, weighted sampling, dice notation, and team generators.**
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/decision-kit/)
|
|
29
|
+
[](https://pypi.org/project/decision-kit/)
|
|
30
|
+
[](https://opensource.org/licenses/MIT)
|
|
31
|
+
[](https://entscheidomat.com)
|
|
32
|
+
|
|
33
|
+
[Interactive Web Tools](https://entscheidomat.com) • [Documentation](#usage) • [CLI Usage](#cli-tool)
|
|
34
|
+
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🚀 Overview
|
|
40
|
+
|
|
41
|
+
`decision-kit` provides fast, battle-tested algorithms and data structures to build decision-making applications, tabletop RPG tools, and random selection systems in Python.
|
|
42
|
+
|
|
43
|
+
Maintained and sponsored by **[Entscheidomat.com](https://entscheidomat.com)** — the free online decision suite.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 🌐 Interactive Web Demos
|
|
48
|
+
|
|
49
|
+
Try these online decision tools powered by **[Entscheidomat.com](https://entscheidomat.com)**:
|
|
50
|
+
|
|
51
|
+
- 🎡 **[Glücksrad / Wheel of Fortune](https://entscheidomat.com/gluecksrad)**
|
|
52
|
+
- ❓ **[Ja / Nein Generator](https://entscheidomat.com/ja-nein-generator)**
|
|
53
|
+
- 🎲 **[3D Würfel Simulator](https://entscheidomat.com/wuerfel)**
|
|
54
|
+
- 🪙 **[Münzwurf Simulation](https://entscheidomat.com/muenzwurf)**
|
|
55
|
+
- 🎱 **[Magic 8-Ball Orakel](https://entscheidomat.com/magic-8-ball)**
|
|
56
|
+
- 🏷️ **[Namen Auslosung & Team Generator](https://entscheidomat.com/namen-auslosung)**
|
|
57
|
+
- 🔢 **[Zufallszahl Generator](https://entscheidomat.com/zufallszahl-generator)**
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 📦 Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install decision-kit
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 💡 Usage
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from decision_kit import AliasMethod, DiceEngine, TeamBalancer, CoinFlipEngine
|
|
73
|
+
|
|
74
|
+
# 1. Weighted Random Selection (O(1) Alias Algorithm)
|
|
75
|
+
items = [
|
|
76
|
+
{"id": "A", "label": "Common Item", "weight": 70},
|
|
77
|
+
{"id": "B", "label": "Rare Item", "weight": 25},
|
|
78
|
+
{"id": "C", "label": "Legendary Item", "weight": 5},
|
|
79
|
+
]
|
|
80
|
+
alias = AliasMethod(items)
|
|
81
|
+
winner = alias.next()
|
|
82
|
+
print("Won:", winner["label"])
|
|
83
|
+
|
|
84
|
+
# 2. Tabletop RPG Dice Parser
|
|
85
|
+
roll = DiceEngine.roll("3d6+2")
|
|
86
|
+
print(roll["total"], roll["breakdown"])
|
|
87
|
+
|
|
88
|
+
# 3. Fair Team Generator
|
|
89
|
+
teams = TeamBalancer.divide_teams(["Anna", "Ben", "Clara", "Dan"], team_count=2)
|
|
90
|
+
print(teams)
|
|
91
|
+
|
|
92
|
+
# 4. Coin Flip
|
|
93
|
+
coin = CoinFlipEngine.flip("de")
|
|
94
|
+
print(coin["side"])
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 🖥️ CLI Tool
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Pick random winner
|
|
103
|
+
decision-kit pick "Pizza" "Sushi" "Burger"
|
|
104
|
+
|
|
105
|
+
# Roll dice
|
|
106
|
+
decision-kit roll 2d6+3
|
|
107
|
+
|
|
108
|
+
# Flip coin
|
|
109
|
+
decision-kit flip
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 📄 License
|
|
115
|
+
|
|
116
|
+
MIT © **[Entscheidomat](https://entscheidomat.com)**
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
decision_kit/__init__.py
|
|
4
|
+
decision_kit/alias_method.py
|
|
5
|
+
decision_kit/cli.py
|
|
6
|
+
decision_kit/coin_flip.py
|
|
7
|
+
decision_kit/dice_engine.py
|
|
8
|
+
decision_kit/magic_8_ball.py
|
|
9
|
+
decision_kit/team_balancer.py
|
|
10
|
+
decision_kit/wheel_physics.py
|
|
11
|
+
entscheidomat_decision_kit.egg-info/PKG-INFO
|
|
12
|
+
entscheidomat_decision_kit.egg-info/SOURCES.txt
|
|
13
|
+
entscheidomat_decision_kit.egg-info/dependency_links.txt
|
|
14
|
+
entscheidomat_decision_kit.egg-info/entry_points.txt
|
|
15
|
+
entscheidomat_decision_kit.egg-info/top_level.txt
|
|
16
|
+
tests/test_decision_kit.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
decision_kit
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "entscheidomat-decision-kit"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "The ultimate Python library for decision-making algorithms, spinner wheels, weighted sampling, dice notation, and team generators."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Entscheidomat", email = "info@entscheidomat.com" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
]
|
|
20
|
+
keywords = ["decision-maker", "random-picker", "wheel-of-fortune", "spinner-wheel", "weighted-random", "dice-roller", "team-generator", "entscheidomat"]
|
|
21
|
+
requires-python = ">=3.8"
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
"Homepage" = "https://entscheidomat.com"
|
|
26
|
+
"Documentation" = "https://entscheidomat.com"
|
|
27
|
+
"Repository" = "https://github.com/knuthtimo-lab/decision-kit-py"
|
|
28
|
+
"Glücksrad / Wheel" = "https://entscheidomat.com/gluecksrad"
|
|
29
|
+
"Münzwurf / Coin Flip" = "https://entscheidomat.com/muenzwurf"
|
|
30
|
+
"Würfel / Dice Roller" = "https://entscheidomat.com/wuerfel"
|
|
31
|
+
"Ja/Nein Generator" = "https://entscheidomat.com/ja-nein-generator"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
decision-kit = "decision_kit.cli:main"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from decision_kit import (
|
|
3
|
+
AliasMethod,
|
|
4
|
+
DiceEngine,
|
|
5
|
+
TeamBalancer,
|
|
6
|
+
CoinFlipEngine,
|
|
7
|
+
Magic8BallEngine,
|
|
8
|
+
WheelPhysics,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
class TestDecisionKit(unittest.TestCase):
|
|
12
|
+
def test_alias_method(self):
|
|
13
|
+
items = [{"id": "A", "weight": 70}, {"id": "B", "weight": 30}]
|
|
14
|
+
alias = AliasMethod(items)
|
|
15
|
+
counts = {"A": 0, "B": 0}
|
|
16
|
+
for _ in range(5000):
|
|
17
|
+
res = alias.next()
|
|
18
|
+
counts[res["id"]] += 1
|
|
19
|
+
self.assertGreater(counts["A"], 2500)
|
|
20
|
+
|
|
21
|
+
def test_dice_engine(self):
|
|
22
|
+
res = DiceEngine.roll("2d6+3")
|
|
23
|
+
self.assertEqual(len(res["rolls"]), 2)
|
|
24
|
+
self.assertGreaterEqual(res["total"], 5)
|
|
25
|
+
self.assertLessEqual(res["total"], 15)
|
|
26
|
+
|
|
27
|
+
def test_team_balancer(self):
|
|
28
|
+
members = ["Alice", "Bob", "Charlie", "Dan"]
|
|
29
|
+
teams = TeamBalancer.divide_teams(members, 2)
|
|
30
|
+
self.assertEqual(len(teams), 2)
|
|
31
|
+
|
|
32
|
+
def test_coin_flip(self):
|
|
33
|
+
res = CoinFlipEngine.flip("de")
|
|
34
|
+
self.assertIn(res["side"], ["Kopf", "Zahl"])
|
|
35
|
+
|
|
36
|
+
def test_magic_8_ball(self):
|
|
37
|
+
res = Magic8BallEngine.ask("de")
|
|
38
|
+
self.assertTrue(len(res["answer"]) > 0)
|
|
39
|
+
|
|
40
|
+
def test_wheel_physics(self):
|
|
41
|
+
physics = WheelPhysics()
|
|
42
|
+
items = [{"id": "1", "weight": 1}, {"id": "2", "weight": 1}]
|
|
43
|
+
spin = physics.calculate_spin(items)
|
|
44
|
+
self.assertGreater(spin["target_angle"], 1000)
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
unittest.main()
|