11 Commits

Author SHA1 Message Date
david-spacil 8c8e029a70 Merge pull request 'Automatické vyhodnocení a nová dynamika kola' (#31) from feature/automaticke-vyhodnoceni into version/v0.3.0
Reviewed-on: #31
2026-07-12 20:53:28 +02:00
david-spacil c2bd4fd6ac chore: nastavit mypy python_version na 3.10 2026-07-12 20:45:53 +02:00
david-spacil 222df46c47 docs: přidat pravidlo o nepředkládání kódu do CLAUDE.md 2026-07-12 20:45:53 +02:00
david-spacil 21efddc1a0 style: drobný úklid pro ruff a mypy
key bez zbytečného str(), odstranění count = int(count) v throw().
2026-07-12 20:45:31 +02:00
david-spacil b3650b7243 refactor: clear() přes subprocess místo os.system 2026-07-12 20:45:31 +02:00
david-spacil b070e22f33 feat: automatické vyhodnocení a nová dynamika kola
Jedno vyhodnocení kostek = jeden hod; poté nový hod (h), ukončení tahu (k), nebo exit (x). Přepis game() a evaluate() na použití eval_wrapper, odstranění globálů score a h_bool. Zahrnuje správné chování smůly (ztráta bodů z tahu), potvrzení výběru a opravu zobrazení strikes.
2026-07-12 20:45:31 +02:00
david-spacil e9cf875192 refactor: vytažení funkcí z evaluate
Funkce eval_score vytažena z evaluate; translate_letters_to_numbers,
pick_throw_proc a eval_wrapper nově vytvořeny z fragmentů evaluate.
Oproti dřívějšímu pokusu odstraněny i přesunuté (redundantní) řádky.
2026-07-12 20:23:26 +02:00
david-spacil caa743fcc9 update .gitignore 2026-07-11 22:46:25 +02:00
david-spacil 26f53761a7 enhancement: řazení hodnot
po načtení hodnot kombinací z csv dojde pro jistotu ještě k seřazení podle hodnoty
2026-07-11 16:30:33 +02:00
david-spacil d420664fde feat: upravena funkce eval_score()
Došlo k úpravě funkce eval_score() tak, aby přijímala input s více kombinacemi najednou. V případě vstupu, který kromě platných kombinací obsahuje i hodnoty navíc, vrátí False a uživatel má možnost vybrat kostky k vyhodnocení znovu
2026-07-11 16:16:39 +02:00
david-spacil 5dee428752 docs: přidat CLAUDE.md s přehledem architektury a příkazů
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 22:25:43 +02:00
4 changed files with 207 additions and 117 deletions
+2 -1
View File
@@ -2,4 +2,5 @@ __pycache__/
*.pyc *.pyc
dist/ dist/
build/ build/
*.egg-info/ *.egg-info/
test.py
+51
View File
@@ -0,0 +1,51 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Working style
- During code review and code discussions, do NOT present ready-made code
proposals or concrete snippets. Limit yourself to advice and recommendations
(what is wrong, why, and the direction of the fix). Only write concrete code
when the user explicitly asks for it.
## Project
`kostky` is a Czech-language terminal CLI game — a Farkle/"tisícovky" dice game variant for multiple players. Single runtime dependency: `tabulate` (for the final results table).
## Commands
```bash
pip install -e ".[dev]" # editable install with mypy + ruff
kostky # run the game (after install)
python -m kostky # run without installing (requires tabulate)
mypy # type-check (strict mode, see pyproject.toml)
ruff check . # lint
```
There are no tests in this repository.
## Architecture
The entire game lives in `kostky/kostky.py` as a single module with module-level mutable state (no classes):
- `hodnoty: dict[str, int]` — scoring table, loaded at import time from `kostky/hodnoty.csv` via `importlib.resources`. Keys are sorted-digit strings representing dice combos (e.g. `"111"` = three ones), values are point totals.
- `players: dict[str, list[int]]` — per-player history of per-turn scores (insertion order == turn order).
- `aggr: dict[str, int]` — per-player running total, used for win checks and the results table.
- `pointer` / `first` — current player's name / the player who started the round (used to detect when a full round has completed in `next_player`/`check_win`).
- `score` — points accumulated *within the current turn*, reset on bust or on banking (`k`).
- `h_bool` — guards re-rolling: you can't roll again until you've banked at least one scoring combination in the current roll sequence.
Control flow is a manual state machine driven by tuples, not exceptions or return codes:
- `evaluate(inp, count, throw)` parses a single line of user input against the current dice state and returns a `data` tuple whose first element is a tag: `"count"` (continue turn with N remaining dice and a throw string), `"msg"` (show an error/message, possibly re-prompting), `"win"`, or `"exit"`.
- `game(data)` is the main loop: it pattern-matches on `data[0]`, prints state, prompts for input, and calls `evaluate` again to get the next `data` tuple. This loop is how every turn — and the whole game — progresses.
- Dice picks are entered as letters (`a`-`f` mapped to position 1-6 via `ord(x)-96`), not digit values; `evaluate` converts letters to positions internally before checking the picked dice values against `hodnoty`.
- Bust detection (`check_value`) checks whether *any* key in `hodnoty` is a substring of the sorted current throw — not a full combinatorial scoring check, so this is the single source of truth for "is this throw dead."
- Win condition (`check_win`) only fires once `pointer` cycles back to `first`, i.e. checked once per full round rather than once per turn, and only declares a winner if exactly one player is alone at/above `final_score`.
`kostky/__main__.py` and the `kostky` console-script entry point (in `pyproject.toml`) both call `kostky.kostky.main()`.
## Scoring data
`kostky/hodnoty.csv` (digit-string-of-dice-values → points) is the single source of truth for scoring and is also documented in tables in `README.md`. If you change one, update the other.
+153 -115
View File
@@ -1,19 +1,18 @@
import csv import csv
import re import re
import subprocess
from collections import Counter from collections import Counter
from importlib.resources import files from importlib.resources import files
from os import name, system from os import name
from random import randint from random import randint
from typing import Any from typing import Any
from tabulate import tabulate from tabulate import tabulate
score: int = 0
hodnoty: dict[str, int] = {} hodnoty: dict[str, int] = {}
players: dict[str, list[int]] = {} players: dict[str, list[int]] = {}
pointer: str = "" pointer: str = ""
first: str = "" first: str = ""
h_bool: bool = True
aggr: dict[str, int] = {} aggr: dict[str, int] = {}
final_score: int = 10000 final_score: int = 10000
@@ -21,13 +20,14 @@ csv_text = files(__package__).joinpath("hodnoty.csv").read_text(encoding="utf-8"
csv_reader = csv.reader(csv_text.splitlines()) csv_reader = csv.reader(csv_text.splitlines())
for row in csv_reader: for row in csv_reader:
key = str(row[0]) key = row[0]
value = int(row[1]) value = int(row[1])
hodnoty[key] = value hodnoty[key] = value
hodnoty = dict(sorted(hodnoty.items(), key=lambda x: x[1], reverse=True))
def throw(count: int) -> str: def throw(count: int) -> str:
count = int(count)
new_throw: list[str] = [] new_throw: list[str] = []
for _dice in range(count): for _dice in range(count):
new_throw.append(str(randint(1, 6))) new_throw.append(str(randint(1, 6)))
@@ -49,19 +49,110 @@ def show_throw(throw: str) -> str:
return pretty_list return pretty_list
def clear() -> None: def clear() -> None:
_ = system('cls') if name == 'nt' else system('clear') _ = subprocess.call('cls', shell=True) if name == 'nt' else subprocess.call('clear', shell=True)
def evaluate(inp: Any, count: int, throw: str) -> tuple[Any, ...]: def translate_letters_to_numbers(inp_hod: str, count: int) -> tuple[bool, str]:
inp_temp: str = ""
global h_bool for x in inp_hod:
x_int: int = ord(x)-96
def eval_score(pick: str) -> bool: if x_int > count:
if(pick in hodnoty): ran = "1" if count == 1 else f"1{count}"
global score return False, f"Zvolte prosím kostky v platném rozsahu ({ran}): "
score += hodnoty[pick]
return True
else: else:
return False inp_temp += str(x_int)
return True, inp_temp
def pick_throw_proc(count: int, inp_hod: str, throw: str) -> tuple[str, str]:
old_throw: list[str] = []
pick: list[str] = []
for y in range(count):
if str(y+1) not in inp_hod:
old_throw.append(throw[y])
if str(y+1) in inp_hod:
pick.append(throw[y])
old_throw_str: str = "".join(old_throw)
pick.sort()
pick_str: str = "".join(pick)
return old_throw_str, pick_str
def eval_score(pick: str) -> int:
score = 0
pick_counter = Counter(pick)
while pick_counter:
bool_temp = False
for k in hodnoty:
k_counter = Counter(k)
if k_counter <= pick_counter:
pick_counter = pick_counter - k_counter
score += hodnoty[k]
bool_temp = True
if not bool_temp:
break
if pick_counter:
score = 0
return score
def eval_wrapper(count: int, throw: str, inp_hod: str) -> tuple[bool, int, int, str]:
if not inp_hod:
data = (
True,
0,
count,
"",
)
elif re.search(r"^[a-f]+$", inp_hod):
if len(inp_hod) > count:
data = (
False,
0,
count,
"Tolika kostkami nemůžete hodit. Zkuste to znovu: ",
)
else:
tltn = translate_letters_to_numbers(inp_hod, count)
if tltn[0]:
inp_hod = tltn[1]
old_throw_str, pick_str = pick_throw_proc(count, inp_hod, throw)
new_count: int = len(old_throw_str)
score = eval_score(pick_str)
if not score:
new_count = count
old_throw_str = throw
data = (
True,
score,
new_count,
"",
)
else:
data = (
False,
0,
count,
tltn[1]
)
else:
data = (
False,
0,
count,
"Zadejte prosím platnou hodnotu: ",
)
return data
def evaluate(inp_akce: str, count: int, score: int) -> tuple[Any, ...]:
def next_player() -> None: def next_player() -> None:
global pointer global pointer
@@ -74,69 +165,10 @@ def evaluate(inp: Any, count: int, throw: str) -> tuple[Any, ...]:
pointer = nxt_key pointer = nxt_key
inp = str(inp) if inp_akce == "h":
inp_temp: str = "" data = ("count", count, score)
if re.search(r"^[a-f]+$", inp):
if len(inp) > count:
data: tuple[Any, ...] = (
"msg",
"ER01",
"Tolika kostkami nemůžete hodit. Zkuste to znovu: ",
count,
throw
)
else:
for x in inp:
x = ord(x)-96
if x > count:
ran = "1" if count == 1 else f"1{count}"
data = (
"msg",
"ER02",
f"Zvolte prosím kostky v platném rozsahu ({ran}): ",
count,
throw
)
return data
else:
inp_temp += str(x)
inp = inp_temp
old_throw: list[str] = []
pick: list[str] = []
for y in range(count):
if str(y+1) not in inp:
old_throw.append(throw[y])
if str(y+1) in inp:
pick.append(throw[y])
old_throw_str: str = "".join(old_throw)
pick.sort()
pick_str: str = "".join(pick)
new_count: int = len(old_throw_str)
if not eval_score(pick_str):
new_count = count
old_throw_str = throw
else:
h_bool = False
data = ("count", new_count, old_throw_str)
elif inp == "h":
if h_bool:
clear()
input("Nemůžete házet znovu, dokud nevyhodnotíte některé kostky.")
data = ("count", count, throw)
else:
h_bool = True
data = ("count", count, "")
elif inp == "k":
global score
elif inp_akce == "k" or not inp_akce:
if score == 0: if score == 0:
last_two = players[pointer][-2:] last_two = players[pointer][-2:]
if len(last_two) > 1: if len(last_two) > 1:
@@ -148,21 +180,16 @@ def evaluate(inp: Any, count: int, throw: str) -> tuple[Any, ...]:
players[pointer].append(score) players[pointer].append(score)
aggr[pointer] = sum(players[pointer]) aggr[pointer] = sum(players[pointer])
score = 0
h_bool = True
next_player() next_player()
data = ("count", 6, "") data = ("count", 6, 0)
if pointer == first: if pointer == first:
winner: str = check_win() winner: str = check_win()
if winner: if winner:
data = ("win", winner, aggr[winner]) data = ("win", winner, aggr[winner])
elif inp == "exit": elif inp_akce == "x":
data = ("exit",) data = ("exit",)
else:
data = ("msg", "ER03", "Zadejte prosím platnou hodnotu:", count, throw)
return data return data
def check_value(throw: str) -> bool: def check_value(throw: str) -> bool:
@@ -264,54 +291,65 @@ def game(data: tuple) -> None:
while True: while True:
if data[0] == "count": if data[0] == "count":
count: int = data[1] count: int = data[1]
th: str = data[2] score: int = data[2]
if(th == ""): if not count:
th = throw(count) count = 6
th = throw(count)
clear() clear()
# print(f"Vítězné skóre: {final_score}.")
strikes: str = "" strikes: str = ""
if len(players[pointer]) > 0: if len(players[pointer]) > 0:
if players[pointer][-1:][0] == 0: if players[pointer][-1:][0] == 0:
strikes = "x" strikes = "x"
if players[pointer][-2:][0] == 0: if players[pointer][-2:][0] == 0 and len(players[pointer]) > 1:
strikes += "x" strikes += "x"
else: else:
strikes = "" strikes = ""
print(f"Na tahu je {pointer} ({aggr[pointer]}{strikes}/{final_score}).") print(f"Na tahu je {pointer} ({aggr[pointer]}{strikes}/{final_score}).")
print(show_throw(th)) print(show_throw(th))
if not check_value(th) and data[2] == "" and count != 0: if not check_value(th) and count != 0:
global score
score = 0 score = 0
input("Oh no... anyway.") input("Oh no... anyway.")
data = evaluate("k", count, th) data = evaluate("k", count, score)
else: else:
if count == 0: if count == 0:
count = 6 count = 6
msg: str = ("Jakou akci chcete provést?\n" inp_hod: str = input("Vyberte kostky k vyhodnocení: ")
" - Zadejte kombinaci kostek k ponechání (dojde k jejich vyhodnocení): " while True:
"[pozice kostek] (např. a nebo cd nebo abcdef).\n" data = eval_wrapper(count, th, inp_hod)
" - Hodit znovu: [h].\n" if not data[0]:
f" - Ukončit hod a ponechat si skóre z tohoto hodu ({score}): [k].\n" inp_hod = input(data[3])
" - Ukončit hru: [exit].\n" continue
) score_temp = data[1]
inp: str = input(msg) print("Hodnota hodu:", score_temp)
data = evaluate(inp, count, th) add = ("Hodnota je nulová. Ukončit kolo a přičíst "
f"{score} bodů? ") if not score_temp else ""
while True:
inp = input(f"Potvrdit výběr? {add}[a]no/[n]e: ")
if inp in ["a", "y", "n"]:
break
if inp in ["a", "y"]:
count = data[2]
score += score_temp
break
inp_hod = input("Vyberte kostky k vyhodnocení: ")
while True:
if not score_temp:
inp_akce: str = "k"
else:
inp_akce = input(
f"[h]odit znovu\nu[k]ončit tah a přičíst {score} bodů\ne[x]it?\n"
)
if inp_akce in ["h", "k", "x"]:
break
else:
print("Zadejte prosím platnou hodnotu [h/k/x]. ")
data = evaluate(inp_akce, count, score)
elif data[0] == "msg":
if data[1] == "ER01" or data[1] == "ER02" or data[1] == "ER03":
msg = data[2]
count = data[3]
th = data[4]
inp = input(msg)
data = evaluate(inp, count, th)
else:
msg = data[1]
print(msg)
break
elif data[0] == "win": elif data[0] == "win":
winner: str = data[1] winner: str = data[1]
score = data[2] score = data[2]
@@ -329,7 +367,7 @@ def main() -> None:
clear() clear()
print("Vítejte ve hře v kostky!") print("Vítejte ve hře v kostky!")
add_players() add_players()
data = ("count", 6, "") data = ("count", 6, 0)
game(data) game(data)
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -38,7 +38,7 @@ source = "vcs"
packages = ["kostky"] packages = ["kostky"]
[tool.mypy] [tool.mypy]
python_version = "3.9" python_version = "3.10"
strict = true strict = true
files = ["kostky"] files = ["kostky"]