Merge pull request 'release: v0.2.0' (#28) from version/v0.2.0 into main

Reviewed-on: david/kostky#28
This commit was merged in pull request #28.
This commit is contained in:
2026-06-27 19:42:21 +02:00
7 changed files with 232 additions and 107 deletions
+4 -1
View File
@@ -1,2 +1,5 @@
__pycache__/
*.pyc
*.pyc
dist/
build/
*.egg-info/
+27 -21
View File
@@ -1,25 +1,28 @@
# kostky
Textová (CLI) hra v kostky pro více hráčů varianta klasického *Farkle*
(„tisícovky“). Běží celá v terminálu a je napsaná v čistém Pythonu bez
externích závislostí.
(„tisícovky“). Běží celá v terminálu, v Pythonu, s jedinou závislostí
(`tabulate` pro závěrečnou výsledkovou tabulku).
## Požadavky
- Python 3.7+ (hra se spoléhá na zachování pořadí klíčů ve slovníku)
- Žádné externí balíčky používá se jen standardní knihovna
(`random`, `os`, `re`, `csv`, `collections`, `typing`)
- Python 3.9+
- Závislost `tabulate` (doinstaluje se automaticky při instalaci)
## Spuštění
## Instalace a spuštění
```bash
git clone https://gitea.spacilovi.eu/david/kostky.git
cd kostky
python main.py
pip install .
kostky
```
> Hru spouštěj z adresáře projektu soubor `hodnoty.csv` (bodovací tabulka)
> se načítá relativní cestou vůči aktuálnímu adresáři.
Případně bez instalace (s nainstalovaným `tabulate`) přes `python -m kostky`,
nebo izolovaně jako CLI nástroj přes `pipx install .`.
> Pro vývoj: `pip install -e ".[dev]"` editovatelná instalace včetně `mypy`
> a `ruff`.
## Jak se hraje
@@ -31,42 +34,45 @@ python main.py
### Průběh tahu
Na začátku tahu uvidíš cílové skóre, kdo je na tahu (a jeho celkové skóre)
a hod šesti kostkami s očíslovanými pozicemi:
Na začátku tahu uvidíš, kdo je na tahu (s jeho skóre a cílem), a hod šesti
kostkami s písmennými pozicemi:
```
Vítězné skóre: 10000.
Na tahu je Anna (0).
Na tahu je Anna (0/10000).
1, 5, 3, 2, 6, 4
^ ^ ^ ^ ^ ^
1 2 3 4 5 6
a b c d e f
```
Pak zvolíš jednu z akcí:
| Vstup | Akce |
| --- | --- |
| `pozice kostek` | Odložíš vybrané kostky k vyhodnocení (např. `1`, `34`, `123456`). Hodnoty na zvolených pozicích musí dohromady tvořit některou kombinaci z bodovací tabulky. |
| `pozice kostek` | Odložíš vybrané kostky k vyhodnocení písmeny (např. `a`, `cd`, `abcdef`). Hodnoty na zvolených pozicích musí dohromady tvořit některou kombinaci z bodovací tabulky. |
| `h` | Hodíš znovu zbývajícími kostkami. Povoleno až poté, co v tomto tahu odložíš aspoň jednu bodující kombinaci. |
| `k` | Ukončíš tah, připíšeš si nasbírané skóre a předáš tah dalšímu hráči. |
| `exit` | Předčasně ukončíš celou hru. |
Odkládat můžeš i víc kombinací po sobě po každém vyhodnocení pokračuješ se
zbývajícími kostkami a body se v rámci tahu sčítají.
### Smůla, hot dice a výhra
### Smůla, hot dice, vynulování a výhra
- **Smůla (bust):** když čerstvý hod neobsahuje žádnou bodující kombinaci,
o body z tohoto tahu přijdeš a tah přechází na dalšího hráče.
- **Hot dice:** když se ti podaří odložit všech šest kostek, dostaneš čerstvý
hod šesti kostkami a pokračuješ ve stejném tahu.
- **Výhra:** po dokončení každého kola se kontroluje, jestli někdo dosáhl
cílového skóre. Vyhrává osamocený lídr nad limitem; při shodě na první
příčce se hraje dál.
- **Vynulování:** tři po sobě jdoucí nulové tahy vynulují tvé celkové skóre.
Počet aktuálních nul se vedle skóre ukazuje jako `x` / `xx`.
- **Výhra:** po dokončení každého kola vyhrává osamocený lídr nad cílovým
skóre; při shodě na první příčce se hraje dál. Na konci se vypíše výsledková
tabulka s pořadím všech hráčů.
## Bodování
Kompletní tabulka je v souboru [`hodnoty.csv`](hodnoty.csv). Kombinace se
vyhodnocuje podle hodnot odložených kostek na jejich pořadí nezáleží.
Kompletní tabulka je v souboru [`kostky/hodnoty.csv`](kostky/hodnoty.csv).
Kombinace se vyhodnocuje podle hodnot odložených kostek na jejich pořadí
nezáleží.
### Stejné kostky
View File
+4
View File
@@ -0,0 +1,4 @@
from kostky.kostky import main
if __name__ == "__main__":
main()
View File
+147 -85
View File
@@ -1,57 +1,57 @@
from typing import Any
from random import randint
from os import system, name
import re
import csv
import re
from collections import Counter
from importlib.resources import files
from os import name, system
from random import randint
from typing import Any
score = 0
hodnoty = {}
players = {}
pointer = ""
first = ""
h_bool = True
aggr = {}
final_score = 10000
from tabulate import tabulate
with open("hodnoty.csv", "r") as csv_file:
csv_reader = csv.reader(csv_file)
score: int = 0
hodnoty: dict[str, int] = {}
players: dict[str, list[int]] = {}
pointer: str = ""
first: str = ""
h_bool: bool = True
aggr: dict[str, int] = {}
final_score: int = 10000
for row in csv_reader:
key = str(row[0])
value = int(row[1])
csv_text = files(__package__).joinpath("hodnoty.csv").read_text(encoding="utf-8")
csv_reader = csv.reader(csv_text.splitlines())
hodnoty[key] = value
for row in csv_reader:
key = str(row[0])
value = int(row[1])
hodnoty[key] = value
def throw(count: int) -> str:
count = int(count)
new_throw = []
for dice in range(count):
new_throw: list[str] = []
for _dice in range(count):
new_throw.append(str(randint(1, 6)))
new_throw = "".join(new_throw)
return new_throw
new_throw_str: str = "".join(new_throw)
return new_throw_str
def show_throw(throw: str) -> str:
throw_temp = []
throw_temp: list[str] = []
for x in throw:
throw_temp.append(x)
pretty_list = ", ".join(throw_temp)
pretty_list: str = ", ".join(throw_temp)
pretty_list += "\n"
for i in range(1, len(throw_temp) + 1):
for _i in range(1, len(throw_temp) + 1):
pretty_list += "^ "
pretty_list += "\n"
for i in range(1, len(throw_temp) + 1):
pretty_list += f"{i} "
for i in range(len(throw_temp)):
pretty_list += f"{chr(97+i)} "
return pretty_list
def clear():
if name == 'nt':
_ = system('cls')
else:
_ = system('clear')
def clear() -> None:
_ = system('cls') if name == 'nt' else system('clear')
def evaluate(inp: Any, count: int, throw: str) -> tuple:
def evaluate(inp: Any, count: int, throw: str) -> tuple[Any, ...]:
global h_bool
@@ -75,42 +75,56 @@ def evaluate(inp: Any, count: int, throw: str) -> tuple:
pointer = nxt_key
inp = str(inp)
if re.search(r"^\d+$", inp):
inp_temp: str = ""
if re.search(r"^[a-f]+$", inp):
if len(inp) > count:
data = ("msg", "ER01", "Tolika kostkami nemůžete hodit. Zkuste to znovu:", count, throw)
return data
data: tuple[Any, ...] = (
"msg",
"ER01",
"Tolika kostkami nemůžete hodit. Zkuste to znovu: ",
count,
throw
)
else:
for x in inp:
x = int(x)
x = ord(x)-96
if x > count:
if count == 1:
ran = "1"
else:
ran = f"1{count}"
data = ("msg", "ER02", f"Zvolte prosím kostky v platném rozsahu ({ran}):", count, throw)
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
old_throw = []
pick = []
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 = "".join(old_throw)
old_throw_str: str = "".join(old_throw)
pick.sort()
pick = "".join(pick)
pick_str: str = "".join(pick)
new_count = len(old_throw)
new_count: int = len(old_throw_str)
if not eval_score(pick):
if not eval_score(pick_str):
new_count = count
old_throw = throw
old_throw_str = throw
else:
h_bool = False
data = ("count", new_count, old_throw)
return data
data = ("count", new_count, old_throw_str)
elif inp == "h":
if h_bool:
clear()
@@ -119,9 +133,19 @@ def evaluate(inp: Any, count: int, throw: str) -> tuple:
else:
h_bool = True
data = ("count", count, "")
return data
elif inp == "k":
global score
if score == 0:
last_two = players[pointer][-2:]
if len(last_two) > 1:
last_two_sum = sum(last_two)
if last_two_sum == 0:
score = -sum(players[pointer])
clear()
input("Po třech nulových hodech bylo vaše skóre vynulováno.")
players[pointer].append(score)
aggr[pointer] = sum(players[pointer])
score = 0
@@ -129,18 +153,22 @@ def evaluate(inp: Any, count: int, throw: str) -> tuple:
next_player()
data = ("count", 6, "")
if pointer == first:
winner = check_win()
winner: str = check_win()
if winner:
data = ("win", winner, aggr[winner])
return data
elif inp == "exit":
data = ("exit",)
else:
data = ("msg", "ER03", "Zadejte prosím platnou hodnotu:", count, throw)
return data
return data
def check_value(throw: str) -> bool:
throw_s = sorted(throw)
throw_str = "".join(throw_s)
hodnota = False
throw_s: list[str] = sorted(throw)
throw_str: str = "".join(throw_s)
hodnota: bool = False
for h in hodnoty:
if h in throw_str:
hodnota = True
@@ -148,11 +176,11 @@ def check_value(throw: str) -> bool:
return hodnota
def add_players():
def add_players() -> None:
global pointer, first, final_score
while True:
inp = input("Zadejte jméno prvního hráče: ")
inp: str = input("Zadejte jméno prvního hráče: ")
if inp == "":
print("Jméno nemůže být prázdné.")
else:
@@ -180,8 +208,8 @@ def add_players():
inp = input("Zadejte finální skóre (nechte prázdné pro výchozích 10 000): ")
if inp != "":
try:
inp = int(inp)
final_score = inp
inp_int: int = int(inp)
final_score = inp_int
except ValueError:
print("Zadejte prosím platnou číselnou hodnotu.")
else:
@@ -189,40 +217,70 @@ def add_players():
else:
break
def to_sorted_tuple(d: dict[str, int], s: bool = True) -> tuple[list[tuple[int, str, int]],
dict[str, int]]:
L: list[tuple[int, str, int]] = []
if s:
d = dict(sorted(d.items(), key=lambda x: x[1], reverse=True))
for i, (k, v) in enumerate(d.items()):
L.append((i+1, k, v))
return L, d
def check_win() -> str:
over_limit = {}
over_limit: dict[str, int] = {}
for p in aggr:
if aggr[p] >= final_score:
over_limit[p] = aggr[p]
winner = ""
winner: str = ""
if over_limit:
over_limit_sorted = dict(sorted(over_limit.items(), key=lambda x: x[1], reverse=True))
over_limit_sorted_list = []
for ols in over_limit_sorted:
over_limit_sorted_list.append((ols, over_limit_sorted[ols]))
L, d = to_sorted_tuple(over_limit)
count = Counter(list(over_limit_sorted.values()))
count: Counter = Counter(list(d.values()))
for c in count:
if count[c] == 1:
winner = over_limit_sorted_list[0][0]
winner = L[0][1]
break
return winner
def game(data: tuple):
def win(winner: str, score: int) -> None:
results, d = to_sorted_tuple(aggr)
headers = ["Pořadí", "Hráč", "Skóre"]
table = tabulate(results, headers=headers, tablefmt="fancy_grid")
print(f"Vítězem se stává {winner} s {score} body!")
print("Díky za hru a zase příště.\n")
print(table)
def game(data: tuple) -> None:
while True:
if data[0] == "count":
count = data[1]
th = data[2]
count: int = data[1]
th: str = data[2]
if(th == ""):
th = throw(count)
clear()
print(f"Vítězné skóre: {final_score}.")
print(f"Na tahu je {pointer} ({aggr[pointer]}).")
# print(f"Vítězné skóre: {final_score}.")
strikes: str = ""
if len(players[pointer]) > 0:
if players[pointer][-1:][0] == 0:
strikes = "x"
if players[pointer][-2:][0] == 0:
strikes += "x"
else:
strikes = ""
print(f"Na tahu je {pointer} ({aggr[pointer]}{strikes}/{final_score}).")
print(show_throw(th))
if not check_value(th) and data[2] == "" and count != 0:
global score
@@ -232,12 +290,14 @@ def game(data: tuple):
else:
if count == 0:
count = 6
msg = ("Jakou akci chcete provést?\n"
" - Zadejte kombinaci kostek k ponechání (dojde k jejich vyhodnocení): [pozice kostek] (např. 1 nebo 34 nebo 123456).\n"
msg: str = ("Jakou akci chcete provést?\n"
" - Zadejte kombinaci kostek k ponechání (dojde k jejich vyhodnocení): "
"[pozice kostek] (např. a nebo cd nebo abcdef).\n"
" - Hodit znovu: [h].\n"
f" - Ukončit hod a ponechat si skóre z tohoto hodu ({score}): [k].\n"
" - Ukončit hru: [exit].\n"
)
inp = input(msg)
inp: str = input(msg)
data = evaluate(inp, count, th)
elif data[0] == "msg":
@@ -245,7 +305,6 @@ def game(data: tuple):
msg = data[2]
count = data[3]
th = data[4]
clear()
inp = input(msg)
data = evaluate(inp, count, th)
else:
@@ -254,19 +313,22 @@ def game(data: tuple):
break
elif data[0] == "win":
winner = data[1]
winner: str = data[1]
score = data[2]
clear()
print(f"Vítězem se stává {winner} s {score} body!")
print("Díky za hru a zase příště.")
win(winner, score)
break
def main():
elif data[0] == "exit":
clear()
print("Hra byla předčasně ukončena. Snad se zase brzy uvidíme.")
break
def main() -> None:
clear()
print("Vítejte ve hře v kostky!")
add_players()
input("Prosím, stiskněte enter pro první hod:")
data = ("count", 6, "")
game(data)
+50
View File
@@ -0,0 +1,50 @@
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "kostky"
dynamic = ["version"]
description = "Textová hra v kostky."
readme = "README.md"
requires-python = ">=3.9"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
authors = [{ name = "David Spáčil" }]
keywords = ["hra", "kostky", "cli"]
classifiers = [
"Programming Language :: Python :: 3",
"Environment :: Console",
"Operating System :: OS Independent",
"Topic :: Games/Entertainment",
]
dependencies = [
"tabulate",
]
[project.optional-dependencies]
dev = ["mypy", "ruff", "types-tabulate"]
[project.urls]
Repository = "https://gitea.spacilovi.eu/david/kostky"
[project.scripts]
kostky = "kostky.kostky:main"
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.targets.wheel]
packages = ["kostky"]
[tool.mypy]
python_version = "3.9"
strict = true
files = ["kostky"]
[tool.ruff]
target-version = "py39"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]