feat(app/flags): game data shares
All checks were successful
build / build (push) Successful in 24s
release-tag / release-image (push) Successful in 16m55s

This commit is contained in:
Tijl 2024-08-26 20:28:20 +02:00
parent 5182cf8bfd
commit 84041c6500
Signed by: tijl
GPG Key ID: DAE24BFCD722F053
9 changed files with 276 additions and 65 deletions

View File

@ -5,13 +5,13 @@ import (
"database/sql"
"net/http"
"strconv"
"time"
"git.tijl.dev/tijl/tijl.dev-core/internal/queries"
"git.tijl.dev/tijl/tijl.dev-core/internal/user"
"git.tijl.dev/tijl/tijl.dev-core/internal/utils"
"git.tijl.dev/tijl/tijl.dev-core/modules/db"
"git.tijl.dev/tijl/tijl.dev-core/modules/i18n"
log "git.tijl.dev/tijl/tijl.dev-core/modules/logger"
"git.tijl.dev/tijl/tijl.dev-core/modules/web"
"github.com/enescakir/emoji"
"github.com/gofiber/fiber/v2"
@ -29,7 +29,7 @@ func answerHandler(c *fiber.Ctx) error {
answer := c.FormValue("answer")
gameSession, err := db.Queries.GetFlagsGame(context.TODO(), gameId)
gameSession, err := db.Queries.AppFlagsGetGame(context.TODO(), gameId)
if err != nil {
return err
}
@ -39,29 +39,29 @@ func answerHandler(c *fiber.Ctx) error {
return err
}
shuffledCountries := shuffleSlice(countries, gameSession.GameSeed.UUID.String())
shuffledCountries := shuffleSlice(countries, gameSession.GameSeed.String())
correctAnswer := shuffledCountries[gameSession.QuestionCurrent-1]
if answer == correctAnswer {
db.Queries.UpdateFlagsGame(context.TODO(), queries.UpdateFlagsGameParams{
db.Queries.AppFlagsUpdateGame(context.TODO(), queries.AppFlagsUpdateGameParams{
GameID: gameId,
QuestionCurrent: gameSession.QuestionCurrent + 1,
})
db.Queries.UpsertGameAnswer(context.TODO(), queries.UpsertGameAnswerParams{
db.Queries.AppFlagsUpsertGameAnswer(context.TODO(), queries.AppFlagsUpsertGameAnswerParams{
GameID: gameId,
Question: gameSession.QuestionCurrent,
Errors: 0,
})
} else {
db.Queries.UpsertGameAnswer(context.TODO(), queries.UpsertGameAnswerParams{
db.Queries.AppFlagsUpsertGameAnswer(context.TODO(), queries.AppFlagsUpsertGameAnswerParams{
GameID: gameId,
Question: gameSession.QuestionCurrent,
Errors: 1,
})
}
err = db.Queries.UpdateQuestionCorrect(context.TODO(), gameId)
err = db.Queries.AppFlagsUpdateQuestionCorrect(context.TODO(), gameId)
if err != nil {
return err
}
@ -87,7 +87,7 @@ func questionHandler(c *fiber.Ctx, newGame NewGameUUID) error {
}
}
gameSession, err := db.Queries.GetFlagsGame(context.TODO(), gameId)
gameSession, err := db.Queries.AppFlagsGetGame(context.TODO(), gameId)
if err != nil {
return err
}
@ -111,17 +111,20 @@ func questionHandler(c *fiber.Ctx, newGame NewGameUUID) error {
return gameEndHandler(c)
}
shuffledCountries := shuffleSlice(countries, gameSession.GameSeed.UUID.String())
shuffledAnswers := shuffleSlice(countries, gameSession.GameSeed.UUID.String()+string(gameSession.QuestionCurrent))
shuffledCountries := shuffleSlice(countries, gameSession.GameSeed.String())
shuffledAnswers := shuffleSlice(countries, gameSession.GameSeed.String()+string(gameSession.QuestionCurrent))
shuffledAnswers = shuffledAnswers[0:4] // 4 random aswers
shuffledAnswers = append(shuffledAnswers, shuffledCountries[gameSession.QuestionCurrent-1]) // add correct answer
shuffledAnswers = shuffleSlice(shuffledAnswers, gameSession.GameSeed.UUID.String()+string(gameSession.QuestionCurrent)) // shuffle again
shuffledAnswers = shuffleSlice(shuffledAnswers, gameSession.GameSeed.String()+string(gameSession.QuestionCurrent)) // shuffle again
if gameSession.QuestionAmount != 0 && int(gameSession.QuestionAmount) < len(countries) {
shuffledCountries = shuffledCountries[0:gameSession.QuestionAmount]
}
log.Debug().Interface("shuffledCountries", shuffledCountries).Interface("shuffledAnswers", shuffledAnswers).Msg("data")
var timeleft = []string{}
if gameSession.Seconds != 0 {
timeleft = append(timeleft, strconv.Itoa(int(gameSession.Seconds)-int(time.Since(gameSession.CreatedAt).Seconds())))
}
flag, err := emoji.CountryFlag(shuffledCountries[gameSession.QuestionCurrent-1])
if err != nil {
@ -130,13 +133,23 @@ func questionHandler(c *fiber.Ctx, newGame NewGameUUID) error {
data := *web.Common(c)
data["Title"] = "tmp"
data["TimeLeft"] = timeleft
data["QuestionsLeft"] = len(shuffledCountries) - int(gameSession.QuestionCurrent) + 1
data["Answers"] = shuffledAnswers
data["Flag"] = flag
data["Errors"] = gameSession.QuestionsErrors
return c.Render("apps/flags/question", data, "layouts/base")
}
func getGameData() {
func sharedGameHandler(c *fiber.Ctx) error {
shareKey := c.FormValue("sharekey")
data, err := db.Queries.AppFlagsGetSharedData(context.TODO(), shareKey)
if err != nil {
return err
}
return setupGame(c, data.Tags, int(data.Questions), int(data.Seconds), true, data.GameSeed)
}
func startNewGameHandler(c *fiber.Ctx) error {
@ -164,6 +177,37 @@ func startNewGameHandler(c *fiber.Ctx) error {
return err
}
seconds, err := strconv.Atoi(c.FormValue("seconds"))
if err != nil {
return err
}
if c.FormValue("share") != "" {
return createSharedGameData(c, selectedTags, maxQuestions, seconds)
}
return setupGame(c, selectedTags, maxQuestions, seconds, false, uuid.UUID{})
}
func createSharedGameData(c *fiber.Ctx, tags []string, maxQuestions int, seconds int) error {
shareKey := utils.RandString(4)
_, err := db.Queries.AppFlagsNewSharedData(context.TODO(), queries.AppFlagsNewSharedDataParams{
ShareKey: shareKey,
Tags: tags,
Questions: int32(maxQuestions),
Seconds: int32(seconds),
})
if err != nil {
return err
}
data := *web.Common(c)
data["Title"] = "tmp"
data["ShareKey"] = shareKey
return c.Render("apps/flags/shared", data, "layouts/base")
}
func setupGame(c *fiber.Ctx, tags []string, maxQuestions int, seconds int, useGameSeed bool, gameSeed uuid.UUID) error {
var Quid = sql.NullString{}
uid, err := user.GetSession(c)
if err == nil {
@ -171,22 +215,30 @@ func startNewGameHandler(c *fiber.Ctx) error {
Quid.String = uid
}
row, err := db.Queries.CreateFlagsGame(context.TODO(), queries.CreateFlagsGameParams{
createGameParams := queries.AppFlagsCreateGameWithSeedParams{
Uid: Quid,
Tags: selectedTags,
Tags: tags,
QuestionAmount: int32(maxQuestions),
})
Seconds: int32(seconds),
GameSeed: uuid.New(),
}
if useGameSeed {
createGameParams.GameSeed = gameSeed
}
gameID, err := db.Queries.AppFlagsCreateGameWithSeed(context.TODO(), createGameParams)
if err != nil {
return err
}
c.Cookie(&fiber.Cookie{
Name: flagSessionCookie,
Value: row.GameID.String(),
Value: gameID.String(),
//Secure: true,
})
return questionHandler(c, NewGameUUID{used: true, UUID: row.GameID})
return questionHandler(c, NewGameUUID{used: true, UUID: gameID})
}
func gameEndHandler(c *fiber.Ctx) error {
@ -194,7 +246,7 @@ func gameEndHandler(c *fiber.Ctx) error {
if err != nil {
return err
}
gameSession, err := db.Queries.GetFlagsGame(context.TODO(), gameId)
gameSession, err := db.Queries.AppFlagsGetGame(context.TODO(), gameId)
if err != nil {
return err
}

View File

@ -51,6 +51,8 @@ func Setup() {
return answerHandler(c)
case "exit":
return stopGameHandler(c)
case "shared":
return sharedGameHandler(c)
}
return nil

View File

@ -1,28 +1,39 @@
-- name: CreateFlagsGame :one
INSERT INTO app_flags_games (uid, tags, question_amount)
VALUES ($1, $2, $3)
-- name: AppFlagsCreateGame :one
INSERT INTO app_flags_games (uid, tags, question_amount, seconds)
VALUES ($1, $2, $3, $4)
RETURNING game_id, game_seed;
-- name: GetFlagsGame :one
-- name: AppFlagsCreateGameWithSeed :one
INSERT INTO app_flags_games (uid, tags, question_amount, seconds, game_seed)
VALUES ($1, $2, $3, $4, $5)
RETURNING game_id;
-- name: AppFlagsGetGame :one
SELECT * FROM app_flags_games WHERE game_id = $1 LIMIT 1;
-- name: UpdateFlagsGame :exec
-- name: AppFlagsUpdateGame :exec
UPDATE app_flags_games
SET question_current = $1, last_activity = CURRENT_TIMESTAMP
WHERE game_id = $2;
-- name: UpdateQuestionCorrect :exec
-- name: AppFlagsUpdateQuestionCorrect :exec
UPDATE app_flags_games
SET questions_errors = (
SELECT COALESCE(SUM(errors), 0)
FROM app_flags_games_answers
WHERE app_flags_games_answers.game_id = app_flags_games.game_id
)
), last_activity = CURRENT_TIMESTAMP
WHERE app_flags_games.game_id = $1;
-- name: UpsertGameAnswer :exec
-- name: AppFlagsUpsertGameAnswer :exec
INSERT INTO app_flags_games_answers (game_id, question, errors)
VALUES ($1, $2, $3)
ON CONFLICT (game_id, question)
DO UPDATE SET errors = app_flags_games_answers.errors + EXCLUDED.errors;
-- name: AppFlagsNewSharedData :one
INSERT INTO app_flags_games_shared_data (share_key, game_seed, tags, questions, seconds) VALUES ($1, $2, $3, $4, $5) RETURNING game_seed;
-- name: AppFlagsGetSharedData :one
SELECT * FROM app_flags_games_shared_data WHERE share_key = $1 LIMIT 1;

View File

@ -13,42 +13,76 @@ import (
"github.com/lib/pq"
)
const createFlagsGame = `-- name: CreateFlagsGame :one
INSERT INTO app_flags_games (uid, tags, question_amount)
VALUES ($1, $2, $3)
const appFlagsCreateGame = `-- name: AppFlagsCreateGame :one
INSERT INTO app_flags_games (uid, tags, question_amount, seconds)
VALUES ($1, $2, $3, $4)
RETURNING game_id, game_seed
`
type CreateFlagsGameParams struct {
type AppFlagsCreateGameParams struct {
Uid sql.NullString
Tags []string
QuestionAmount int32
Seconds int32
}
type CreateFlagsGameRow struct {
type AppFlagsCreateGameRow struct {
GameID uuid.UUID
GameSeed uuid.NullUUID
GameSeed uuid.UUID
}
func (q *Queries) CreateFlagsGame(ctx context.Context, arg CreateFlagsGameParams) (CreateFlagsGameRow, error) {
row := q.db.QueryRowContext(ctx, createFlagsGame, arg.Uid, pq.Array(arg.Tags), arg.QuestionAmount)
var i CreateFlagsGameRow
func (q *Queries) AppFlagsCreateGame(ctx context.Context, arg AppFlagsCreateGameParams) (AppFlagsCreateGameRow, error) {
row := q.db.QueryRowContext(ctx, appFlagsCreateGame,
arg.Uid,
pq.Array(arg.Tags),
arg.QuestionAmount,
arg.Seconds,
)
var i AppFlagsCreateGameRow
err := row.Scan(&i.GameID, &i.GameSeed)
return i, err
}
const getFlagsGame = `-- name: GetFlagsGame :one
SELECT game_id, game_seed, uid, tags, question_amount, question_current, questions_errors, created_at, last_activity FROM app_flags_games WHERE game_id = $1 LIMIT 1
const appFlagsCreateGameWithSeed = `-- name: AppFlagsCreateGameWithSeed :one
INSERT INTO app_flags_games (uid, tags, question_amount, seconds, game_seed)
VALUES ($1, $2, $3, $4, $5)
RETURNING game_id
`
func (q *Queries) GetFlagsGame(ctx context.Context, gameID uuid.UUID) (AppFlagsGame, error) {
row := q.db.QueryRowContext(ctx, getFlagsGame, gameID)
type AppFlagsCreateGameWithSeedParams struct {
Uid sql.NullString
Tags []string
QuestionAmount int32
Seconds int32
GameSeed uuid.UUID
}
func (q *Queries) AppFlagsCreateGameWithSeed(ctx context.Context, arg AppFlagsCreateGameWithSeedParams) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, appFlagsCreateGameWithSeed,
arg.Uid,
pq.Array(arg.Tags),
arg.QuestionAmount,
arg.Seconds,
arg.GameSeed,
)
var game_id uuid.UUID
err := row.Scan(&game_id)
return game_id, err
}
const appFlagsGetGame = `-- name: AppFlagsGetGame :one
SELECT game_id, game_seed, uid, tags, seconds, question_amount, question_current, questions_errors, created_at, last_activity FROM app_flags_games WHERE game_id = $1 LIMIT 1
`
func (q *Queries) AppFlagsGetGame(ctx context.Context, gameID uuid.UUID) (AppFlagsGame, error) {
row := q.db.QueryRowContext(ctx, appFlagsGetGame, gameID)
var i AppFlagsGame
err := row.Scan(
&i.GameID,
&i.GameSeed,
&i.Uid,
pq.Array(&i.Tags),
&i.Seconds,
&i.QuestionAmount,
&i.QuestionCurrent,
&i.QuestionsErrors,
@ -58,51 +92,95 @@ func (q *Queries) GetFlagsGame(ctx context.Context, gameID uuid.UUID) (AppFlagsG
return i, err
}
const updateFlagsGame = `-- name: UpdateFlagsGame :exec
const appFlagsGetSharedData = `-- name: AppFlagsGetSharedData :one
SELECT id, share_key, game_seed, questions, tags, seconds, created_at FROM app_flags_games_shared_data WHERE share_key = $1 LIMIT 1
`
func (q *Queries) AppFlagsGetSharedData(ctx context.Context, shareKey string) (AppFlagsGamesSharedDatum, error) {
row := q.db.QueryRowContext(ctx, appFlagsGetSharedData, shareKey)
var i AppFlagsGamesSharedDatum
err := row.Scan(
&i.ID,
&i.ShareKey,
&i.GameSeed,
&i.Questions,
pq.Array(&i.Tags),
&i.Seconds,
&i.CreatedAt,
)
return i, err
}
const appFlagsNewSharedData = `-- name: AppFlagsNewSharedData :one
INSERT INTO app_flags_games_shared_data (share_key, game_seed, tags, questions, seconds) VALUES ($1, $2, $3, $4, $5) RETURNING game_seed
`
type AppFlagsNewSharedDataParams struct {
ShareKey string
GameSeed uuid.UUID
Tags []string
Questions int32
Seconds int32
}
func (q *Queries) AppFlagsNewSharedData(ctx context.Context, arg AppFlagsNewSharedDataParams) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, appFlagsNewSharedData,
arg.ShareKey,
arg.GameSeed,
pq.Array(arg.Tags),
arg.Questions,
arg.Seconds,
)
var game_seed uuid.UUID
err := row.Scan(&game_seed)
return game_seed, err
}
const appFlagsUpdateGame = `-- name: AppFlagsUpdateGame :exec
UPDATE app_flags_games
SET question_current = $1, last_activity = CURRENT_TIMESTAMP
WHERE game_id = $2
`
type UpdateFlagsGameParams struct {
type AppFlagsUpdateGameParams struct {
QuestionCurrent int32
GameID uuid.UUID
}
func (q *Queries) UpdateFlagsGame(ctx context.Context, arg UpdateFlagsGameParams) error {
_, err := q.db.ExecContext(ctx, updateFlagsGame, arg.QuestionCurrent, arg.GameID)
func (q *Queries) AppFlagsUpdateGame(ctx context.Context, arg AppFlagsUpdateGameParams) error {
_, err := q.db.ExecContext(ctx, appFlagsUpdateGame, arg.QuestionCurrent, arg.GameID)
return err
}
const updateQuestionCorrect = `-- name: UpdateQuestionCorrect :exec
const appFlagsUpdateQuestionCorrect = `-- name: AppFlagsUpdateQuestionCorrect :exec
UPDATE app_flags_games
SET questions_errors = (
SELECT COALESCE(SUM(errors), 0)
FROM app_flags_games_answers
WHERE app_flags_games_answers.game_id = app_flags_games.game_id
)
), last_activity = CURRENT_TIMESTAMP
WHERE app_flags_games.game_id = $1
`
func (q *Queries) UpdateQuestionCorrect(ctx context.Context, gameID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, updateQuestionCorrect, gameID)
func (q *Queries) AppFlagsUpdateQuestionCorrect(ctx context.Context, gameID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, appFlagsUpdateQuestionCorrect, gameID)
return err
}
const upsertGameAnswer = `-- name: UpsertGameAnswer :exec
const appFlagsUpsertGameAnswer = `-- name: AppFlagsUpsertGameAnswer :exec
INSERT INTO app_flags_games_answers (game_id, question, errors)
VALUES ($1, $2, $3)
ON CONFLICT (game_id, question)
DO UPDATE SET errors = app_flags_games_answers.errors + EXCLUDED.errors
`
type UpsertGameAnswerParams struct {
type AppFlagsUpsertGameAnswerParams struct {
GameID uuid.UUID
Question int32
Errors int32
}
func (q *Queries) UpsertGameAnswer(ctx context.Context, arg UpsertGameAnswerParams) error {
_, err := q.db.ExecContext(ctx, upsertGameAnswer, arg.GameID, arg.Question, arg.Errors)
func (q *Queries) AppFlagsUpsertGameAnswer(ctx context.Context, arg AppFlagsUpsertGameAnswerParams) error {
_, err := q.db.ExecContext(ctx, appFlagsUpsertGameAnswer, arg.GameID, arg.Question, arg.Errors)
return err
}

View File

@ -13,9 +13,10 @@ import (
type AppFlagsGame struct {
GameID uuid.UUID
GameSeed uuid.NullUUID
GameSeed uuid.UUID
Uid sql.NullString
Tags []string
Seconds int32
QuestionAmount int32
QuestionCurrent int32
QuestionsErrors int32
@ -27,6 +28,17 @@ type AppFlagsGamesAnswer struct {
GameID uuid.UUID
Question int32
Errors int32
CreatedAt time.Time
}
type AppFlagsGamesSharedDatum struct {
ID int32
ShareKey string
GameSeed uuid.UUID
Questions int32
Tags []string
Seconds int32
CreatedAt time.Time
}
type Session struct {

View File

@ -1,8 +1,9 @@
CREATE TABLE app_flags_games (
game_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
game_seed UUID DEFAULT gen_random_uuid(),
game_id UUID PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
game_seed UUID NOT NULL DEFAULT gen_random_uuid(),
uid VARCHAR DEFAULT NULL,
tags VARCHAR[] NOT NULL,
seconds INT NOT NULL,
question_amount INT NOT NULL,
question_current INT DEFAULT 1 NOT NULL,
questions_errors INT DEFAULT 0 NOT NULL,
@ -11,10 +12,21 @@ CREATE TABLE app_flags_games (
FOREIGN KEY (uid) REFERENCES users (uid)
);
CREATE TABLE app_flags_games_shared_data (
id SERIAL PRIMARY KEY,
share_key VARCHAR NOT NULL,
game_seed UUID NOT NULL DEFAULT gen_random_uuid(),
questions INT DEFAULT 0 NOT NULL,
tags VARCHAR[] NOT NULL,
seconds INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE TABLE app_flags_games_answers (
game_id UUID NOT NULL,
question INT NOT NULL,
errors INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (game_id) REFERENCES app_flags_games (game_id),
CONSTRAINT app_flags_games_answers_unique UNIQUE (game_id, question)
);

View File

@ -2,9 +2,17 @@
<div>
<div>
Questions left: {{.QuestionsLeft}}
</div>
<div>
Errors: {{.Errors}}
</div>
{{range .TimeLeft}}
<div>
Seconds left: <span id="countdown">{{.}}</span>
</div>
{{end}}
<div class="flex justify-center p-4 mt-4">
<span style="font-size: 105px;">{{.Flag}}</span>
@ -28,3 +36,18 @@
</div>
</div>
{{range .TimeLeft}}
<script>
function startCountdown() {
const countdownElement = document.getElementById('countdown');
let countdownNumber = parseInt(countdownElement.innerHTML, 10);
const intervalId = setInterval(() => {
countdownNumber -= 1;
countdownElement.innerHTML = countdownNumber;
}, 1000);
}
startCountdown();
</script>
{{end}}

View File

@ -0,0 +1 @@
<div>{{.ShareKey}}</div>

View File

@ -14,12 +14,32 @@
{{end}}
</div>
<!--<label class="cursor-pointer label">
<span class="label-text">Aantal vragen</span>
<input name="tags" value="{{.}}" type="checkbox" class="checkbox checkbox-primary" />
</label>-->
<label class="cursor-pointer label mt-5">
<span class="label-text">Share</span>
<input name="share" type="checkbox" class="checkbox checkbox-primary" />
</label>
<div>
<label>
<span>max questions</span>
<input value="0" placeholder="max questions" name="max_questions" type="number"
class="input input-bordered" />
</label>
</div>
<div>
<label>
<span>time limit</span>
<input value="0" placeholder="seconds time limit" name="seconds" type="number"
class="input input-bordered" />
</label>
</div>
<input value="0" placeholder="max questions" name="max_questions" type="number" class="input input-bordered" />
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<form class="mt-4" method="post">
<input type="text" class="hidden" name="type" value="shared" />
<input type="text" class="input input-bordered" name="sharekey" placeholder="sharekey" />
<button type="submit" class="btn btn-primary">Shared</button>
</form>
</div>