19 Commits

Author SHA1 Message Date
0ae7b8618f Reset game state when new/different game has started 2024-11-24 16:49:00 +01:00
f52ae3cb8f Cleaned up non-Alpine.js persistence code 2024-11-24 16:45:23 +01:00
bbfa172e7a Persist the important game data in localStorage 2024-11-24 16:32:13 +01:00
795ee7aa16 Started work on saving game state 2024-11-23 22:16:47 +01:00
48ccb99492 Restored accidentally removed test-for-localStorage functions 2024-11-23 21:38:42 +01:00
4058dc3a7a Merge branch 'main' into savegames 2024-11-23 21:20:18 +01:00
e7a39becfc Latest requirements 2024-11-23 21:18:18 +01:00
cc20e80bf5 Initial support for server-sourced deadline 2024-11-23 14:05:46 +01:00
622d3d9681 Latest clipboard.js 2024-11-23 13:36:35 +01:00
28ff9368bb var-b-let 2024-11-23 13:24:45 +01:00
1ce340853f Use CSS to style centering the guess lists 2024-11-22 13:05:56 +01:00
20321f0bb4 Use UTC for calculating the current game 2024-11-22 13:05:52 +01:00
f8a3561d9a Use localStorage for restoring saveGame 2024-11-19 22:18:56 +01:00
6d12d3bbc5 Fixed paths to the favicons 2024-11-19 22:01:21 +01:00
f0deb1a6f3 Latest requirements 2024-11-19 21:20:53 +01:00
f3117ce121 Added favicon in various formats 2024-11-19 21:19:57 +01:00
b3c6da59b8 Only use Scrabble words when available 2024-11-19 15:20:20 +01:00
49e40201dc 20241108: Scrabblewoordenboek as source 2024-11-19 15:17:21 +01:00
c0c0ecb28b Make sure there's no accidental space before or after the word 2024-11-09 18:46:13 +01:00
14 changed files with 181 additions and 86 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,13 +48,13 @@ 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.10.1
# via # via
# fastapi # fastapi
# pydantic-settings # pydantic-settings
pydantic-core==2.23.4 pydantic-core==2.27.1
# via pydantic # via pydantic
pydantic-settings==2.6.1 pydantic-settings==2.6.1
# via -r requirements.in # via -r requirements.in
@@ -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
@@ -86,7 +86,7 @@ typing-extensions==4.12.2
# pydantic # pydantic
# pydantic-core # pydantic-core
# typer # typer
uvicorn==0.32.0 uvicorn==0.32.1
# via # via
# fastapi # fastapi
# fastapi-cli # fastapi-cli
@@ -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
@@ -46,11 +46,11 @@ markupsafe==3.0.2
# via jinja2 # via jinja2
mdurl==0.1.2 mdurl==0.1.2
# via markdown-it-py # via markdown-it-py
pydantic==2.9.2 pydantic==2.10.1
# via # via
# fastapi # fastapi
# pydantic-settings # pydantic-settings
pydantic-core==2.23.4 pydantic-core==2.27.1
# via pydantic # via pydantic
pydantic-settings==2.6.1 pydantic-settings==2.6.1
# via -r requirements.in # via -r requirements.in
@@ -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
@@ -82,7 +82,7 @@ typing-extensions==4.12.2
# pydantic # pydantic
# pydantic-core # pydantic-core
# typer # typer
uvicorn==0.32.0 uvicorn==0.32.1
# via # via
# fastapi # fastapi
# fastapi-cli # fastapi-cli
@@ -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

@@ -1,24 +1,30 @@
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.store('alfagok', { Alpine.store('alfagok', {
// isLocalStorageAvailable: this.testLocalStorage(),
isLocalStorageAvailable: false,
savedGameKey: 'saveGame',
/* Main alfagok application, state etc */ /* Main alfagok application, state etc */
gameID: 0, gameID: Alpine.$persist(0).as('gameID'),
countingDown: '',
loading: false, loading: false,
winTime: null, winTime: Alpine.$persist(null).as('winTime'),
startTime: null, startTime: Alpine.$persist(null).as('startTime'),
gaveUpTime: Alpine.$persist(null).as('gaveUpTime'), // not implemented yet
nrGuesses: 0, nrGuesses: Alpine.$persist(0).as('nrGuesses'),
guessesBefore: [], guessesBefore: Alpine.$persist([]).as('guessesBefore'),
guessesAfter: [], guessesAfter: Alpine.$persist([]).as('guessesAfter'),
guessValue: '', guessValue: Alpine.$persist('').as('guessValue'),
guessError: '', guessError: '',
resultGameID: '', resultGameID: Alpine.$persist('').as('resultGameID'),
resultGuesses: '', resultGuesses: Alpine.$persist('').as('resultGuesses'),
resultTimeTaken: '', resultTimeTaken: Alpine.$persist('').as('resultTimeTaken'),
async getGameID() { async getGameID() {
/* Get the game number from the backend */ /* Get the game number from the backend */
@@ -29,6 +35,9 @@ document.addEventListener('alpine:init', () => {
console.log(result); console.log(result);
this.loading = false; this.loading = false;
if (result.game) { if (result.game) {
if (this.gameID !== result.game) {
this.setEmptyGameState();
}
return this.gameID = result.game; return this.gameID = result.game;
} }
}, },
@@ -36,7 +45,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');
@@ -85,10 +95,69 @@ document.addEventListener('alpine:init', () => {
this.winTime = new Date(); this.winTime = new Date();
this.resultGameID = '🧩 Puzzel #' + this.gameID; this.resultGameID = '🧩 Puzzel #' + this.gameID;
this.resultGuesses = '🤔 '+ this.nrGuesses + ' gokken'; this.resultGuesses = '🤔 '+ this.nrGuesses + ' gokken';
this.resultTimeTaken = '⏱️ ' + getFormattedTime(this.winTime - this.startTime); let winTimeDate = new Date(this.winTime);
let startTimeDate = new Date(this.startTime);
// this.resultTimeTaken = '⏱️ ' + getFormattedTime(this.winTime - this.startTime);
this.resultTimeTaken = '⏱️ ' + getFormattedTime(winTimeDate - startTimeDate);
} }
},
setEmptyGameState() {
this.winTime = null;
this.startTime = null;
this.nrGuesses = 0;
this.guessesBefore = [];
this.guessesAfter = [];
this.guessValue = '';
this.guessError = '';
this.resultGameID = '';
this.resultGuesses = '';
this.resultTimeTaken = '';
this.getGameID();
},
// # Countdown timer
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', {
/* Different Alpine app, dark mode settings for the game */ /* Different Alpine app, dark mode settings for the game */
@@ -134,7 +203,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 +222,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,25 @@
<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 src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@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> <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><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">
@@ -42,7 +47,7 @@
<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 class="link">alfagok.diginaut.net</span></p>
</div> </div>
</div> </div>
<div id="copyresults"></div> <div id="copyresults"></div>
@@ -58,11 +63,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
@@ -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)}')