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