11 Commits

Author SHA1 Message Date
s3rius 042e8b8581 Added random board sharing.
/ docker_build (push) Successful in 1m12s
/ helm_deploy (push) Successful in 23s
Testing zangramru / lint (push) Successful in 29s
Testing zangramru / pytest (push) Successful in 18s
Signed-off-by: Pavel Kirilin <s3riussan@gmail.com>
2026-07-02 18:33:37 +02:00
s3rius 9069f751c3 Some small enhancements additional words.
Testing zangramru / lint (push) Has been cancelled
Testing zangramru / pytest (push) Has been cancelled
/ docker_build (push) Successful in 5m49s
/ helm_deploy (push) Successful in 36s
Signed-off-by: Pavel Kirilin <s3riussan@gmail.com>
2026-07-02 15:20:25 +02:00
s3rius 472956c76d lint fixes.
Testing zangramru / lint (push) Successful in 4m16s
Testing zangramru / pytest (push) Failing after 3m22s
Signed-off-by: Pavel Kirilin <s3riussan@gmail.com>
2026-07-02 13:42:17 +02:00
s3rius 27c275bccb Possible tests fix.
Testing zangramru / lint (push) Failing after 48s
Testing zangramru / pytest (push) Failing after 43s
Signed-off-by: Pavel Kirilin <s3riussan@gmail.com>
2026-07-02 13:23:45 +02:00
s3rius f89a031997 Fixed some backend issues.
Testing zangramru / lint (push) Failing after 34s
Testing zangramru / pytest (push) Failing after 47s
Signed-off-by: Pavel Kirilin <s3riussan@gmail.com>
2026-07-02 13:05:20 +02:00
s3rius 8033b498f9 Merge pull request 'Increased contrast of letter connections for readability.' (#2) from task_frontend_relations_fix into master
Testing zangramru / lint (push) Successful in 32s
Testing zangramru / pytest (push) Failing after 22s
Reviewed-on: #2
Reviewed-by: s3rius <s3rius@noreply.gitea.le-memese.com>
2026-07-02 10:57:08 +00:00
shared_mutex 129a1ea5f7 Increased contrast of letter connections for readability.
Testing zangramru / lint (push) Successful in 33s
Testing zangramru / pytest (push) Has been cancelled
Bumped the board connection line color in both themes so the links
between letters read more clearly:
- light: #e2dbc7 -> #c7bb95
- dark:  #33333f -> #52525f

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 13:55:32 +03:00
s3rius c5dc537844 Merge pull request 'Redesigned frontend: neo-brutalist UI with swipe, keyboard input and animations.' (#1) from task_better_frontend into master
/ docker_build (push) Successful in 1m38s
/ helm_deploy (push) Successful in 37s
Testing zangramru / lint (push) Successful in 31s
Testing zangramru / pytest (push) Failing after 22s
Reviewed-on: #1
Reviewed-by: s3rius <s3rius@noreply.gitea.le-memese.com>
2026-07-02 10:30:50 +00:00
shared_mutex 2062083f80 Redesigned frontend: neo-brutalist UI with swipe, keyboard input and animations.
Testing zangramru / lint (push) Successful in 31s
Testing zangramru / pytest (push) Failing after 49s
Single-file rewrite of static/index.html preserving the puzzle API contract,
board coordinates and localStorage format. Adds swipe-to-select, keyboard
tracing (type/Backspace/Enter/Esc), juicy tile animations + confetti, and a
light/dark theme toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 13:28:49 +03:00
s3rius 55b4089e43 Updated dictionary according to 10_000 most fequent words form НКРЯ.
/ docker_build (push) Successful in 1m33s
/ helm_deploy (push) Successful in 35s
Testing zangramru / lint (push) Successful in 32s
Testing zangramru / pytest (push) Failing after 51s
Signed-off-by: Pavel Kirilin <s3riussan@gmail.com>
2026-07-02 11:15:04 +02:00
s3rius 5355159e49 Updated dictionary.
Testing zangramru / lint (push) Successful in 30s
Testing zangramru / pytest (push) Failing after 51s
Signed-off-by: Pavel Kirilin <s3riussan@gmail.com>
2026-07-02 04:05:25 +02:00
9 changed files with 15272 additions and 17595 deletions
+2 -10
View File
@@ -6,7 +6,7 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
@@ -20,15 +20,7 @@ jobs:
pytest: pytest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Update docker-compose
uses: KengoTODA/actions-setup-docker-compose@v1
with:
version: "2.28.0"
- name: run tests - name: run tests
run: docker compose run --build --rm api pytest -vv run: docker compose run --build --rm api pytest -vv
- name: show logs - name: show logs
+7 -6
View File
@@ -8,7 +8,7 @@ services:
restart: always restart: always
ports: ports:
# Exposes application port. # Exposes application port.
- "8000:8000" - "8000:8000"
env_file: env_file:
- path: .env - path: .env
required: false required: false
@@ -25,8 +25,8 @@ services:
ZANGRAMRU_DB_USER: zangramru ZANGRAMRU_DB_USER: zangramru
ZANGRAMRU_DB_PASS: zangramru ZANGRAMRU_DB_PASS: zangramru
ZANGRAMRU_DB_BASE: zangramru ZANGRAMRU_DB_BASE: zangramru
volumes: # volumes:
- .:/app/src/ # - .:/app/src/
db: db:
image: postgres:18.3-trixie image: postgres:18.3-trixie
@@ -49,13 +49,14 @@ services:
migrator: migrator:
<<: *main_app <<: *main_app
restart: "no" restart: "no"
command: piccolo migrations forwards all command:
- bash
- -c
- "ls && piccolo migrations forwards all"
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
volumes: volumes:
zangramru-db-data: zangramru-db-data:
name: zangramru-db-data name: zangramru-db-data
@@ -0,0 +1,26 @@
from piccolo.apps.migrations.auto.migration_manager import MigrationManager
from piccolo.columns.column_types import Integer
ID = "2026-07-02T12:57:47:552419"
VERSION = "1.34.0"
DESCRIPTION = ""
async def forwards():
manager = MigrationManager(
migration_id=ID, app_name="zangramru_db", description=DESCRIPTION
)
manager.alter_column(
table_class_name="Puzzle",
tablename="puzzle",
column_name="number",
db_column_name="number",
params={"unique": True},
old_params={"unique": False},
column_class=Integer,
old_column_class=Integer,
schema=None,
)
return manager
+1 -1
View File
@@ -12,4 +12,4 @@ class Puzzle(Table):
solutions = JSON() solutions = JSON()
extra = JSON() extra = JSON()
definitions = JSON() definitions = JSON()
number = Integer() number = Integer(unique=True)
+14370 -17236
View File
File diff suppressed because it is too large Load Diff
+32 -14
View File
@@ -113,7 +113,7 @@ class PuzzleGenerator:
self.words[word] = row["def"] self.words[word] = row["def"]
self.trie.add(word) self.trie.add(word)
def find_solutions( # noqa: C901 def _find_solutions( # noqa: C901
self, board: list[list[str]] self, board: list[list[str]]
) -> dict[str, list[tuple[int, int]]]: ) -> dict[str, list[tuple[int, int]]]:
""" """
@@ -258,7 +258,7 @@ class PuzzleGenerator:
return cover, extras return cover, extras
def get_valid_points( def _get_valid_points(
self, self,
solutions: dict[str, list[tuple[int, int]]], solutions: dict[str, list[tuple[int, int]]],
) -> set[tuple[int, int]]: ) -> set[tuple[int, int]]:
@@ -269,6 +269,32 @@ class PuzzleGenerator:
return used_cells return used_cells
def _verify_solution(
self, board: list[list[str]], target: int
) -> tuple[set[tuple[int, int]], Puzzle | None]:
solutions = self._find_solutions(board)
cover, extra = self._find_minimal_cover(solutions, target)
valid_points = self._get_valid_points(cover)
# We found a valid solution | board combination
if len(valid_points) != 16 or len(cover) < target:
return valid_points, None
word_defs = {word: self.words[word] for word in cover | extra}
return valid_points, Puzzle(
board=board,
solutions=cover,
extra=extra,
word_defs=word_defs,
)
def check_board(self, compressed_board: str, target: int = 10) -> Puzzle | None:
"""Check if given board is valid."""
board = [list(compressed_board[i * 4 : i * 4 + 4]) for i in range(4)]
_, puzzle = self._verify_solution(board, target)
return puzzle
def generate(self, target: int, timeout: int = 10) -> Puzzle: def generate(self, target: int, timeout: int = 10) -> Puzzle:
""" """
Generate a valid puzzle that has solutions. Generate a valid puzzle that has solutions.
@@ -289,27 +315,19 @@ class PuzzleGenerator:
) )
if single_board_attempt > 50: if single_board_attempt > 50:
valid_points = set() valid_points = set()
single_board_attempt = 0
for x in range(4): for x in range(4):
for y in range(4): for y in range(4):
if (x, y) not in valid_points: if (x, y) not in valid_points:
board[x][y] = secrets.choice(WEIGHTED_ALPHABET) board[x][y] = secrets.choice(WEIGHTED_ALPHABET)
solutions = self.find_solutions(board) valid_points, puzzle = self._verify_solution(board, target)
cover, extra = self._find_minimal_cover(solutions, target) if not puzzle:
valid_points = self.get_valid_points(cover)
word_defs = {word: self.words[word] for word in cover | extra}
# We found a valid solution | board combination
if len(valid_points) != 16 or len(cover) < target:
single_board_attempt += 1 single_board_attempt += 1
continue continue
return Puzzle( return puzzle
board=board,
solutions=cover,
extra=extra,
word_defs=word_defs,
)
def setup_generator(app: FastAPI) -> None: def setup_generator(app: FastAPI) -> None:
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 26 KiB

+19 -5
View File
@@ -1,3 +1,4 @@
import asyncio
import json import json
from datetime import date, timedelta from datetime import date, timedelta
@@ -15,9 +16,20 @@ router = APIRouter()
@router.get("/random") @router.get("/random")
def random_puzzle(generator: PuzzleGeneratorDep) -> PuzzleOutputDTO: def random_puzzle(
generator: PuzzleGeneratorDep,
board: str | None = None,
) -> PuzzleOutputDTO:
"""Generate a random puzzle.""" """Generate a random puzzle."""
puzzle = generator.generate(target=10) if board:
puzzle = generator.check_board(board, target=10)
if puzzle is None:
raise HTTPException(
status_code=status.HTTP_421_MISDIRECTED_REQUEST,
detail="Given board has no solution",
)
else:
puzzle = generator.generate(10)
return PuzzleOutputDTO.model_validate(puzzle, from_attributes=True) return PuzzleOutputDTO.model_validate(puzzle, from_attributes=True)
@@ -30,7 +42,7 @@ async def daily_puzzle(
# Because they start at one, so we need to subtract one. # Because they start at one, so we need to subtract one.
target_date = date.today() target_date = date.today()
if number is not None: if number is not None:
target_date = settings.start_date + timedelta(days=number - 1) target_date = settings.start_date + timedelta(days=number)
if target_date > date.today(): if target_date > date.today():
raise HTTPException( raise HTTPException(
@@ -45,13 +57,15 @@ async def daily_puzzle(
if daily_puzzle is not None: if daily_puzzle is not None:
board = [list(daily_puzzle.board[i * 4 : i * 4 + 4]) for i in range(4)] board = [list(daily_puzzle.board[i * 4 : i * 4 + 4]) for i in range(4)]
return DailyPuzzleDTO( return DailyPuzzleDTO(
number=(date.today() - settings.start_date).days, number=daily_puzzle.number,
board=board, board=board,
solutions=json.loads(daily_puzzle.solutions), solutions=json.loads(daily_puzzle.solutions),
extra=json.loads(daily_puzzle.extra), extra=json.loads(daily_puzzle.extra),
word_defs=json.loads(daily_puzzle.definitions), word_defs=json.loads(daily_puzzle.definitions),
) )
puzzle = generator.generate(target=10)
loop = asyncio.get_running_loop()
puzzle = await loop.run_in_executor(None, generator.generate, 10)
compressedboard = "".join("".join(row) for row in puzzle.board) compressedboard = "".join("".join(row) for row in puzzle.board)
await Puzzle( await Puzzle(
{ {