10 Commits

14 changed files with 133 additions and 67 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%;

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,
@@ -88,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', {
@@ -135,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>';
@@ -150,23 +191,23 @@ clip.on("error", function() {
/* Game timer, original from alphaguess.com **/ /* Game timer, original from alphaguess.com **/
function go() { function go() {
window.timerID = window.setInterval(timer, 0); window.timerID = window.setInterval(timer, 0);
} }
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>';
} }
function addZero(num){ function addZero(num){
if(num <=9) return '0'+num; if(num <=9) return '0'+num;
else return num; else return num;
} }
go(); go();

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,63 +6,65 @@
<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 x-text="$store.alfagok.nrGuesses"></span> gokken</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>
<ul class="guessesbefore">
<template x-for="item in $store.alfagok.guessesBefore" :key="item">
<li x-text="item"></li>
</template>
</ul>
<p class="guessesheading" x-cloak x-show="$store.alfagok.guessesBefore.length">Het woord van de dag komt <em>na</em>:</p> <div x-show="!$store.alfagok.winTime">
<ul class="guessesbefore"> <input type="text" autocomplete="new-password" autocorrect="off" x-model="$store.alfagok.guessValue" @keyup.enter="$store.alfagok.doGuess()">
<template x-for="item in $store.alfagok.guessesBefore" :key="item">
<li x-text="item"></li>
</template>
</ul>
<div x-show="!$store.alfagok.winTime">
<input type="text" autocomplete="new-password" autocorrect="off" x-model="$store.alfagok.guessValue" @keyup.enter="$store.alfagok.doGuess()">
{# <p x-cloak>Je huidige gok is: <span x-text="$store.alfagok.guessValue"></span></p>#} {# <p x-cloak>Je huidige gok is: <span x-text="$store.alfagok.guessValue"></span></p>#}
<button @click="$store.alfagok.doGuess()">Doe een gok</button> <button @click="$store.alfagok.doGuess()">Doe een gok</button>
<p class="error" x-cloak x-show="$store.alfagok.guessError" x-text="$store.alfagok.guessError"></p> <p class="error" x-cloak x-show="$store.alfagok.guessError" x-text="$store.alfagok.guessError"></p>
</div> </div>
<div x-cloak x-show="$store.alfagok.winTime" class="win"> <div x-cloak x-show="$store.alfagok.winTime" class="win">
<h3><b>Je hebt hem! 🎉</b></h3> <h3><b>Je hebt hem! 🎉</b></h3>
<p>Het woord van vandaag was <b x-text="$store.alfagok.guessValue"></b>.</p> <p>Het woord van vandaag was <b x-text="$store.alfagok.guessValue"></b>.</p>
<div id="stats"> <div id="stats">
<div id="results"> <div id="results">
<p><b x-text="$store.alfagok.resultGameID"></b></p> <p><b x-text="$store.alfagok.resultGameID"></b></p>
<p x-text="$store.alfagok.resultGuesses"></p> <p x-text="$store.alfagok.resultGuesses"></p>
<p x-text="$store.alfagok.resultTimeTaken"></p> <p x-text="$store.alfagok.resultTimeTaken"></p>
<p>🔗 <span style="color:var(--blue)">alfagok.diginaut.net</span></p> <p>🔗 <span style="color:var(--blue)">alfagok.diginaut.net</span></p>
</div>
</div> </div>
<div id="copyresults"></div>
<button class="copy" data-clipboard-target="#results">
Tik om te kopi&euml;ren en te delen ❤️
</button>
</div> </div>
<div id="copyresults"></div>
<button class="copy" data-clipboard-target="#results">
Tik om te kopi&euml;ren en te delen ❤️
</button>
</div>
<p class="guessesheading" x-cloak x-show="$store.alfagok.guessesAfter.length">Het woord van de dag komt <em>voor</em>:</p> <p class="guessesheading" x-cloak x-show="$store.alfagok.guessesAfter.length">Het woord van de dag komt <em>voor</em>:</p>
<ul class="guessesafter"> <ul class="guessesafter">
<template x-for="item in $store.alfagok.guessesAfter" :key="item"> <template x-for="item in $store.alfagok.guessesAfter" :key="item">
<li x-text="item"></li> <li x-text="item"></li>
</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
@@ -12,6 +13,14 @@ with open('wikiwoordenboek_basiswoorden.lst', 'r', encoding='utf-8') as wordfile
wikiwoorden_words = wordfile.readlines() wikiwoorden_words = wordfile.readlines()
print(f'wikiwoorden basic list contains {len(wikiwoorden_words)} 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: with open('basiswoorden-gekeurd.txt', 'r', encoding='utf-8') as wordfile:
basis_words = wordfile.readlines() basis_words = wordfile.readlines()
print(f'opentaal basic list contains {len(basis_words)} words') print(f'opentaal basic list contains {len(basis_words)} words')
@@ -33,7 +42,7 @@ print()
all_words_count = 0 all_words_count = 0
dictionary_list = [] dictionary_list = []
result_list = [] result_list = []
for word in wikiwoorden_words + basis_words + flexies_words: for word in wikiwoorden_words + scrabble_words + basis_words + flexies_words:
all_words_count += 1 all_words_count += 1
word = word.strip() word = word.strip()
if word.isalpha() and word.lower() == word: if word.isalpha() and word.lower() == word:
@@ -47,7 +56,8 @@ if USE_OPENTAAL:
# Use basis_words if you want to use the big but difficult OpenTaal list # Use basis_words if you want to use the big but difficult OpenTaal list
source_words = basis_words source_words = basis_words
else: else:
source_words = wikiwoorden_words # Combine the basic words and the Scrabble word lists
source_words = wikiwoorden_words + scrabble_words
for word in source_words: for word in source_words:
word = word.strip() word = word.strip()
@@ -63,7 +73,7 @@ if USE_OPENTAAL:
filtered_set = nl_set.difference(en_set) filtered_set = nl_set.difference(en_set)
filtered_list = sorted(list(filtered_set), key=str.casefold) filtered_list = sorted(list(filtered_set), key=str.casefold)
else: else:
filtered_list = sorted(list(wikiwoorden_words), key=str.casefold) filtered_list = sorted(list(set(result_list)), key=str.casefold)
print(f'words total: {all_words_count}') print(f'words total: {all_words_count}')
print(f'words in dictionary: {len(dictionary_list)}') print(f'words in dictionary: {len(dictionary_list)}')