summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--extractor/extractor.py142
-rw-r--r--pub/blankplays.js201
-rw-r--r--pub/index.html118
4 files changed, 463 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..85807a3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+challenges
+challenges-*
diff --git a/extractor/extractor.py b/extractor/extractor.py
new file mode 100644
index 0000000..8fac3bf
--- /dev/null
+++ b/extractor/extractor.py
@@ -0,0 +1,142 @@
+import sys
+import os
+import shutil
+
+if len(sys.argv) < 4:
+ print('Usage: extractor.py <MACONDO LOG FILE> <WORD LIST> <START IDX>')
+ sys.exit(1)
+
+alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+# board size
+N = 15
+
+word_list = set(line.strip().upper() for line in open(sys.argv[2], encoding='utf-8'))
+
+# can change these to implement non-ASCII tiles
+def encode(tile: str) -> int:
+ return ord(tile)
+def decode(tile: int) -> str:
+ if tile == 0: return '.'
+ return chr(tile)
+
+
+class GameState:
+ def __init__(self) -> None:
+ self.board = bytearray(N * N)
+ def make_play(self, play: str) -> None:
+ if play.startswith('(exch') or play == '(Pass)':
+ return
+ coord = play[:3].strip()
+ tiles = play[4:]
+ horizontal = coord[0].isdigit()
+ if horizontal:
+ row = int(coord[:-1]) - 1
+ col = ord(coord[-1]) - ord('A')
+ else:
+ row = int(coord[1:]) - 1
+ col = ord(coord[0]) - ord('A')
+ for c in tiles:
+ assert row >= 0 and col >= 0 and row < N and col < N
+ if c != '.':
+ self.board[row * N + col] = encode(c)
+ if horizontal:
+ col += 1
+ else:
+ row += 1
+
+ def board_string(self) -> str:
+ rows = []
+ for row in range(15):
+ rows.append(''.join(decode(tile) for tile in self.board[row*N:(row+1)*N]) + '\n')
+ return ''.join(rows)
+
+ def try_place(self, row: int, col: int, letter: str) -> list[str]:
+ board = self.board[:]
+ if board[row*N+col]:
+ # there's already a tile here
+ return []
+ board[row*N+col] = encode(letter)
+ neighbours = []
+ if row > 0:
+ neighbours.append(board[(row-1)*N+col])
+ if row < N-1:
+ neighbours.append(board[(row+1)*N+col])
+ if col > 0:
+ neighbours.append(board[row*N+col-1])
+ if col < N-1:
+ neighbours.append(board[row*N+col+1])
+ if all(n == 0 for n in neighbours):
+ # doesn't connect to any existing plays
+ return []
+ i = col
+ while i > 0 and board[row*N+i-1]:
+ i -= 1
+ horizontal_word = ''
+ while i < 15 and board[row*N+i]:
+ horizontal_word += decode(board[row*N+i])
+ i += 1
+ words = []
+ if len(horizontal_word) > 1:
+ words.append(horizontal_word)
+ i = row
+ while i > 0 and board[(i-1)*N+col]:
+ i -= 1
+ vertical_word = ''
+ while i < 15 and board[i*N+col]:
+ vertical_word += decode(board[i*N+col])
+ i += 1
+ if len(vertical_word) > 1:
+ words.append(vertical_word)
+ if not all(word in word_list for word in words):
+ return []
+ return words
+
+ def blank_plays(self) -> list[tuple[int, str, list[str]]]:
+ plays = []
+ for row in range(N):
+ for col in range(N):
+ for letter in alphabet:
+ words_formed = self.try_place(row, col, letter)
+ if words_formed:
+ plays.append((row*15+col, letter, words_formed))
+ return plays
+
+ def output(self, filename: str) -> None:
+ if os.path.exists(filename):
+ print(filename, 'already exists')
+ sys.exit(1)
+ with open('challenge.tmp', 'wb') as out:
+ out.write(self.board_string().encode())
+ for square, letter, _ in self.blank_plays():
+ out.write(f'{square} {letter}\n'.encode())
+ os.rename('challenge.tmp', filename)
+games_file = open(sys.argv[1], encoding='utf-8')
+try:
+ os.mkdir('challenges')
+except FileExistsError:
+ pass
+lines = iter(games_file)
+next(lines) # skip header
+games = {}
+count = 0
+game_idx = int(sys.argv[3])
+for line in lines:
+ line = line.strip()
+ fields = line.split(',')
+ game_id = fields[1]
+ if game_id not in games:
+ games[game_id] = GameState()
+ play = fields[4]
+ games[game_id].make_play(play)
+ if fields[3] == '?':
+ game = games[game_id]
+ plays = game.blank_plays()
+ nice_plays = [play for play in plays if any(len(w) > 4 for w in play[2])]
+ # require at least 3 plays that make 5+-letter words
+ if len(nice_plays) >= 3:
+ game.output(f'challenges/{game_idx:05}.txt')
+ print(game.board_string())
+ print(len(plays), 'plays')
+ count += 1
+ game_idx += 1
+print(count, 'challenges made.')
diff --git a/pub/blankplays.js b/pub/blankplays.js
new file mode 100644
index 0000000..4dc05ee
--- /dev/null
+++ b/pub/blankplays.js
@@ -0,0 +1,201 @@
+'use strict';
+
+const N = 15; // board size
+
+let lexicon = 'nwl23';
+
+function updateBoardSize() {
+ let board = document.getElementById('board');
+ // sucks for desktop zooming, but there's no way around it.
+ let width = innerWidth;
+ let height = innerHeight;
+ let boardSize = Math.min(width - 20, Math.floor(height * 0.6));
+ let fontSize = (boardSize / N - 4) * 0.6;
+ board.style.fontSize = fontSize + 'px';
+ board.style.width = boardSize + 'px';
+ board.style.height = boardSize + 'px';
+ let selectContainer = document.getElementById('select-container');
+ selectContainer.style.fontSize = fontSize + 'px';
+ selectContainer.style.width = boardSize + 'px';
+ selectContainer.style.height = boardSize / N * 2 + 'px';
+}
+
+const DOUBLE_LETTER = 'double-letter';
+const TRIPLE_LETTER = 'triple-letter';
+const DOUBLE_WORD = 'double-word';
+const TRIPLE_WORD = 'triple-word';
+
+function getBonus(row, col) {
+ row = Math.min(row, N-1 - row);
+ col = Math.min(col, N-1 - col);
+ let id = Math.min(row * N + col, col * N + row);
+ if (id == 0 || id == 7) {
+ return TRIPLE_WORD;
+ } else if (id == N+5 || id == 5*N+5) {
+ return TRIPLE_LETTER;
+ } else if (id == 3 || id == 2*N+6 || id == 3*N+7 || id == 6*N+6) {
+ return DOUBLE_LETTER;
+ } else if (id == 7*N+7 || id == N+1 || id == 2*N+2 || id == 3*N+3 || id == 4*N+4) {
+ return DOUBLE_WORD;
+ } else {
+ return '';
+ }
+}
+
+function pointValue(letter) {
+ return {
+ 'A': 1, 'B': 3, 'C': 3, 'D': 2, 'E': 1,
+ 'F': 4, 'G': 2, 'H': 4, 'I': 1, 'J': 8,
+ 'K': 5, 'L': 1, 'M': 3, 'N': 1, 'O': 1,
+ 'P': 3, 'Q': 10,'R': 1, 'S': 1, 'T': 1,
+ 'U': 1, 'V': 4, 'W': 4, 'X': 8, 'Y': 4,
+ 'Z': 10,
+ }[letter];
+}
+
+let boardSquareElems = [];
+
+function makeTile(container, letter, showPointValue) {
+ let tile = document.createElement('span');
+ tile.classList.add('tile');
+ let blank = false;
+ if (letter === letter.toLowerCase()) {
+ blank = true;
+ }
+ let text = document.createElement('span');
+ text.appendChild(document.createTextNode(letter.toUpperCase()));
+ let points = document.createElement('span');
+ if (showPointValue)
+ points.appendChild(document.createTextNode(blank ? '0' : pointValue(letter) + ''));
+ points.classList.add('point-value');
+ if (blank)
+ text.classList.add('blank');
+ tile.appendChild(text);
+ container.appendChild(tile);
+ container.appendChild(points);
+}
+
+function putTile(row, col, letter) {
+ let squareElem = boardSquareElems[row][col];
+ makeTile(squareElem, letter, true);
+}
+
+function selectTile(elem, row, col) {
+ deselectTile();
+ elem.classList.add('selected');
+ document.getElementById('select').style.display = 'block';
+}
+
+function deselectTile() {
+ let selected = document.querySelector('.selected');
+ if (selected)
+ selected.classList.remove('selected');
+ for (let tile of document.querySelectorAll('.tile.possible')) {
+ tile.classList.remove('possible');
+ }
+ document.getElementById('select').style.display = 'none';
+}
+
+function clickedSquare(highlight, row, col) {
+ return (e) => {
+ if (e.button === 0) {
+ if (highlight.classList.contains('selected')) {
+ deselectTile();
+ } else {
+ selectTile(highlight, row, col);
+ }
+ e.preventDefault();
+ } else if (e.button === 2) {
+ if (highlight.classList.contains('nothing')) {
+ highlight.classList.remove('nothing');
+ } else {
+ highlight.classList.add('nothing');
+ if (highlight.classList.contains('selected'))
+ deselectTile();
+ }
+ e.preventDefault();
+ }
+ };
+}
+
+// TODO : error handling
+async function loadChallenge(id) {
+ let result = await fetch(`challenges-${lexicon}/${id}.txt`);
+ let body = await result.text();
+ let lines = body.split('\n');
+ let board = [];
+ for (let row = 0; row < 15; row++) {
+ board.push([]);
+ for (let col = 0; col < 15; col++) {
+ board[row].push(lines[row][col]);
+ }
+ }
+ for (let row = 0; row < 15; row++) {
+ for (let col = 0; col < 15; col++) {
+ let letter = board[row][col];
+ if (letter !== '.') {
+ putTile(row, col, letter);
+ continue;
+ }
+ let neighbours = [];
+ if (row > 0)
+ neighbours.push(board[row-1][col]);
+ if (row < N-1)
+ neighbours.push(board[row+1][col]);
+ if (col > 0)
+ neighbours.push(board[row][col-1]);
+ if (col < N-1)
+ neighbours.push(board[row][col+1]);
+ if (neighbours.filter((x) => x !== '.').length === 0) {
+ // not connected
+ continue;
+ }
+ let highlight = document.createElement('div');
+ highlight.classList.add('highlight');
+ boardSquareElems[row][col].appendChild(highlight);
+ highlight.addEventListener('contextmenu', (e) => e.preventDefault());
+ highlight.addEventListener('mousedown', clickedSquare(highlight, row, col));
+ }
+ }
+}
+
+function startup() {
+ updateBoardSize();
+ for (let row = 0; row < N; row++) {
+ let rowElem = document.createElement('div');
+ rowElem.classList.add('board-row');
+ board.appendChild(rowElem);
+ boardSquareElems.push([]);
+ for (let col = 0; col < N; col++) {
+ let squareElem = document.createElement('div');
+ squareElem.classList.add('board-square');
+ let bonus = getBonus(row, col);
+ if (bonus) squareElem.classList.add(bonus);
+ rowElem.appendChild(squareElem);
+ boardSquareElems[row].push(squareElem);
+ }
+ }
+ let selectContainer = document.getElementById('select-container');
+ for (let row = 0; row < 2; row++) {
+ let rowContainer = selectContainer.querySelectorAll('.select-container-row')[row];
+ for (let i = row*N; i < (row+1)*N; i++) {
+ let tiles = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ if (i >= tiles.length) break;
+ let elem = document.createElement('span');
+ elem.classList.add('select-tile-container');
+ makeTile(elem, tiles[i], false);
+ let tileElem = elem.querySelector('.tile');
+ elem.addEventListener('click', () => {
+ if (tileElem.classList.contains('possible'))
+ tileElem.classList.remove('possible');
+ else
+ tileElem.classList.add('possible');
+ });
+ rowContainer.appendChild(elem);
+ }
+ }
+ loadChallenge('00000');
+}
+
+window.addEventListener('load', startup);
+window.addEventListener('resize', updateBoardSize);
diff --git a/pub/index.html b/pub/index.html
new file mode 100644
index 0000000..b920617
--- /dev/null
+++ b/pub/index.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta content="width=device-width,initial-scale=1" name="viewport">
+ <title>Blank Plays</title>
+ <link rel="icon" href="data:,"><!--TODO-->
+ <style>
+ body {
+ font-family: sans-serif;
+ }
+ #board {
+ display: grid;
+ grid-template-rows: repeat(15, 1fr);
+ }
+ .board-row {
+ display: grid;
+ grid-template-columns: repeat(15, 1fr);
+ grid-auto-flow: column;
+ }
+ .board-square {
+ margin: 2px;
+ background-color: #dde;
+ line-height: 1;
+ position: relative;
+ }
+ .board-square.double-letter {
+ background-color: #acf;
+ }
+ .board-square.triple-letter {
+ background-color: #458;
+ }
+ .board-square.double-word {
+ background-color: #fac;
+ }
+ .board-square.triple-word {
+ background-color: #900;
+ }
+ .tile {
+ background: #eca;
+ position: absolute;
+ top: 6%;
+ left: 6%;
+ width: 88%;
+ height: 88%;
+ border-radius: 4px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: bold;
+ }
+ .blank {
+ color: #333;
+ font-weight: normal;
+ }
+ .highlight {
+ border: 4px solid #ff0;
+ width: calc(100% - 8px);
+ height: calc(100% - 8px);
+ position: absolute;
+ cursor: pointer;
+ }
+ .highlight.nothing {
+ border: 4px solid #f00;
+ }
+ .highlight.selected {
+ border: 4px solid #00f;
+ }
+ .point-container {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ }
+ .point-value {
+ position: absolute;
+ bottom: 8%;
+ right: 8%;
+ text-align: center;
+ font-size: 40%;
+ }
+ #select-container {
+ display: grid;
+ grid-template-rows: repeat(2, 1fr);
+ }
+ .select-container-row {
+ display: grid;
+ grid-template-columns: repeat(15, 1fr);
+ grid-auto-flow: column;
+ width: 100%;
+ }
+ .select-tile-container {
+ position: relative;
+ cursor: pointer;
+ }
+ .select-tile-container .tile {
+ background-color: #c88;
+ font-weight: normal;
+ }
+ .select-tile-container .tile.possible {
+ background-color: #8c7;
+ font-weight: bold;
+ }
+ </style>
+ <script src="/blankplays.js" async></script>
+ </head>
+ <body>
+ Find all the possible plays with a single blank!
+ <button>All done!</button>
+ <div id="board"></div>
+ <div id="select" style="display: none;">
+ Select which letters can go here:
+ <div id="select-container">
+ <div class="select-container-row"></div>
+ <div class="select-container-row"></div>
+ </div>
+ </div>
+ </body>
+</html>