15 Commits

14 changed files with 210 additions and 95 deletions

View File

@@ -19,7 +19,7 @@ dnspython==2.7.0
# via email-validator # via email-validator
email-validator==2.2.0 email-validator==2.2.0
# via fastapi # via fastapi
fastapi==0.115.4 fastapi==0.115.5
# via -r requirements.in # via -r requirements.in
fastapi-cli==0.0.5 fastapi-cli==0.0.5
# via fastapi # via fastapi
@@ -29,7 +29,7 @@ h11==0.14.0
# via # via
# httpcore # httpcore
# uvicorn # uvicorn
httpcore==1.0.6 httpcore==1.0.7
# via httpx # via httpx
httptools==0.6.4 httptools==0.6.4
# via uvicorn # via uvicorn
@@ -48,7 +48,7 @@ markupsafe==3.0.2
# via jinja2 # via jinja2
mdurl==0.1.2 mdurl==0.1.2
# via markdown-it-py # via markdown-it-py
packaging==24.1 packaging==24.2
# via gunicorn # via gunicorn
pydantic==2.9.2 pydantic==2.9.2
# via # via
@@ -76,9 +76,9 @@ sniffio==1.3.1
# via # via
# anyio # anyio
# httpx # httpx
starlette==0.41.2 starlette==0.41.3
# via fastapi # via fastapi
typer==0.12.5 typer==0.13.1
# via fastapi-cli # via fastapi-cli
typing-extensions==4.12.2 typing-extensions==4.12.2
# via # via
@@ -94,5 +94,5 @@ uvloop==0.21.0
# via uvicorn # via uvicorn
watchfiles==0.24.0 watchfiles==0.24.0
# via uvicorn # via uvicorn
websockets==13.1 websockets==14.1
# via uvicorn # via uvicorn

View File

@@ -19,7 +19,7 @@ dnspython==2.7.0
# via email-validator # via email-validator
email-validator==2.2.0 email-validator==2.2.0
# via fastapi # via fastapi
fastapi==0.115.4 fastapi==0.115.5
# via -r requirements.in # via -r requirements.in
fastapi-cli==0.0.5 fastapi-cli==0.0.5
# via fastapi # via fastapi
@@ -27,7 +27,7 @@ h11==0.14.0
# via # via
# httpcore # httpcore
# uvicorn # uvicorn
httpcore==1.0.6 httpcore==1.0.7
# via httpx # via httpx
httptools==0.6.4 httptools==0.6.4
# via uvicorn # via uvicorn
@@ -72,9 +72,9 @@ sniffio==1.3.1
# via # via
# anyio # anyio
# httpx # httpx
starlette==0.41.2 starlette==0.41.3
# via fastapi # via fastapi
typer==0.12.5 typer==0.13.1
# via fastapi-cli # via fastapi-cli
typing-extensions==4.12.2 typing-extensions==4.12.2
# via # via
@@ -90,5 +90,5 @@ uvloop==0.21.0
# via uvicorn # via uvicorn
watchfiles==0.24.0 watchfiles==0.24.0
# via uvicorn # via uvicorn
websockets==13.1 websockets==14.1
# via uvicorn # via uvicorn

View File

@@ -1,6 +1,6 @@
"""Main alfagok API application.""" """Main alfagok API application."""
import logging import logging
from datetime import date from datetime import date, datetime, timezone
from typing import Union from typing import Union
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
@@ -51,11 +51,19 @@ if settings.debug:
def get_game_id(): def get_game_id():
"""Calculate the index for the game/word we are handling today.""" """Calculate the index for the game/word we are handling today."""
today = date.today() today = datetime.now(timezone.utc).date()
# Calculate the amount of days since the start of the games so we know which word is used today # Calculate the amount of days since the start of the games so we know which word is used today
return (today - settings.start_date).days return (today - settings.start_date).days
def get_game_deadline():
"""Calculate the amount of time left for the current game."""
this_moment = datetime.now(timezone.utc)
midnight = datetime.now(timezone.utc).replace(hour=23, minute=59, second=59, microsecond=0)
# Calculate the amount of time left till midnight (and the start of the next game)
return midnight - this_moment
def is_valid_dictionary_word(word: str) -> bool: def is_valid_dictionary_word(word: str) -> bool:
"""Verify if `word` is in the dictionary provided.""" """Verify if `word` is in the dictionary provided."""
# Either we: [ ] strip all the endlines during file load, or [x] use the endline to search here # Either we: [ ] strip all the endlines during file load, or [x] use the endline to search here
@@ -71,8 +79,8 @@ async def index(request: Request):
@app.get('/api/game') @app.get('/api/game')
def what_game(): def what_game():
"""Handle incoming guess.""" """Which game is currently on?"""
return {'game': get_game_id()} return {'game': get_game_id(), 'deadline': get_game_deadline()}
@app.get('/api/guess/{word}') @app.get('/api/guess/{word}')

View File

@@ -37,6 +37,10 @@ a.title {
color: #CCC;; color: #CCC;;
} }
.guessesheading, .guessesbefore, .guessesafter {
text-align: center;
}
input[type="text"] { input[type="text"] {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
@@ -56,6 +60,7 @@ button {
ul { ul {
list-style-type: none; list-style-type: none;
padding-left: 0;
} }
.error { .error {

View File

@@ -2,6 +2,7 @@ document.addEventListener('alpine:init', () => {
Alpine.store('alfagok', { Alpine.store('alfagok', {
/* Main alfagok application, state etc */ /* Main alfagok application, state etc */
gameID: 0, gameID: 0,
countingDown: '',
loading: false, loading: false,
@@ -36,7 +37,8 @@ document.addEventListener('alpine:init', () => {
async doGuess() { async doGuess() {
this.guessError = null; this.guessError = null;
this.guessValue = this.guessValue.toLowerCase(); /* Normalise on lowercase, and strip whitespace from begin and end, just in case */
this.guessValue = this.guessValue.toLowerCase().trim();
if (this.guessValue === '') { if (this.guessValue === '') {
console.log('Nothing filled in'); console.log('Nothing filled in');
@@ -48,7 +50,6 @@ document.addEventListener('alpine:init', () => {
return; return;
} }
this.nrGuesses++;
if (this.startTime === null) { if (this.startTime === null) {
console.log('Setting startTime to now'); console.log('Setting startTime to now');
this.startTime = new Date(); this.startTime = new Date();
@@ -70,6 +71,7 @@ document.addEventListener('alpine:init', () => {
} }
return; return;
} }
this.nrGuesses++;
if (result.hint && result.hint === 'after') { if (result.hint && result.hint === 'after') {
this.guessesBefore.push(this.guessValue); this.guessesBefore.push(this.guessValue);
this.guessesBefore.sort(); this.guessesBefore.sort();
@@ -87,7 +89,47 @@ document.addEventListener('alpine:init', () => {
this.resultGuesses = '🤔 '+ this.nrGuesses + ' gokken'; this.resultGuesses = '🤔 '+ this.nrGuesses + ' gokken';
this.resultTimeTaken = '⏱️ ' + getFormattedTime(this.winTime - this.startTime); this.resultTimeTaken = '⏱️ ' + getFormattedTime(this.winTime - this.startTime);
} }
},
getFormattedTime(milliseconds) {
if (!Number.isInteger(milliseconds)) {
return '';
} }
let seconds = Math.round((milliseconds) / 1000);
const hours = Math.floor(seconds / 3600);
seconds %= 3600;
const minutes = Math.floor(seconds / 60);
seconds %= 60;
const formattedTime = [];
if (hours) {
formattedTime.push(`${hours}u`);
}
if (minutes) {
formattedTime.push(`${minutes}m`);
}
if (seconds) {
formattedTime.push(`${seconds}s`);
}
return formattedTime.join(' ') || '0s';
},
addZero(num){
if(num <=9) return '0'+num;
else return num;
},
countDownTimer(){
let nextgame = document.getElementById('nextgame');
let now = new Date();
let midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1, 0, 0, 0);
let diff = Math.floor((midnight - now)/1000);
let hoursRemain = Math.floor(diff/(60*60));
let minutesRemain = Math.floor((diff-hoursRemain*60*60)/60);
let secondsRemain = Math.floor(diff%60);
nextgame.innerHTML = '<span class="nextgame">'+addZero(hoursRemain)+':'+addZero(minutesRemain)+':'+addZero(secondsRemain)+' over</span>';
}
}), }),
Alpine.store('darkMode', { Alpine.store('darkMode', {
@@ -134,7 +176,7 @@ function getFormattedTime(milliseconds) {
/* Clipboard stuff **/ /* Clipboard stuff **/
var clip = new Clipboard('.copy'); let clip = new ClipboardJS('.copy');
clip.on("success", function(e) { clip.on("success", function(e) {
document.getElementById('copyresults').innerHTML = '<p style="font-size:var(--small);opacity:50%">Gekopieerd! Deel je resultaat.</p>'; document.getElementById('copyresults').innerHTML = '<p style="font-size:var(--small);opacity:50%">Gekopieerd! Deel je resultaat.</p>';
@@ -153,13 +195,13 @@ function go() {
} }
function timer(){ function timer(){
var nextgame = document.getElementById('nextgame'); let nextgame = document.getElementById('nextgame');
var now = new Date(); let now = new Date();
var midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1, 0, 0, 0); let midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1, 0, 0, 0);
var diff = Math.floor((midnight - now)/1000); let diff = Math.floor((midnight - now)/1000);
var hoursRemain = Math.floor(diff/(60*60)); let hoursRemain = Math.floor(diff/(60*60));
var minutesRemain = Math.floor((diff-hoursRemain*60*60)/60); let minutesRemain = Math.floor((diff-hoursRemain*60*60)/60);
var secondsRemain = Math.floor(diff%60); let secondsRemain = Math.floor(diff%60);
nextgame.innerHTML = '<span class="nextgame">'+addZero(hoursRemain)+':'+addZero(minutesRemain)+':'+addZero(secondsRemain)+' over</span>'; nextgame.innerHTML = '<span class="nextgame">'+addZero(hoursRemain)+':'+addZero(minutesRemain)+':'+addZero(secondsRemain)+' over</span>';
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
{"name":"alfagok","short_name":"alfagok","icons":[{"src":"/static/images/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/static/images//android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -6,20 +6,24 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Raad het woord van de dag. Elke gok geeft een hint over waar het woord zich in het alfabet bevindt. Iedereen speelt hetzelfde woord."> <meta name="description" content="Raad het woord van de dag. Elke gok geeft een hint over waar het woord zich in het alfabet bevindt. Iedereen speelt hetzelfde woord.">
<link rel="stylesheet" href="/static/game.css"> <link rel="stylesheet" href="/static/game.css">
<link id="favicon" rel="icon" type="image/x-icon" href="static/images/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="static/images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/images/favicon-16x16.png">
<link rel="manifest" href="static/images/site.webmanifest">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head> </head>
<body> <body>
<div id="container" x-data=""> <div id="container" x-data="">
<a href="/" x-cloak class="title">alfagok</a> <span class="puzzleno">puzzel #<span x-text="$store.alfagok.gameID"></span><span id="nextgame"></span></span> <a href="/" x-cloak class="title">alfagok</a> <span class="puzzleno">puzzel #<span x-text="$store.alfagok.gameID"></span><span id="nextgame"></span> | <span x-text="$store.alfagok.countingDown" <span x-text="$store.alfagok.nrGuesses"></span> gokken</span>
<div x-cloak class="instructions" x-show="$store.alfagok.guessesBefore.length === 0 && $store.alfagok.guessesAfter.length === 0"> <div x-cloak class="instructions" x-show="$store.alfagok.guessesBefore.length === 0 && $store.alfagok.guessesAfter.length === 0">
<p>Raad het woord van de dag. Elke gok geeft een hint over waar het woord zich in het alfabet bevindt.</p> <p>Raad het woord van de dag. Elke gok geeft een hint over waar het woord zich in het alfabet bevindt.</p>
<p x-show="$store.alfagok.isLocalStorageAvailable"></p>
</div> </div>
<center>
<p class="guessesheading" x-cloak x-show="$store.alfagok.guessesBefore.length">Het woord van de dag komt <em>na</em>:</p> <p class="guessesheading" x-cloak x-show="$store.alfagok.guessesBefore.length">Het woord van de dag komt <em>na</em>:</p>
<ul class="guessesbefore"> <ul class="guessesbefore">
<template x-for="item in $store.alfagok.guessesBefore" :key="item"> <template x-for="item in $store.alfagok.guessesBefore" :key="item">
@@ -58,11 +62,9 @@
</template> </template>
</ul> </ul>
</center>
</div> </div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.5.12/clipboard.min.js" rel=preload></script> <script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.11/dist/clipboard.min.js"></script>
<script src="/static/game.js"></script> <script src="/static/game.js"></script>
{# {#
<button x-data @click="$store.darkMode.toggle()">Toggle Dark Mode</button> <button x-data @click="$store.darkMode.toggle()">Toggle Dark Mode</button>

View File

@@ -1,3 +1,4 @@
import os.path
import random import random
MIN_LENGTH = 4 MIN_LENGTH = 4
@@ -5,41 +6,97 @@ MAX_LENGTH = 10
NUMBER_DAYS = 5 * 365 NUMBER_DAYS = 5 * 365
with open('basiswoorden-gekeurd.txt', 'r', encoding='utf-8') as wordfile: # Set to True if you want to use the big but difficult OpenTaal list
all_words = wordfile.readlines() USE_OPENTAAL = False
print(f'original list contains {len(all_words)} words')
dictionary_list = [] with open('wikiwoordenboek_basiswoorden.lst', 'r', encoding='utf-8') as wordfile:
result_list = [] wikiwoorden_words = wordfile.readlines()
for word in all_words: print(f'wikiwoorden basic list contains {len(wikiwoorden_words)} words')
scrabble_words = []
if os.path.isfile('scrabblewoorden.txt'):
with open('scrabblewoorden.txt', 'r', encoding='utf-8') as wordfile:
scrabble_words = wordfile.readlines()
print(f'scrabblewoorden list contains {len(scrabble_words)} words')
else:
print('scrabblewoorden.txt not found, skipped')
with open('basiswoorden-gekeurd.txt', 'r', encoding='utf-8') as wordfile:
basis_words = wordfile.readlines()
print(f'opentaal basic list contains {len(basis_words)} words')
with open('flexies-ongekeurd.txt', 'r', encoding='utf-8') as wordfile:
# Vervoegingen and such, see https://nl.wikipedia.org/wiki/Flexie_(taalkunde)
flexies_words = wordfile.readlines()
print(f'flexies list contains {len(flexies_words)} words')
with open('/usr/share/dict/american-english', 'r', encoding='utf-8') as wordfile:
# English words we want to filter from the list; don't really care if there's accidental overlap with Dutch words
english_words = wordfile.readlines()
print(f'english list contains {len(english_words)} words')
print()
print('merging and filtering...')
print()
all_words_count = 0
dictionary_list = []
result_list = []
for word in wikiwoorden_words + scrabble_words + basis_words + flexies_words:
all_words_count += 1
word = word.strip() word = word.strip()
if word.isalpha() and word.lower() == word: if word.isalpha() and word.lower() == word:
# Word is valid for our dictionary # Word is valid for our dictionary
dictionary_list.append(f'{word}\n') dictionary_list.append(f'{word}\n')
# Deduplicate dictionary
dictionary_list = sorted(list(set(dictionary_list)), key=str.casefold)
if USE_OPENTAAL:
# Use basis_words if you want to use the big but difficult OpenTaal list
source_words = basis_words
else:
# Combine the basic words and the Scrabble word lists
source_words = wikiwoorden_words + scrabble_words
for word in source_words:
word = word.strip()
if word.isalpha() and word.lower() == word and len(word) >= MIN_LENGTH and len(word) <= MAX_LENGTH: if word.isalpha() and word.lower() == word and len(word) >= MIN_LENGTH and len(word) <= MAX_LENGTH:
# Word is 'fit' for our game # Word is 'fit' for our game
result_list.append(f'{word}\n') result_list.append(f'{word}\n')
# print(result_list) if USE_OPENTAAL:
print(f'words filtered: {len(result_list)} with length >= {MIN_LENGTH} and <= {MAX_LENGTH}') nl_set = set(result_list)
print(f'words in dictionary: {len(dictionary_list)}') en_set = set(english_words)
with open('filtered.txt', 'w', encoding='utf-8') as f: # Only keep the words that are not found in the English set
f.writelines(result_list) filtered_set = nl_set.difference(en_set)
filtered_list = sorted(list(filtered_set), key=str.casefold)
else:
filtered_list = sorted(list(set(result_list)), key=str.casefold)
with open('dictionary.txt', 'w', encoding='utf-8') as f: print(f'words total: {all_words_count}')
print(f'words in dictionary: {len(dictionary_list)}')
print(f'words initially filtered: {len(result_list)} with length >= {MIN_LENGTH} and <= {MAX_LENGTH}')
if USE_OPENTAAL:
print(f'words after filtering english: {len(filtered_list)}')
with open('filtered.txt', 'w', encoding='utf-8') as f:
f.writelines(filtered_list)
with open('dictionary.txt', 'w', encoding='utf-8') as f:
f.writelines(dictionary_list) f.writelines(dictionary_list)
selection_list = [] selection_list = []
# Randomly select words for each day # Randomly select words for each day
while len(selection_list) < NUMBER_DAYS: while len(selection_list) < NUMBER_DAYS:
# Use index - 1 because lists start at index 0 # Use index - 1 because lists start at index 0
word_index = random.randrange(0, len(result_list) - 1) word_index = random.randrange(0, len(filtered_list) - 1)
selection_list.append(result_list[word_index]) selection_list.append(filtered_list[word_index])
# Save the result # Save the result
with open('word_of_the_day.txt', 'w', encoding='utf-8') as f: with open('word_of_the_day.txt', 'w', encoding='utf-8') as f:
f.writelines(selection_list) f.writelines(selection_list)
print(f'done writing {len(selection_list)} random words, enjoy!') print(f'done writing {len(selection_list)} random words, enjoy!')