Glucose Grand Prix Mini Game
Dodge obstacles and answer diabetes trivia to win
Overview
In this game, users can test their knowledge from the past flashcards game while dodging diabetes-related obstacles. The user plays the game and is periodically interrupted by trivia popups. Answer incorrectly and lose a life!
User Story
The user wants to know if they properly learned the diabetes trivia in a fun and engaging way. This game, like other review sites, helps the user quickly check their knowledge in a unique fashion.
Features
Frontend
The game includes a car sprite, parallax road, animation, and obstacles that move towards the player. The player has 3 lives and must dodge obstacles and answer questions to keep playing and gain points the longer they last. After the user loses, they can store their score in a leaderboard.
HTML
%%html
<div id="canvasContainer">
<div id="startButtonContainer" class="center-overlay">
<button id="startButton">Start Game</button>
</div>
<button id="pauseButton" aria-label="Pause/Play">
<svg id="pauseIcon" width="32" height="32" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="4" width="4" height="16" />
<rect x="14" y="4" width="4" height="16" />
</svg>
</button>
<canvas id="gameCanvas" width="360" height="639"></canvas>
<div id="leaderboardContainer">
<h2>Leaderboard</h2>
<table id="leaderboard">
<thead>
<tr>
<th style="padding: 0.5rem; border-bottom: 1px solid #ccc;">Rank</th>
<th style="padding: 0.5rem; border-bottom: 1px solid #ccc;">Name</th>
<th style="padding: 0.5rem; border-bottom: 1px solid #ccc;">Score</th>
<th style="padding: 0.5rem; border-bottom: 1px solid #ccc;">Date</th>
</tr>
</thead>
<tbody id="leaderboardBody">
<!-- Entries here -->
</tbody>
</table>
</div>
<div id="nameInputContainer">
<input id="playerName" type="text" placeholder="Your Name" maxlength="64"/>
<button id="submitScore">Submit</button>
</div>
</div>
<div id="triviaModal" class="popup-overlay" style="display: none;">
<div class="popup-content">
<p id="triviaQuestion"></p>
<div id="triviaOptions" style="margin-top: 1rem;"></div>
<button id="close-popup" style="display: none;">OK</button>
</div>
</div>
CSSJS
%%html
<script>
import { pythonURI, fetchOptions } from '/glucoquest_frontend/assets/js/api/config.js';
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const startButton = document.getElementById("startButton");
const pauseButton = document.getElementById("pauseButton");
const assets = {
background: {
src: "/glucoquest_frontend/images/grandprix/road.jpg",
},
obstacles: {
blood: {
src: "/glucoquest_frontend/images/grandprix/blood.png",
},
/*etc*/
},
cars: {
default: {
src: "/glucoquest_frontend/images/grandprix/default.png",
width: 256,
height: 256
}
}
};
// Game state
let bgImg, carImg;
const carScale = 0.4;
const carWidth = assets.cars.default.width * carScale;
const carHeight = assets.cars.default.height * carScale;
let carX, carY;
let obstacles = [];
const obstacleWidth = 40;
const obstacleHeight = 40;
let obstacleSpawnThreshold = 200;
let distanceSinceLastObstacle = 0;
let obstacleImages = {};
const carSpeed = 5;
let backgroundY;
const backgroundSpeed = 2;
let keys = { a: false, d: false };
let isRunning = false;
let isPaused = false;
/*etc*/
class Obstacle {
constructor(x, y, image) {
this.x = x;
this.y = y;
this.image = image;
this.width = obstacleWidth;
this.height = obstacleHeight;
this.hasCollided = false;
}
update() {
this.y += backgroundSpeed;
}
draw(ctx) {
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
}
}
async function initGame() {
try {
bgImg = await loadImage(assets.background.src);
carImg = await loadImage(assets.cars.default.src);
const obstacleNames = Object.keys(assets.obstacles);
for (const name of obstacleNames) {
obstacleImages[name] = await loadImage(assets.obstacles[name].src);
}
setInterval(() => {
if (isRunning && !isPaused && !showingTrivia && !isGameOver) {
showTrivia();
}
}, 10000);
setInterval(() => {
if (isRunning && !isPaused && !isGameOver && !showingTrivia) {
points += 5;
}
}, 1000);
resetGameState();
drawStaticScene();
} catch (e) {
console.error("Image loading error:", e);
}
}
/*etc*/
</script>
Backend
I have created both a model and an API for my feature and stored the different questions there. The questions are paired to their answers so that they are displayed correctly. I have a separate model for the questions and the answers and an API that fetches both at once.
I also have a leaderboard API that creates a leaderboard of the top scores.
Model
def initQuestions():
question_data = [
{
"question": "What does insulin do?",
"correct_answer": "b"
},
{
"question": "What is diabetes?",
"correct_answer": "b"
},
{
"question": "How does healthy eating help with diabetes?",
"correct_answer": "c"
},
{
"question": "What does exercise do for blood sugar?",
"correct_answer": "b"
}
]
"""""etc"""
for question in question_data:
if not Trivia.query.filter_by(question=question["question"]).first(): # check if question already exists aviods duplicates
new_question = Trivia(
question=question["question"],
correct_answer=question["correct_answer"],
)
db.session.add(new_question)
"""Answers model"""
def initAnswers():
answer_data = [
{
"answer_id": "a",
"answer": "Makes your bones stronger",
"trivia_id": 1
},
{
"answer_id": "b",
"answer": "Helps sugar get into cells for energy",
"trivia_id": 1
},
{
"answer_id": "c",
"answer": "Helps you sleep better",
"trivia_id": 1
},
{
"answer_id": "d",
"answer": "Breaks down fat in your body",
"trivia_id": 1
}
]
API
Leaderboard
@racing_api.route('', methods=['POST'])
def add_leaderboard_entry():
data = request.get_json()
name = data.get('name', 'Anonymous').strip()[:64]
score = int(data.get('score', 0))
date = data.get('date') or datetime.now(timezone('US/Pacific')).strftime('%Y-%m-%d')
entry = RacingLeaderboard(name=name, score=score, date=date)
db.session.add(entry)
db.session.commit()
return jsonify(entry.to_dict()), 201
Trivia
@trivia_api.route('/<int:question_id>', methods=['GET'])
def get_trivia(question_id):
try:
# Fetch the question
question = Trivia.query.get(question_id)
if not question:
return jsonify({'error': 'Question not found'}), 404
# Fetch all corresponding answers
answers = Answers.query.filter_by(trivia_id=question_id).all()
# Combine and return
return jsonify({
'id': question.id,
'question': question.question,
'correct_answer': question.correct_answer,
'answers': [
{
'answer_id': ans.answer_id,
'answer': ans.answer
} for ans in answers
]
}), 200
except Exception as e:
return jsonify({'error': 'Failed to fetch trivia with answers', 'message': str(e)}), 500