diff options
author | pommicket <pommicket@gmail.com> | 2025-09-18 11:39:35 -0400 |
---|---|---|
committer | pommicket <pommicket@gmail.com> | 2025-09-18 11:39:35 -0400 |
commit | c728aae2527221032f47fded2fb697d95661977d (patch) | |
tree | a971c528ebd8b69758275d05b7ae6ddd747ce1c7 | |
parent | 6662ff6460bfe3a963564adb8fbe0d52370f882c (diff) |
Keep track of solution
-rw-r--r-- | extractor/extractor.py | 81 | ||||
-rw-r--r-- | pub/blankplays.js | 92 | ||||
-rw-r--r-- | pub/index.html | 29 |
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> |