7 Commits

4 changed files with 64 additions and 111 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

@@ -33,8 +33,8 @@ a.title {
padding: 2rem 0 0 0; padding: 2rem 0 0 0;
} }
.guessesheading { .guessesheading, .copied {
color: #CCC;; color: #CCC;
} }
.guessesheading, .guessesbefore, .guessesafter { .guessesheading, .guessesbefore, .guessesafter {

View File

@@ -1,12 +1,10 @@
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.store('alfagok', { Alpine.store('alfagok', {
// isLocalStorageAvailable: this.testLocalStorage(), /** Main alfagok application, state etc */
isLocalStorageAvailable: false,
savedGameKey: 'saveGame',
/* Main alfagok application, state etc */
gameID: Alpine.$persist(0).as('gameID'), gameID: Alpine.$persist(0).as('gameID'),
countingDown: '', countingDown: '',
nextGameIn: 0,
gameFetchedAt: null,
loading: false, loading: false,
@@ -26,8 +24,18 @@ document.addEventListener('alpine:init', () => {
resultGuesses: Alpine.$persist('').as('resultGuesses'), resultGuesses: Alpine.$persist('').as('resultGuesses'),
resultTimeTaken: Alpine.$persist('').as('resultTimeTaken'), resultTimeTaken: Alpine.$persist('').as('resultTimeTaken'),
resultsCopied: false,
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');
@@ -38,11 +46,16 @@ document.addEventListener('alpine:init', () => {
if (this.gameID !== result.game) { if (this.gameID !== result.game) {
this.setEmptyGameState(); 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 */
@@ -97,13 +110,14 @@ document.addEventListener('alpine:init', () => {
this.resultGuesses = '🤔 '+ this.nrGuesses + ' gokken'; this.resultGuesses = '🤔 '+ this.nrGuesses + ' gokken';
let winTimeDate = new Date(this.winTime); let winTimeDate = new Date(this.winTime);
let startTimeDate = new Date(this.startTime); let startTimeDate = new Date(this.startTime);
// this.resultTimeTaken = '⏱️ ' + getFormattedTime(this.winTime - this.startTime); this.resultTimeTaken = '⏱️ ' + this.getFormattedTime(winTimeDate - startTimeDate);
this.resultTimeTaken = '⏱️ ' + getFormattedTime(winTimeDate - startTimeDate);
} }
}, },
setEmptyGameState() { setEmptyGameState() {
/** Clean slate for new game */
this.winTime = null; this.winTime = null;
this.startTime = null; this.startTime = null;
this.gaveUpTime = null;
this.nrGuesses = 0; this.nrGuesses = 0;
this.guessesBefore = []; this.guessesBefore = [];
@@ -116,11 +130,9 @@ document.addEventListener('alpine:init', () => {
this.resultGameID = ''; this.resultGameID = '';
this.resultGuesses = ''; this.resultGuesses = '';
this.resultTimeTaken = ''; this.resultTimeTaken = '';
this.getGameID();
}, },
// # Countdown timer
getFormattedTime(milliseconds) { getFormattedTime(milliseconds) {
/** Nicely format time for 'time played' */
if (!Number.isInteger(milliseconds)) { if (!Number.isInteger(milliseconds)) {
return ''; return '';
} }
@@ -143,19 +155,24 @@ 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';
} }
}) })
@@ -174,75 +191,7 @@ document.addEventListener('alpine:init', () => {
}); });
// document.addEventListener('alpine:initialized', () => {
/* 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 **/
let clip = new ClipboardJS('.copy');
clip.on("success", function(e) {
document.getElementById('copyresults').innerHTML = '<p style="font-size:var(--small);opacity:50%">Gekopieerd! Deel je resultaat.</p>';
e.clearSelection();
});
clip.on("error", function() {
document.getElementById('copyresults').innerHTML = '<p style="font-size:var(--small);opacity:50%">Fout. Graag handmatig kopi&euml;ren...</p>';
});
/* 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 **/
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,24 +5,24 @@
<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 src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-clipboard@2.x.x/dist/alpine-clipboard.js" defer></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><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>
@@ -50,8 +50,8 @@
<p>🔗 <span class="link">alfagok.diginaut.net</span></p> <p>🔗 <span class="link">alfagok.diginaut.net</span></p>
</div> </div>
</div> </div>
<div id="copyresults"></div> <div class="copied" x-show="$store.alfagok.resultsCopied">Gekopieerd! Deel je resultaat.</div>
<button class="copy" data-clipboard-target="#results"> <button class="copy" @click="$clipboard($store.alfagok.resultGameID + '\n' + $store.alfagok.resultGuesses + '\n' + $store.alfagok.resultTimeTaken + '\n' + '🔗 alfagok.diginaut.net'); $store.alfagok.resultsCopied = true">
Tik om te kopi&euml;ren en te delen ❤️ Tik om te kopi&euml;ren en te delen ❤️
</button> </button>
</div> </div>
@@ -65,12 +65,10 @@
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.11/dist/clipboard.min.js"></script> <script src="/static/game.js?v={{ version }}"></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>
<div x-data :class="$store.darkMode.on && 'bg-black'">...</div> <div x-data :class="$store.darkMode.on && 'bg-black'">...</div>
#} #}
</body> </body>
</html> </html>