Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
a2f96e247a
|
|||
|
cf98f5ccbd
|
|||
|
fcd7f6aaf9
|
|||
|
042e8b8581
|
|||
|
9069f751c3
|
|||
|
472956c76d
|
|||
|
27c275bccb
|
|||
|
f89a031997
|
|||
| 8033b498f9 | |||
| 129a1ea5f7 | |||
| c5dc537844 | |||
| 2062083f80 | |||
|
55b4089e43
|
|||
|
5355159e49
|
@@ -6,7 +6,7 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
@@ -20,15 +20,7 @@ jobs:
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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"
|
||||
- uses: actions/checkout@v7
|
||||
- name: run tests
|
||||
run: docker compose run --build --rm api pytest -vv
|
||||
- name: show logs
|
||||
|
||||
+7
-6
@@ -8,7 +8,7 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
# Exposes application port.
|
||||
- "8000:8000"
|
||||
- "8000:8000"
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
@@ -25,8 +25,8 @@ services:
|
||||
ZANGRAMRU_DB_USER: zangramru
|
||||
ZANGRAMRU_DB_PASS: zangramru
|
||||
ZANGRAMRU_DB_BASE: zangramru
|
||||
volumes:
|
||||
- .:/app/src/
|
||||
# volumes:
|
||||
# - .:/app/src/
|
||||
|
||||
db:
|
||||
image: postgres:18.3-trixie
|
||||
@@ -49,13 +49,14 @@ services:
|
||||
migrator:
|
||||
<<: *main_app
|
||||
restart: "no"
|
||||
command: piccolo migrations forwards all
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- "ls && piccolo migrations forwards all"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
|
||||
|
||||
volumes:
|
||||
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
|
||||
@@ -12,4 +12,4 @@ class Puzzle(Table):
|
||||
solutions = JSON()
|
||||
extra = JSON()
|
||||
definitions = JSON()
|
||||
number = Integer()
|
||||
number = Integer(unique=True)
|
||||
|
||||
+23702
-17236
File diff suppressed because it is too large
Load Diff
@@ -113,7 +113,7 @@ class PuzzleGenerator:
|
||||
self.words[word] = row["def"]
|
||||
self.trie.add(word)
|
||||
|
||||
def find_solutions( # noqa: C901
|
||||
def _find_solutions( # noqa: C901
|
||||
self, board: list[list[str]]
|
||||
) -> dict[str, list[tuple[int, int]]]:
|
||||
"""
|
||||
@@ -258,7 +258,7 @@ class PuzzleGenerator:
|
||||
|
||||
return cover, extras
|
||||
|
||||
def get_valid_points(
|
||||
def _get_valid_points(
|
||||
self,
|
||||
solutions: dict[str, list[tuple[int, int]]],
|
||||
) -> set[tuple[int, int]]:
|
||||
@@ -269,6 +269,32 @@ class PuzzleGenerator:
|
||||
|
||||
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:
|
||||
"""
|
||||
Generate a valid puzzle that has solutions.
|
||||
@@ -289,27 +315,19 @@ class PuzzleGenerator:
|
||||
)
|
||||
if single_board_attempt > 50:
|
||||
valid_points = set()
|
||||
single_board_attempt = 0
|
||||
|
||||
for x in range(4):
|
||||
for y in range(4):
|
||||
if (x, y) not in valid_points:
|
||||
board[x][y] = secrets.choice(WEIGHTED_ALPHABET)
|
||||
|
||||
solutions = self.find_solutions(board)
|
||||
cover, extra = self._find_minimal_cover(solutions, target)
|
||||
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:
|
||||
valid_points, puzzle = self._verify_solution(board, target)
|
||||
if not puzzle:
|
||||
single_board_attempt += 1
|
||||
continue
|
||||
|
||||
return Puzzle(
|
||||
board=board,
|
||||
solutions=cover,
|
||||
extra=extra,
|
||||
word_defs=word_defs,
|
||||
)
|
||||
return puzzle
|
||||
|
||||
|
||||
def setup_generator(app: FastAPI) -> None:
|
||||
|
||||
+808
-317
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 |
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import date, timedelta
|
||||
|
||||
@@ -15,9 +16,20 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/random")
|
||||
def random_puzzle(generator: PuzzleGeneratorDep) -> PuzzleOutputDTO:
|
||||
def random_puzzle(
|
||||
generator: PuzzleGeneratorDep,
|
||||
board: str | None = None,
|
||||
) -> PuzzleOutputDTO:
|
||||
"""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)
|
||||
|
||||
|
||||
@@ -30,7 +42,7 @@ async def daily_puzzle(
|
||||
# Because they start at one, so we need to subtract one.
|
||||
target_date = date.today()
|
||||
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():
|
||||
raise HTTPException(
|
||||
@@ -45,13 +57,15 @@ async def daily_puzzle(
|
||||
if daily_puzzle is not None:
|
||||
board = [list(daily_puzzle.board[i * 4 : i * 4 + 4]) for i in range(4)]
|
||||
return DailyPuzzleDTO(
|
||||
number=(date.today() - settings.start_date).days,
|
||||
number=daily_puzzle.number,
|
||||
board=board,
|
||||
solutions=json.loads(daily_puzzle.solutions),
|
||||
extra=json.loads(daily_puzzle.extra),
|
||||
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)
|
||||
await Puzzle(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user