Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/linters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- uses: astral-sh/setup-uv@v3
- run: uv python install
- run: uv sync --all-extras --dev
- run: uv run ruff check --select I --output-format=github clonenames
- run: uv run ruff check --output-format=github clonenames
- run: uv run ruff format --diff clonenames
- run: uv run mypy clonenames
- run: uv run ruff check --select I --output-format=github src/clonenames
- run: uv run ruff check --output-format=github src/clonenames
- run: uv run ruff format --diff src/clonenames
- run: uv run mypy -p clonenames
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ repos:
- id: check-toml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.0
rev: v0.12.2
hooks:
- id: ruff-check
name: check
Expand Down
5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@ dev = [
"mypy>=1.16.1",
"pre-commit>=4.2.0",
"ruff>=0.12.0",
"types-flask-socketio>=5.5.0.20250516",
]

# # BUILDING
# [tool.setuptools]
# packages = ["clonenames"]

# FORMATTING
[tool.ruff]
line-length = 120
Expand Down
4 changes: 2 additions & 2 deletions src/clonenames/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .clonenames import load_wordlists, Board, Card
from .clonenames import Board, Card, load_wordlists

__ALL__ = ["load_wordlists", "Board", "Card"]
__all__ = ["load_wordlists", "Board", "Card"]
86 changes: 46 additions & 40 deletions src/clonenames/clonenames.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import dataclasses
import importlib.resources.abc
import pathlib
import random
import time
import typing

def load_wordlists(*path: pathlib.Path) -> typing.Iterable[tuple[str, list[str]]]:

def load_wordlists(*path: pathlib.Path | importlib.resources.abc.Traversable) -> typing.Iterable[tuple[str, list[str]]]:
for _path in path:
if not _path.is_file():
continue
text = _path.read_text(encoding='utf8')

text = _path.read_text(encoding="utf8")
name, *words = [line for line in text.splitlines() if line]
yield name, words


class Board(object):
@dataclasses.dataclass
class Card:
number: int
word: str
team: str

remnant: int = dataclasses.field(default=0, init=False)
shown: bool = dataclasses.field(default=False, init=False)

def __repr__(self) -> str:
return self.word

def get(self) -> typing.Self:
self.shown = True
return self


class Board:
"""docstring for Board"""

def __init__(self, source: list[str]) -> None:
self.source = source

Expand All @@ -39,26 +61,25 @@ def check_size(self, size: int) -> None:
if size > MAX:
self.size = MAX

elif (size ** 0.5) % 1 == 0:
elif (size**0.5) % 1 == 0:
self.size = size if size > MIN else MIN

elif size < MIN:
self.size = MIN

else:
self.size = int(size ** 0.5) ** 2
self.size = int(size**0.5) ** 2

self.length = int(self.size ** 0.5)
self.length = int(self.size**0.5)

def load_words(self) -> None:
from time import perf_counter
random.seed(perf_counter())
random.seed(time.perf_counter())

self.COLORS = [u'red', u'blue', u'green', u'yellow']
self.COLORS = ["red", "blue", "green", "yellow"]
random.shuffle(self.COLORS)

self.words: list = list()
self.remnants: dict = {'assassin': 1}
self.words: list[Card] = list()
self.remnants: dict[str, int] = {"assassin": 1}

source = list(enumerate(random.sample(self.source, self.size)))

Expand All @@ -72,42 +93,42 @@ def load_words(self) -> None:
self.turn = 0

for _ in range(first_team_cards):
self.words.append(Card(source.pop(0), self.FIRST))
self.words.append(Card(*source.pop(0), self.FIRST))

self.remnants[self.FIRST] = first_team_cards

for team in range(self.teams - 1):
for _ in range(self.teams - 1):
color = self.COLORS.pop()
self.order.append(color)
for _ in range(other_team_cards):
self.words.append(Card(source.pop(0), color))
self.words.append(Card(*source.pop(0), color))

self.remnants[color] = other_team_cards

for _ in range(bystanders_cards):
self.words.append(Card(source.pop(0), u'bystander'))
self.words.append(Card(*source.pop(0), "bystander"))

self.remnants[u'bystander'] = bystanders_cards
self.remnants["bystander"] = bystanders_cards

self.words.append(Card(source.pop(0), u'assassin'))
self.words.append(Card(*source.pop(0), "assassin"))

random.shuffle(self.words)

self.legend = {j.number: i for i, j in enumerate(self.words)}

def table(self) -> list:
return [self.words[i: i + self.length] for i in range(0, self.size, self.length)]
def table(self) -> list[list[Card]]:
return [self.words[i : i + self.length] for i in range(0, self.size, self.length)]

def get(self, entry):
response = self.words[self.legend[int(entry)]].get()
def get(self, id: int) -> Card:
response = self.words[self.legend[id]].get()

self.remnants[response[u'team']] -= 1
self.remnants[response.team] -= 1

response[u'remnant'] = self.remnants[response[u'team']]
response.remnant = self.remnants[response.team]

return response

def advance_turn(self):
def advance_turn(self) -> str:
if self.turn == 1200:
self.turn = 1
else:
Expand All @@ -118,18 +139,3 @@ def advance_turn(self):
# def statistics(self):
# words = [pack for pack, file in wordlists.items() if file == self.wordlist][0]
# return [self.teams, len(self.words), words]


class Card(object):
def __init__(self, word: tuple, team: str) -> None:
self.word = word[1]
self.number = word[0]
self.team = team
self.shown = False

def __repr__(self) -> str:
return self.word

def get(self) -> dict:
self.shown = True
return {u'word': self.word, u'team': self.team}
Empty file added src/clonenames/py.typed
Empty file.
2 changes: 1 addition & 1 deletion src/clonenames/templates/start.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ <h4>
<label for="wordlist">Words List:</label>
<select name="words">
{%- for option in words.keys() %}
<option value="{{ words[option] }}">{{ option }}</option>
<option value="{{ option }}">{{ option }}</option>
{%- endfor %}
</select>
<small class="form-text text-muted">If choosing builtins, your maximum board size is 64.</small>
Expand Down
112 changes: 50 additions & 62 deletions src/clonenames/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,11 @@
# -*- coding: utf-8 -*-

import importlib.resources
import sys

if sys.version_info[0] < 3:
print("Sorry, but Clonenames requires Python 3. Please install it to play!")
print("Exiting now...")
sys.exit()
import random

from flask import Flask, render_template, request, redirect, url_for
from flask import Flask, redirect, render_template, request, url_for
from flask_socketio import SocketIO, join_room

import random
from werkzeug import Response

import clonenames
import clonenames.wordlists
Expand All @@ -22,22 +16,20 @@
app = Flask(__name__)
socketio = SocketIO(app)

games = dict()
games: dict[str, clonenames.Board] = dict()


@app.route("/")
def home_page():
def home_page() -> str:
return render_template("index.html")


@app.route("/start", methods=["GET", "POST"])
def start_page():
def start_page() -> str | Response:
if request.method == "POST":
game = clonenames.Board(request.form.get("words"))
game = clonenames.Board(wordlists[request.form["words"]])

success = game.load_settings(
teams=int(request.form.get("number")), size=int(request.form.get("size"))
)
success = game.load_settings(teams=int(request.form["number"]), size=int(request.form["size"]))

if success:
room = generate_room_code()
Expand All @@ -53,14 +45,12 @@ def start_page():
alert="The word list selected must be played on a smaller game board... Sorry!",
)

elif request.method == "GET":
return render_template("start.html", words=wordlists)
return render_template("start.html", words=wordlists)


@app.route("/game", methods=["GET", "POST"])
def game_page():
if request.args.get("room"):
room = request.args.get("room")
def game_page() -> str | Response:
if room := request.args.get("room"):
return render_template(
"game.html",
show_input=False,
Expand All @@ -71,33 +61,31 @@ def game_page():
remnants=games[room].remnants,
)

else:
try:
if request.method == "GET":
return render_template("game.html", show_input=True)

elif request.method == "POST":
room = request.form.get("room", False).upper()
if check_room_code(room):
return render_template(
"game.html",
show_input=False,
room=room,
host=request.form.get("host", False) == "on",
words=games[room].table(),
start=games[room].order[0],
remnants=games[room].remnants,
)

else:
return render_template(
"game.html",
show_input=True,
alert="The room code you entered does not exist. Please try again!",
)

except AttributeError:
return redirect(url_for("home_page"))
try:
if request.method == "GET":
return render_template("game.html", show_input=True)

room = request.form["room"].upper()
if check_room_code(room):
return render_template(
"game.html",
show_input=False,
room=room,
host=request.form["host"] == "on",
words=games[room].table(),
start=games[room].order[0],
remnants=games[room].remnants,
)

else:
return render_template(
"game.html",
show_input=True,
alert="The room code you entered does not exist. Please try again!",
)

except AttributeError:
return redirect(url_for("home_page"))


# @app.route(u'/statistics')
Expand All @@ -107,27 +95,27 @@ def game_page():


@socketio.on("join")
def join(data):
def join(data: dict[str, str]) -> None:
join_room(data["room"])


@socketio.on("clicked")
def handle_host_click(json):
response = games[json["room"]].get(json["id"])
def handle_host_click(json: dict[str, str]) -> None:
response = games[json["room"]].get(int(json["id"]))
socketio.emit(
"revealed",
{
"text": "Host clicked on {word}".format(word=response["word"]),
"text": "Host clicked on {word}".format(word=response.word),
"id": "#{id}".format(id=json["id"]),
"remnant": response["remnant"],
"class": "btn-{team}".format(team=response["team"]),
"remnant": response.remnant,
"class": "btn-{team}".format(team=response.team),
},
room=json["room"],
to=json["room"],
)


@socketio.on("ended_turn")
def handle_end_turn(json):
def handle_end_turn(json: dict[str, str]) -> None:
team = games[json["room"]].advance_turn()

alert = "alert {team}-start alert-start".format(team=team)
Expand All @@ -136,21 +124,21 @@ def handle_end_turn(json):
socketio.emit(
"change_turn",
{"alert": alert, "text": team, "button": button},
room=json["room"],
to=json["room"],
)


def generate_room_code():
def generate_room_code() -> str:
letters = "".join(random.sample(list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), 5))
while letters in games.keys():
while check_room_code(letters):
return generate_room_code()

return letters


def check_room_code(code):
def check_room_code(code: str) -> bool:
return code in games.keys()


def run():
socketio.run(app, debug=False, host="0.0.0.0")
def run() -> None:
socketio.run(app, debug=False, host="0.0.0.0", port=12345)
Loading