summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--extractor/extractor.py81
-rw-r--r--pub/blankplays.js92
-rw-r--r--pub/index.html29
3 files changed, 157 insertions, 45 deletions
diff --git a/extractor/extractor.py b/extractor/extractor.py
index 8fac3bf..da16717 100644
--- a/extractor/extractor.py
+++ b/extractor/extractor.py
@@ -1,16 +1,27 @@
+#!/usr/bin/env python3
+
import sys
import os
import shutil
+import argparse
+
+parser = argparse.ArgumentParser(description='''BlankPlays challenge extractor.
+Extracts challenges from log files generated by Macondo's autoplay command.''')
+parser.add_argument('-w', '--word-list', required=True, help='File containing list of words, separated by newlines.')
+parser.add_argument('-o', '--output', required=True, help='Directory to put challenges in (will be created if does not exist).')
+parser.add_argument('--start-idx', default=0, help='Number of first challenge (default: 0)')
+parser.add_argument('log_files', nargs='*', help='Macondo log files')
+args = parser.parse_args()
-if len(sys.argv) < 4:
- print('Usage: extractor.py <MACONDO LOG FILE> <WORD LIST> <START IDX>')
+if not args.log_files:
+ print('At least one log file must be specified.')
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'))
+word_list = set(line.strip().upper() for line in open(args.word_list, encoding='utf-8'))
# can change these to implement non-ASCII tiles
def encode(tile: str) -> int:
@@ -103,40 +114,50 @@ class GameState:
def output(self, filename: str) -> None:
if os.path.exists(filename):
- print(filename, 'already exists')
+ print(filename, 'already exists. Aborting.')
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')
+
+def is_nice(play: tuple[int, str, list[str]]) -> bool:
+ _square, letter, words = play
+ # we want 5+-letter words that aren't just S hooks
+ return any(len(w) >= 5 and not (letter == 'S' and w.endswith('S')) for w in words)
+
+output_dir = args.output
+game_idx = args.start_idx
+count = 0
+
try:
- os.mkdir('challenges')
+ os.mkdir(output_dir)
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
+
+for filename in args.log_files:
+ games_file = open(filename, encoding='utf-8')
+ lines = iter(games_file)
+ next(lines) # skip header
+ games = {}
+ 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 is_nice(play)]
+ # require at least 3 plays that make 5+-letter words
+ if len(nice_plays) >= 3:
+ game.output(f'{output_dir}/{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
index 4dc05ee..e58a4b2 100644
--- a/pub/blankplays.js
+++ b/pub/blankplays.js
@@ -54,6 +54,49 @@ function pointValue(letter) {
}
let boardSquareElems = [];
+let currSolution = [];
+
+function updatePossibilities(highlightElem, letters) {
+ let possibilitiesElem = highlightElem.querySelector('.possibilities');
+ possibilitiesElem.innerText = letters.join('');
+ let n = letters.length;
+ let fontSize = n === 1 ? 100
+ : n < 5 ? 60
+ : n < 7 ? 48
+ : n < 12 ? 40
+ : n < 20 ? 32
+ : 26;
+ possibilitiesElem.style.fontSize = fontSize + '%';
+}
+
+function addToSolution(row, col, letter) {
+ let letters = currSolution[row][col];
+ if (letters.indexOf(letter) !== -1) return;
+ letters.push(letter);
+ letters.sort();
+ let highlight = document.querySelector(`.highlight[data-row="${row}"][data-col="${col}"]`);
+ highlight.classList.remove('nothing');
+ updatePossibilities(highlight, letters);
+}
+
+function removeFromSolution(row, col, letter) {
+ let letters = currSolution[row][col];
+ let idx = letters.indexOf(letter);
+ if (idx === -1) return;
+ letters.splice(idx, 1);
+ let highlight = document.querySelector(`.highlight[data-row="${row}"][data-col="${col}"]`);
+ updatePossibilities(highlight, letters);
+}
+
+function toggleInSolution(row, col, letter) {
+ let letters = currSolution[row][col];
+ let idx = letters.indexOf(letter);
+ if (idx === -1) {
+ addToSolution(row, col, letter);
+ } else {
+ removeFromSolution(row, col, letter);
+ }
+}
function makeTile(container, letter, showPointValue) {
let tile = document.createElement('span');
@@ -81,25 +124,37 @@ function putTile(row, col, letter) {
}
function selectTile(elem, row, col) {
+ let placing = document.querySelector('.tile.placing');
+ if (placing)
+ placing.classList.remove('placing');
deselectTile();
elem.classList.add('selected');
document.getElementById('select').style.display = 'block';
+ document.getElementById('place').style.display = 'none';
+ for (let letter of currSolution[row][col]) {
+ document.querySelector(`.tile[data-letter="${letter}"]`)
+ .classList.add('possible');
+ }
}
function deselectTile() {
- let selected = document.querySelector('.selected');
+ let selected = document.querySelector('.highlight.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';
+ document.getElementById('place').style.display = 'block';
}
function clickedSquare(highlight, row, col) {
return (e) => {
if (e.button === 0) {
- if (highlight.classList.contains('selected')) {
+ let placing = document.querySelector('.placing');
+ if (placing) {
+ toggleInSolution(row, col, placing.dataset.letter);
+ } else if (highlight.classList.contains('selected')) {
deselectTile();
} else {
selectTile(highlight, row, col);
@@ -108,7 +163,7 @@ function clickedSquare(highlight, row, col) {
} else if (e.button === 2) {
if (highlight.classList.contains('nothing')) {
highlight.classList.remove('nothing');
- } else {
+ } else if (currSolution[row][col].length === 0) {
highlight.classList.add('nothing');
if (highlight.classList.contains('selected'))
deselectTile();
@@ -152,6 +207,11 @@ async function loadChallenge(id) {
}
let highlight = document.createElement('div');
highlight.classList.add('highlight');
+ highlight.dataset.row = row;
+ highlight.dataset.col = col;
+ let possibilities = document.createElement('span');
+ possibilities.classList.add('possibilities');
+ highlight.appendChild(possibilities);
boardSquareElems[row][col].appendChild(highlight);
highlight.addEventListener('contextmenu', (e) => e.preventDefault());
highlight.addEventListener('mousedown', clickedSquare(highlight, row, col));
@@ -166,6 +226,7 @@ function startup() {
rowElem.classList.add('board-row');
board.appendChild(rowElem);
boardSquareElems.push([]);
+ currSolution.push([]);
for (let col = 0; col < N; col++) {
let squareElem = document.createElement('div');
squareElem.classList.add('board-square');
@@ -173,6 +234,7 @@ function startup() {
if (bonus) squareElem.classList.add(bonus);
rowElem.appendChild(squareElem);
boardSquareElems[row].push(squareElem);
+ currSolution[row].push([]);
}
}
let selectContainer = document.getElementById('select-container');
@@ -185,11 +247,27 @@ function startup() {
elem.classList.add('select-tile-container');
makeTile(elem, tiles[i], false);
let tileElem = elem.querySelector('.tile');
+ tileElem.dataset.letter = tiles[i];
elem.addEventListener('click', () => {
- if (tileElem.classList.contains('possible'))
- tileElem.classList.remove('possible');
- else
- tileElem.classList.add('possible');
+ let tileSelected = document.querySelector('.highlight.selected');
+ let className = tileSelected ? 'possible' : 'placing';
+ if (tileElem.classList.contains(className)) {
+ tileElem.classList.remove(className);
+ if (tileSelected) {
+ let row = parseInt(tileSelected.dataset.row);
+ let col = parseInt(tileSelected.dataset.col);
+ removeFromSolution(row, col, tiles[i]);
+ }
+ } else {
+ let placing = document.querySelector('.placing');
+ if (placing) placing.classList.remove('placing');
+ tileElem.classList.add(className);
+ if (tileSelected) {
+ let row = parseInt(tileSelected.dataset.row);
+ let col = parseInt(tileSelected.dataset.col);
+ addToSolution(row, col, tiles[i]);
+ }
+ }
});
rowContainer.appendChild(elem);
}
diff --git a/pub/index.html b/pub/index.html
index b920617..aead4de 100644
--- a/pub/index.html
+++ b/pub/index.html
@@ -7,7 +7,7 @@
<link rel="icon" href="data:,"><!--TODO-->
<style>
body {
- font-family: sans-serif;
+ font-family: Helvetica, sans-serif;
}
#board {
display: grid;
@@ -28,13 +28,13 @@
background-color: #acf;
}
.board-square.triple-letter {
- background-color: #458;
+ background-color: #99f;
}
.board-square.double-word {
background-color: #fac;
}
.board-square.triple-word {
- background-color: #900;
+ background-color: #c66;
}
.tile {
background: #eca;
@@ -59,6 +59,9 @@
height: calc(100% - 8px);
position: absolute;
cursor: pointer;
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
.highlight.nothing {
border: 4px solid #f00;
@@ -66,6 +69,9 @@
.highlight.selected {
border: 4px solid #00f;
}
+ .possibilities {
+ word-break: break-all;
+ }
.point-container {
position: relative;
width: 100%;
@@ -100,19 +106,26 @@
background-color: #8c7;
font-weight: bold;
}
+ .select-tile-container .tile.placing {
+ background-color: #88c;
+ font-weight: bold;
+ }
</style>
<script src="/blankplays.js" async></script>
</head>
<body>
- Find all the possible plays with a single blank!
+ Find all the possible plays with a single blank!<br>
<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>
+ <div id="place">
+ Choose a letter and click where it goes:
+ </div>
+ <div id="select-container">
+ <div class="select-container-row"></div>
+ <div class="select-container-row"></div>
</div>
</body>
</html>