Dice simulator

This commit is contained in:
Jan-Niclas Loosen
2025-10-24 20:58:31 +02:00
parent 49438ccaaf
commit 90ce78f17d
9 changed files with 312 additions and 6 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.idea
.venv
.venv
*.build

View File

@@ -0,0 +1,14 @@
import math
def decomp(x: int, base: list[int]) -> dict[int, int]:
base = sorted(base, reverse=True)
coef = {}
for b in base:
c = math.floor(x / b)
coef[b] = c
x -= b * c
return coef
print(decomp(25, [1, 5, 10, 25]))

View File

@@ -10,28 +10,34 @@ max_y = len(data)
max_x = len(data[0])
start = (max_y-1, max_x-1)
# Cache with size of n points
cache = [[None for _ in range(max_x)] for _ in range(max_y)]
# Table with size of data
table = [[None for _ in range(max_x)] for _ in range(max_y)]
def find_path(y, x):
# Return bad value for positions which are out of bounds
if not (0 <= y < max_y and 0 <= x < max_x):
return (), float('-inf')
if cache[y][x] is not None:
return cache[y][x]
# Table look up
if table[y][x] is not None:
return table[y][x]
# End of the recursion is the starting position
if y == 0 and x == 0:
res = (((y, x),), data[y][x])
# Calculate defined recursion on all other existing positions
else:
west_path, west_val = find_path(y, x-1)
north_path, north_val = find_path(y-1, x)
# Choose most valuable path to the considered position
if west_val > north_val:
res = (west_path + ((y, x),), west_val + data[y][x])
else:
res = (north_path + ((y, x),), north_val + data[y][x])
cache[y][x] = res
# Store most valuable path in the table and return
table[y][x] = res
return res
path, val = find_path(*start)

60
Uebung-02/dice.cpp Normal file
View File

@@ -0,0 +1,60 @@
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <random>
#include <map>
#include <set>
#include <string>
#include <vector>
class Dice {
std::vector<std::string> sides;
std::vector<double> probs;
std::mt19937 gen;
std::discrete_distribution<> dist;
public:
size_t accepted = 0;
size_t rejected = 0;
Dice(const std::vector<std::string>& sides, const std::vector<double>& probs)
: sides(sides), probs(probs), gen(std::random_device{}()), dist(probs.begin(), probs.end()) {}
std::string fair_dice() {
while (true) {
std::vector<std::string> res;
for (size_t i = 0; i < sides.size(); ++i)
res.push_back(sides[dist(gen)]);
std::set<std::string> uniq(res.begin(), res.end());
if (uniq.size() == res.size()) {
++accepted;
return res[0];
}
++rejected;
}
}
std::map<std::string,int> throw_dice(int n) {
std::map<std::string,int> res;
for (auto& s : sides) res[s] = 0;
for (int i = 0; i < n; ++i)
res[fair_dice()]++;
return res;
}
std::pair<size_t,size_t> stats() const { return {accepted, rejected}; }
void reset_stats() { accepted = rejected = 0; }
};
namespace py = pybind11;
PYBIND11_MODULE(dice, m) {
py::class_<Dice>(m, "Dice")
.def(py::init<const std::vector<std::string>&, const std::vector<double>&>())
.def("fair_dice", &Dice::fair_dice)
.def("throw_dice", &Dice::throw_dice)
.def("stats", &Dice::stats)
.def("reset_stats", &Dice::reset_stats)
.def_readwrite("accepted", &Dice::accepted)
.def_readwrite("rejected", &Dice::rejected);
}

36
Uebung-02/dice.py Normal file
View File

@@ -0,0 +1,36 @@
from lib.Natives import NativeLoader
def throw_dice_cpp(n: int, sides: list, probs: list):
loader = NativeLoader()
dice_mod = loader.cpp("dice")
Dice = dice_mod.Dice
d = Dice(sides, probs)
d.reset_stats()
res = d.throw_dice(n)
accepted, rejected = d.stats()
return res, accepted, rejected
dice_sides = ['1', '2', '3', '4', '5', '6']
dice_props = [0.1, 0.1, 0.1, 0.1, 0.1, 0.5]
coin_sides = ['Head', 'Number']
coin_head = [0.1, 0.5]
rep = 100000
# --- Dice test ---
res_cpp, acc_dice, rej_dice = throw_dice_cpp(rep, dice_sides, dice_props)
total = sum(res_cpp.values())
probs_estimated = {k: v / total for k, v in res_cpp.items()}
print("C++ Dice result (relative):", probs_estimated)
print(f"Accepted: {acc_dice/(acc_dice+rej_dice)*100:.2f}%, "
f"Rejected: {rej_dice/(acc_dice+rej_dice)*100:.2f}%\n")
# --- Coin test ---
res_coin, acc_coin, rej_coin = throw_dice_cpp(rep, coin_sides, coin_head)
total_coin = sum(res_coin.values())
probs_coin = {k: v / total_coin for k, v in res_coin.items()}
print("C++ Coin result (relative):", probs_coin)
print(f"Accepted: {acc_coin/(acc_coin+rej_coin)*100:.2f}%, "
f"Rejected: {rej_coin/(acc_coin+rej_coin)*100:.2f}%")

27
Uebung-02/minsearch.py Normal file
View File

@@ -0,0 +1,27 @@
import random
numsTest = [1, 2, 3, 4, 5]
def search_min(nums):
if len(nums) == 1:
return nums[0]
num = nums[0]
nums.remove(num)
# n-1 recursions
rec_num = search_min(nums)
if rec_num < num:
return rec_num
else:
# n-1 times called in worst case
smaller = True
for n in nums:
if n <= num:
smaller = False
print('Hit')
assert smaller == True
return num
print(search_min(numsTest))

159
lib/Natives.py Normal file
View File

@@ -0,0 +1,159 @@
import ctypes
import pathlib
import subprocess
import hashlib
import json
import platform
import sys
import importlib.util
import sysconfig
import shutil
from datetime import datetime
from typing import Callable, Any, Tuple
class LazyCall:
def __init__(self, func: Callable[..., Any], args: Tuple[Any, ...]) -> None:
self.func = func
self.args = args
# Calls C function lazily and returns string
def to_str(self) -> str:
self.func.restype = ctypes.c_char_p
res = self.func(*self.__convert_args())
return res.decode() if res else ""
# Calls C function lazily and returns int
def to_int(self) -> int:
self.func.restype = ctypes.c_int
return self.func(*self.__convert_args())
# Calls C function lazily and returns float
def to_float(self) -> float:
self.func.restype = ctypes.c_double
return self.func(*self.__convert_args())
# Convert arguments to C types
def __convert_args(self):
out = []
for a in self.args:
if isinstance(a, str):
out.append(ctypes.c_char_p(a.encode()))
elif isinstance(a, int):
out.append(ctypes.c_int(a))
elif isinstance(a, float):
out.append(ctypes.c_double(a))
else:
out.append(a)
return out
class FunctionWrapper:
def __init__(self, lib: ctypes.CDLL, name: str) -> None:
self._lib = lib
self._name = name
# Prepares lazy function calls
def __call__(self, *args) -> LazyCall:
func = getattr(self._lib, self._name)
return LazyCall(func, args)
class LibWrapper:
def __init__(self, lib: ctypes.CDLL) -> None:
self._lib = lib
# Exposes all C functions within the lib
def __getattr__(self, name: str) -> FunctionWrapper:
return FunctionWrapper(self._lib, name)
class NativeLoader:
def __init__(self) -> None:
self.build_dir = pathlib.Path(".build")
self.build_dir.mkdir(exist_ok=True)
self.ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") or sysconfig.get_config_var("SO")
# Loads a Cpp class
def cpp(self, path: str):
src = pathlib.Path(path).with_suffix(".cpp")
if not src.exists():
raise FileNotFoundError(src)
out = self.build_dir / f"{src.stem}_cpp"
if self.__needs_rebuild(src, out):
target = self.__build_cpp(src, out)
else:
target = str(out) + ".so"
spec = importlib.util.spec_from_file_location(src.stem, target)
mod = importlib.util.module_from_spec(spec)
sys.modules[src.stem] = mod
spec.loader.exec_module(mod)
return mod
# Loads a C lib
def c(self, path: str) -> LibWrapper:
src = pathlib.Path(path).with_suffix(".c")
if not src.exists():
raise FileNotFoundError(src)
out = self.build_dir / f"{src.stem}_c.so"
if self.__needs_rebuild(src, out):
self.__build_c(src, out)
lib = ctypes.CDLL(str(out))
return LibWrapper(lib)
@staticmethod
def __create_hash(path: pathlib.Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
@staticmethod
def __info_path(target: pathlib.Path) -> pathlib.Path:
return target.with_suffix(target.suffix + ".buildinfo")
@staticmethod
def __load_info(info_path: pathlib.Path):
if info_path.exists():
with open(info_path, "r") as f:
return json.load(f)
return None
def __write_info(self, info_path: pathlib.Path, src: pathlib.Path, out: pathlib.Path) -> None:
data = {
"os": platform.system(),
"build_date": datetime.now().isoformat(),
"src": str(src),
"out": str(out),
"src_hash": self.__create_hash(src),
}
with open(info_path, "w") as f:
json.dump(data, f, indent=2)
def __needs_rebuild(self, src: pathlib.Path, out: pathlib.Path) -> bool:
info = self.__load_info(self.__info_path(out))
final_out = pathlib.Path(str(out) + ".so")
if not info or not final_out.exists():
return True
try:
return info.get("src_hash") != self.__create_hash(src)
except FileNotFoundError:
return True
def __build_cpp(self, src: pathlib.Path, out: pathlib.Path) -> str:
includes = (subprocess.check_output(["python3", "-m", "pybind11", "--includes"]).decode().strip().split())
built_temp = str(out) + self.ext_suffix
final_path = str(out) + ".so"
subprocess.run(["c++", "-O3", "-shared", "-fPIC", "-std=c++17", *includes, str(src), "-o", built_temp], check=True)
shutil.move(built_temp, final_path)
self.__write_info(self.__info_path(out), src, out)
return final_path
def __build_c(self, src: pathlib.Path, out: pathlib.Path) -> None:
subprocess.run(["gcc", "-O3", "-shared", "-fPIC", "-std=c11", str(src), "-o", str(out)], check=True)
self.__write_info(self.__info_path(out), src, out)

Binary file not shown.

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
pybind11>=2.12
setuptools>=70.0
wheel>=0.44