Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
a2f96e247a
|
|||
|
cf98f5ccbd
|
|||
|
fcd7f6aaf9
|
|||
|
042e8b8581
|
|||
|
9069f751c3
|
|||
|
472956c76d
|
|||
|
27c275bccb
|
|||
|
f89a031997
|
|||
| 8033b498f9 | |||
| 129a1ea5f7 |
@@ -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
|
||||||
|
|||||||
+6
-5
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
+22837
-3652
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||||
|
|||||||
+37
-14
@@ -403,7 +403,7 @@
|
|||||||
const cctx = confettiCanvas.getContext('2d');
|
const cctx = confettiCanvas.getContext('2d');
|
||||||
|
|
||||||
// ===================== Constants =====================
|
// ===================== Constants =====================
|
||||||
const CELL = 70, GAP = 20, GRID = 4, PITCH = CELL + GAP, SH = 5;
|
const CELL = 70, GAP = 30, GRID = 4, PITCH = CELL + GAP, SH = 5;
|
||||||
const ENTRANCE_DUR = 360, ENTRANCE_STAGGER = 42, PULSE_DUR = 520;
|
const ENTRANCE_DUR = 360, ENTRANCE_STAGGER = 42, PULSE_DUR = 520;
|
||||||
const PALETTE = [
|
const PALETTE = [
|
||||||
'#ff3d81','#ffd400','#00e0c6','#7c5cff','#54a0ff',
|
'#ff3d81','#ffd400','#00e0c6','#7c5cff','#54a0ff',
|
||||||
@@ -417,13 +417,13 @@
|
|||||||
light: {
|
light: {
|
||||||
tile: '#ffffff', border: '#14110c', shadow: '#14110c',
|
tile: '#ffffff', border: '#14110c', shadow: '#14110c',
|
||||||
ink: '#14110c', inkOn: '#14110c',
|
ink: '#14110c', inkOn: '#14110c',
|
||||||
line: '#e2dbc7', sel: '#14110c', found: '#12b886', bonus: '#ff7a00', selTile: '#ff3d81',
|
line: '#c7bb95', sel: '#14110c', found: '#12b886', bonus: '#ff7a00', selTile: '#ff3d81',
|
||||||
msgDefault: '#c98a00', muted: '#6f6650',
|
msgDefault: '#c98a00', muted: '#6f6650',
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
tile: '#262633', border: '#f4efe3', shadow: '#000000',
|
tile: '#262633', border: '#f4efe3', shadow: '#000000',
|
||||||
ink: '#f4efe3', inkOn: '#14110c',
|
ink: '#f4efe3', inkOn: '#14110c',
|
||||||
line: '#33333f', sel: '#f4efe3', found: '#24d38a', bonus: '#ffa733', selTile: '#ff479a',
|
line: '#52525f', sel: '#f4efe3', found: '#24d38a', bonus: '#ffa733', selTile: '#ff479a',
|
||||||
msgDefault: '#ffd400', muted: '#9a927f',
|
msgDefault: '#ffd400', muted: '#9a927f',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -501,8 +501,10 @@
|
|||||||
// ===================== Persistence =====================
|
// ===================== Persistence =====================
|
||||||
function save() {
|
function save() {
|
||||||
if (puzzleNumber === null) return;
|
if (puzzleNumber === null) return;
|
||||||
|
if (!allWordsFound()) {
|
||||||
elapsed += Date.now() - lastSaveTime;
|
elapsed += Date.now() - lastSaveTime;
|
||||||
lastSaveTime = Date.now();
|
lastSaveTime = Date.now();
|
||||||
|
}
|
||||||
const state = { foundWords: Array.from(foundWords), elapsed, board, allSolutions, extraWords, wordDefs };
|
const state = { foundWords: Array.from(foundWords), elapsed, board, allSolutions, extraWords, wordDefs };
|
||||||
localStorage.setItem('puzzle_' + puzzleNumber, JSON.stringify(state));
|
localStorage.setItem('puzzle_' + puzzleNumber, JSON.stringify(state));
|
||||||
}
|
}
|
||||||
@@ -560,7 +562,7 @@
|
|||||||
// ===================== Rendering =====================
|
// ===================== Rendering =====================
|
||||||
function drawConnections() {
|
function drawConnections() {
|
||||||
const p = PAL();
|
const p = PAL();
|
||||||
ctx.strokeStyle = p.line; ctx.lineWidth = GAP - 4; ctx.lineCap = 'round';
|
ctx.strokeStyle = p.line; ctx.lineWidth = 8; ctx.lineCap = 'round';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
for (const key of getActiveConnections()) {
|
for (const key of getActiveConnections()) {
|
||||||
const [ka, kb] = key.split('-');
|
const [ka, kb] = key.split('-');
|
||||||
@@ -576,7 +578,7 @@
|
|||||||
function drawSelectionLine(color) {
|
function drawSelectionLine(color) {
|
||||||
if (selected.length < 2) return;
|
if (selected.length < 2) return;
|
||||||
const t1 = CELL / (2 * PITCH), t2 = 1 - t1;
|
const t1 = CELL / (2 * PITCH), t2 = 1 - t1;
|
||||||
ctx.strokeStyle = color; ctx.lineWidth = GAP - 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
ctx.strokeStyle = color; ctx.lineWidth = 10; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
for (let i = 0; i < selected.length - 1; i++) {
|
for (let i = 0; i < selected.length - 1; i++) {
|
||||||
const c1 = cellCenter(selected[i][0], selected[i][1]);
|
const c1 = cellCenter(selected[i][0], selected[i][1]);
|
||||||
@@ -706,7 +708,7 @@
|
|||||||
'<span class="word-text">' + word + '</span>' +
|
'<span class="word-text">' + word + '</span>' +
|
||||||
(isExtra ? '<span class="word-tag">бонус</span>' : '') +
|
(isExtra ? '<span class="word-tag">бонус</span>' : '') +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
(def ? '<span class="word-def">— ' + def + '</span>' : '');
|
(def ? '<span class="word-def">' + def.replace(/\n/g, '<br>') + '</span>' : '');
|
||||||
grid.appendChild(item);
|
grid.appendChild(item);
|
||||||
}
|
}
|
||||||
foundDiv.appendChild(grid);
|
foundDiv.appendChild(grid);
|
||||||
@@ -951,7 +953,9 @@
|
|||||||
}
|
}
|
||||||
function finishGame(withConfetti) {
|
function finishGame(withConfetti) {
|
||||||
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
|
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
|
||||||
gameTimeEl.textContent = formatTime(displayElapsed());
|
elapsed = displayElapsed();
|
||||||
|
if (puzzleNumber !== null) save();
|
||||||
|
gameTimeEl.textContent = formatTime(elapsed);
|
||||||
gameCompleteEl.classList.add('show');
|
gameCompleteEl.classList.add('show');
|
||||||
if (withConfetti) celebrate();
|
if (withConfetti) celebrate();
|
||||||
}
|
}
|
||||||
@@ -1005,14 +1009,20 @@
|
|||||||
startTimer();
|
startTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRandomPuzzle() {
|
async function loadRandomPuzzle(compressed) {
|
||||||
gameCompleteEl.classList.remove('show');
|
gameCompleteEl.classList.remove('show');
|
||||||
statLeft.textContent = '…';
|
statLeft.textContent = '…';
|
||||||
resetTimer();
|
resetTimer();
|
||||||
puzzleNumber = null;
|
puzzleNumber = null;
|
||||||
statNumber.textContent = '?';
|
statNumber.textContent = '?';
|
||||||
|
let data;
|
||||||
|
if (compressed) {
|
||||||
|
const res = await fetch('/api/puzzles/random?board=' + encodeURIComponent(compressed));
|
||||||
|
data = await res.json();
|
||||||
|
} else {
|
||||||
const res = await fetch('/api/puzzles/random');
|
const res = await fetch('/api/puzzles/random');
|
||||||
const data = await res.json();
|
data = await res.json();
|
||||||
|
}
|
||||||
if (data.error) { statNumber.textContent = 'ERR'; return; }
|
if (data.error) { statNumber.textContent = 'ERR'; return; }
|
||||||
foundWords = new Set();
|
foundWords = new Set();
|
||||||
board = data.board;
|
board = data.board;
|
||||||
@@ -1047,9 +1057,16 @@
|
|||||||
|
|
||||||
// ===================== Copy result =====================
|
// ===================== Copy result =====================
|
||||||
function copyResult() {
|
function copyResult() {
|
||||||
const text = 'Занграммы #' + puzzleNumber +
|
let text;
|
||||||
'\n🎉 Мне потребовалось всего лишь ' + formatTime(displayElapsed()) +
|
if (puzzleNumber === null) {
|
||||||
|
const compressedBoard = board.flat().join('');
|
||||||
|
const url = window.location.origin + window.location.pathname + '#' + encodeURIComponent(compressedBoard);
|
||||||
|
text = 'Занграммы!\n🎉 Мне потребовалось всего лишь ' + formatTime(elapsed) + '\n\n' + url;
|
||||||
|
} else {
|
||||||
|
text = 'Занграммы #' + puzzleNumber +
|
||||||
|
'\n🎉 Мне потребовалось всего лишь ' + formatTime(elapsed) +
|
||||||
'\n\n' + window.location.href;
|
'\n\n' + window.location.href;
|
||||||
|
}
|
||||||
navigator.clipboard.writeText(text).then(
|
navigator.clipboard.writeText(text).then(
|
||||||
() => showMessage('Результат скопирован!', PAL().found),
|
() => showMessage('Результат скопирован!', PAL().found),
|
||||||
() => showMessage('Не удалось скопировать', PAL().selTile),
|
() => showMessage('Не удалось скопировать', PAL().selTile),
|
||||||
@@ -1069,19 +1086,25 @@
|
|||||||
wordDisplay.addEventListener('click', submitSelection);
|
wordDisplay.addEventListener('click', submitSelection);
|
||||||
$('copyResultBtn').addEventListener('click', copyResult);
|
$('copyResultBtn').addEventListener('click', copyResult);
|
||||||
$('prevPuzzlesBtn').addEventListener('click', showNumberSelector);
|
$('prevPuzzlesBtn').addEventListener('click', showNumberSelector);
|
||||||
$('randomPuzzleBtn').addEventListener('click', loadRandomPuzzle);
|
$('randomPuzzleBtn').addEventListener('click', () => loadRandomPuzzle());
|
||||||
$('closeNumberBtn').addEventListener('click', () => numberSelectorEl.classList.remove('show'));
|
$('closeNumberBtn').addEventListener('click', () => numberSelectorEl.classList.remove('show'));
|
||||||
themeBtn.addEventListener('click', () => applyTheme(theme === 'dark' ? 'light' : 'dark'));
|
themeBtn.addEventListener('click', () => applyTheme(theme === 'dark' ? 'light' : 'dark'));
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
window.addEventListener('resize', sizeConfetti);
|
window.addEventListener('resize', sizeConfetti);
|
||||||
window.addEventListener('beforeunload', save);
|
|
||||||
document.addEventListener('visibilitychange', () => { if (document.hidden) save(); });
|
|
||||||
|
|
||||||
// ===================== Init =====================
|
// ===================== Init =====================
|
||||||
applyTheme(theme);
|
applyTheme(theme);
|
||||||
sizeConfetti();
|
sizeConfetti();
|
||||||
|
const hash = decodeURIComponent(window.location.hash.slice(1) || '');
|
||||||
|
if (hash && hash.length === 16) {
|
||||||
|
history.replaceState(null, '', window.location.pathname);
|
||||||
|
loadRandomPuzzle(hash);
|
||||||
|
} else {
|
||||||
loadPuzzle();
|
loadPuzzle();
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 26 KiB |
@@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user