12 Commits

3 changed files with 91 additions and 93 deletions

View File

@@ -3,7 +3,7 @@ import logging
from datetime import date, datetime, timezone from datetime import date, datetime, timezone
from typing import Union from typing import Union
from fastapi import FastAPI, Request from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
@@ -11,6 +11,9 @@ from pydantic import DirectoryPath, FilePath
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
VERSION = '0.3.0'
class Settings(BaseSettings): class Settings(BaseSettings):
"""Configuration needed for alfagok to find its word list, using environment variables.""" """Configuration needed for alfagok to find its word list, using environment variables."""
@@ -74,7 +77,7 @@ def is_valid_dictionary_word(word: str) -> bool:
async def index(request: Request): async def index(request: Request):
"""Generate the main HTML page of the game.""" """Generate the main HTML page of the game."""
language = 'nl' language = 'nl'
return templates.TemplateResponse(request=request, name='index.html', context={'language': language}) return templates.TemplateResponse(request=request, name='index.html', context={'language': language, 'version': VERSION})
@app.get('/api/game') @app.get('/api/game')
@@ -106,7 +109,10 @@ def handle_guess(word: Union[str, None] = None):
@app.get('/api/answer/{item_id}') @app.get('/api/answer/{item_id}')
def read_item(item_id: int, guess: Union[str, None] = None): def read_item(item_id: int):
"""Get the word for the current game.""" """Get the word for the game with ID `item_id`."""
current_game_id = get_game_id()
if item_id > current_game_id:
raise HTTPException(status_code=403, detail='No peaking!')
word = words[item_id].strip() word = words[item_id].strip()
return {'item_id': item_id, 'guess': guess, 'word': word} return {'item_id': item_id, 'word': word}

View File

@@ -1,28 +1,39 @@
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.store('alfagok', { Alpine.store('alfagok', {
/* Main alfagok application, state etc */ /** Main alfagok application, state etc */
gameID: 0, gameID: Alpine.$persist(0).as('gameID'),
countingDown: '', countingDown: '',
nextGameIn: 0,
gameFetchedAt: null,
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 init() {
/** Initialise the application after loading */
await this.getGameID();
setInterval(() => {
// Update counter to next game (midnight UTC, fetched from API) every second
this.countDownTimer();
}, 1000);
},
async getGameID() { async getGameID() {
/* Get the game number from the backend */ /** Get the game number from the backend */
this.loading = true; this.loading = true;
console.log('Loading gameID...'); console.log('Loading gameID...');
let response = await fetch('/api/game'); let response = await fetch('/api/game');
@@ -30,11 +41,19 @@ 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();
}
this.nextGameIn = result.deadline;
this.gameFetchedAt = new Date();
if (this.countingDown === '') {
this.countDownTimer();
}
return this.gameID = result.game; return this.gameID = result.game;
} }
}, },
async doGuess() { async doGuess() {
/** Handle the newly entered guess */
this.guessError = null; this.guessError = null;
/* Normalise on lowercase, and strip whitespace from begin and end, just in case */ /* Normalise on lowercase, and strip whitespace from begin and end, just in case */
@@ -87,12 +106,32 @@ 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 = '⏱️ ' + this.getFormattedTime(winTimeDate - startTimeDate);
} }
}, },
setEmptyGameState() {
/** Clean slate for new game */
this.winTime = null;
this.startTime = null;
this.gaveUpTime = null;
this.nrGuesses = 0;
this.guessesBefore = [];
this.guessesAfter = [];
this.guessValue = '';
this.guessError = '';
this.resultGameID = '';
this.resultGuesses = '';
this.resultTimeTaken = '';
},
getFormattedTime(milliseconds) { getFormattedTime(milliseconds) {
/** Nicely format time for 'time played' */
if (!Number.isInteger(milliseconds)) { if (!Number.isInteger(milliseconds)) {
return ''; return '';
} }
@@ -115,22 +154,26 @@ document.addEventListener('alpine:init', () => {
return formattedTime.join(' ') || '0s'; return formattedTime.join(' ') || '0s';
}, },
// # Countdown timer
addZero(num){ addZero(num){
/** Pad with 0 if needed */
if (num <= 9) return '0' + num; if (num <= 9) return '0' + num;
else return num; else return num;
}, },
countDownTimer(){ countDownTimer(){
let nextgame = document.getElementById('nextgame'); /** Update counter to next game (midnight UTC, fetched from API) */
if (this.gameFetchedAt === null) { return; }
let now = new Date(); let now = new Date();
let midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1, 0, 0, 0); let gameDataFetched = new Date(this.gameFetchedAt);
let diff = Math.floor((midnight - now)/1000); let nextGameStart = gameDataFetched.setSeconds(gameDataFetched.getSeconds() + this.nextGameIn);
let diff = Math.floor((nextGameStart - now) / 1000);
let hoursRemain = Math.floor(diff / (60*60)); let hoursRemain = Math.floor(diff / (60*60));
let minutesRemain = Math.floor((diff - hoursRemain*60*60) / 60); let minutesRemain = Math.floor((diff - hoursRemain*60*60) / 60);
let secondsRemain = Math.floor(diff % 60); let secondsRemain = Math.floor(diff % 60);
nextgame.innerHTML = '<span class="nextgame">'+addZero(hoursRemain)+':'+addZero(minutesRemain)+':'+addZero(secondsRemain)+' over</span>';
}
}), this.countingDown = this.addZero(hoursRemain) + ':' + this.addZero(minutesRemain) + ':' + this.addZero(secondsRemain) + ' over';
}
})
Alpine.store('darkMode', { Alpine.store('darkMode', {
/* Different Alpine app, dark mode settings for the game */ /* Different Alpine app, dark mode settings for the game */
@@ -148,32 +191,6 @@ document.addEventListener('alpine:init', () => {
}); });
/* Time formatting **/
function 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';
}
/* Clipboard stuff **/ /* Clipboard stuff **/
let clip = new ClipboardJS('.copy'); let clip = new ClipboardJS('.copy');
@@ -188,34 +205,9 @@ clip.on("error", function() {
}); });
/* Game timer, original from alphaguess.com **/
function go() {
window.timerID = window.setInterval(timer, 0);
}
function timer(){
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>';
}
function addZero(num){
if(num <=9) return '0'+num;
else return num;
}
go();
/* Get current gameID etc **/ /* Get current gameID etc **/
document.addEventListener('alpine:initialized', () => { // document.addEventListener('alpine:initialized', () => {
/* On AlpineJS completely loaded, do all this */ /* On AlpineJS completely loaded, do all this */
Alpine.store('alfagok').getGameID(); // Alpine.store('alfagok').getGameID();
}) // })

View File

@@ -5,23 +5,23 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<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?v={{ version }}">
<link id="favicon" rel="icon" type="image/x-icon" href="static/images/favicon.ico"> <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="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="32x32" href="/static/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/images/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
<link rel="manifest" href="static/images/site.webmanifest"> <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.countingDown" <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 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>
<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>
@@ -46,7 +46,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>
@@ -65,7 +65,7 @@
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.11/dist/clipboard.min.js"></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?v={{ version }}"></script>
{# {#
<button x-data @click="$store.darkMode.toggle()">Toggle Dark Mode</button> <button x-data @click="$store.darkMode.toggle()">Toggle Dark Mode</button>
<div x-data :class="$store.darkMode.on && 'bg-black'">...</div> <div x-data :class="$store.darkMode.on && 'bg-black'">...</div>