diff --git a/.gitignore b/.gitignore index 7bfcd7a..067e3b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ __pycache__/ -*.pyc \ No newline at end of file +*.pyc +dist/ +build/ +*.egg-info/ \ No newline at end of file diff --git a/README.md b/README.md index c240e8c..1ee306b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/kostky/__init__.py b/kostky/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kostky/__main__.py b/kostky/__main__.py new file mode 100644 index 0000000..ac32257 --- /dev/null +++ b/kostky/__main__.py @@ -0,0 +1,4 @@ +from kostky.kostky import main + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/hodnoty.csv b/kostky/hodnoty.csv similarity index 100% rename from hodnoty.csv rename to kostky/hodnoty.csv diff --git a/main.py b/kostky/kostky.py similarity index 50% rename from main.py rename to kostky/kostky.py index ffefad1..3dcc268 100644 --- a/main.py +++ b/kostky/kostky.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ce6429b --- /dev/null +++ b/pyproject.toml @@ -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"] \ No newline at end of file