# Team Quiz

# **📋 Complete Build Prompt**

<span class="s2">**Project name:**</span> Team Bug-Fix Race (ROC Amstelland Edition)

<span class="s2">**Stack:**</span> PHP 8.1, MariaDB, Apache, Vanilla JS, Tailwind CSS.

<span class="s2">**Branding:**</span> ROC Amstelland – red as accent (#e11d48 or Tailwind <span class="s3">rose-600</span>/<span class="s3">red-600</span>). UI language: <span class="s2">**English**</span>.

## **1) One‑paragraph summary for the builder**

Build a lightweight, real‑time(ish) classroom web app where student <span class="s2">**teams**</span> submit a single open‑text answer per question. A <span class="s2">**teacher**</span> controls the match, creates rounds from a pre‑entered question list, and <span class="s2">**judges**</span> answers manually. Scoring is <span class="s2">**6/3/1**</span> for the first/second/third *accepted* answers per question based on the server timestamp of the submissions. A team may submit <span class="s2">**once per question**</span>. The teacher starts/stops each question without a time limit. Live views show the current question, incoming answers for the teacher, and a public <span class="s2">**leaderboard**</span>. Authentication is minimal: teacher login + <span class="s2">**team join by 5‑char code**</span>.

## **2) User roles &amp; pages**

### **Teacher**

- <span class="s1">**Login page**</span> (email + password)
- - Create/manage <span class="s1">**Matches**</span> (a match = N questions selected from the bank)
    - <span class="s1">Start/stop </span>**Current Question**
    - <span class="s1">**Answers Moderation**</span>: live list of all team submissions (sorted by server time); buttons: *Accept* / *Reject*
    - <span class="s1">**Scoreboard view**</span> (open in beamer window)
    - Question Bank CRUD (manual input via form; title, description, code snippet, tags)
        
        **Dashboard**

### **Student (Team)**

- <span class="s1">**Join page**</span>: enter *Team name* and <span class="s1">**join code**</span> (5 alphanumeric chars). No password. Code generated by teacher when creating teams.
- <span class="s1">**Play page**</span>: shows current question (read‑only), text field to submit one answer (disabled after submit or when question is closed). Shows submission status.

### **Public/Share**

- <span class="s1">**Leaderboard page**</span>: responsive board with team rank, score, last question deltas.

## **3) Gameplay rules (authoritative spec)**

- Teacher creates a <span class="s1">**Match**</span>, adds/arranges N questions (from bank) → starts the match.
- - Teacher clicks <span class="s1">**Open Question**</span> → teams can submit once.
    - Each submission receives a <span class="s1">**server-side timestamp**</span> (UTC) that decides order.
    - Teacher views all answers (team, text, timestamp) and marks them <span class="s1">**Accepted**</span> or <span class="s1">**Rejected**</span>.
    - The first 3 <span class="s1">**Accepted**</span> answers award <span class="s1">**6, 3, 1**</span> points respectively. If fewer than 3 accepted, only those points apply.
    - If <span class="s1">**no accepted answers**</span>, everyone gets <span class="s1">0</span>.
    - Teacher clicks <span class="s1">**Close Question**</span> → submissions disabled → scores lock.
        
        For each question:
- Proceed to next question until all are done.

Constraints:

- <span class="s1">Each team: </span>**max 1 submission per question**<span class="s1">.</span>
- Order is determined solely by <span class="s1">**server timestamp**</span> when submission is stored.
- Teacher can <span class="s1">**revoke**</span> an accept/reject before the question is closed; if acceptance order changes, points recompute.

## **4) Real‑time options (choose one at build time)**

1. <span class="s1">**Short‑polling**</span> (default MVP): client polls JSON endpoints every 1–2s. Easiest to host on Apache/PHP.
2. <span class="s1">**SSE (Server‑Sent Events)**</span>: simple one‑way push from server to clients for live updates (scoreboard/answers).
3. <span class="s1">**WebSocket (Ratchet/Swoole)**</span>: lowest latency, most complex. Optional v2.

> <span class="s2">**Implement MVP with short‑polling**</span>, keep code structured so SSE can be swapped in later.

## **5) Data model (MariaDB)**

```
-- users (teachers only)
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(190) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- question bank
CREATE TABLE questions (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  body TEXT NOT NULL,             -- markdown/text
  code_snippet TEXT NULL,          -- optional fenced code
  tags VARCHAR(255) NULL,
  created_by INT NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (created_by) REFERENCES users(id)
) ENGINE=InnoDB;

-- matches (a session for a class)
CREATE TABLE matches (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  status ENUM('draft','active','finished') NOT NULL DEFAULT 'draft',
  created_by INT NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (created_by) REFERENCES users(id)
) ENGINE=InnoDB;

-- match_questions (ordered list)
CREATE TABLE match_questions (
  id INT AUTO_INCREMENT PRIMARY KEY,
  match_id INT NOT NULL,
  question_id INT NOT NULL,
  position INT NOT NULL,                      -- 1..N order
  status ENUM('pending','open','closed') NOT NULL DEFAULT 'pending',
  opened_at DATETIME NULL,
  closed_at DATETIME NULL,
  FOREIGN KEY (match_id) REFERENCES matches(id),
  FOREIGN KEY (question_id) REFERENCES questions(id),
  UNIQUE KEY uq_match_pos (match_id, position)
) ENGINE=InnoDB;

-- teams
CREATE TABLE teams (
  id INT AUTO_INCREMENT PRIMARY KEY,
  match_id INT NOT NULL,
  name VARCHAR(120) NOT NULL,
  join_code CHAR(5) NOT NULL,                 -- generated before start
  total_score INT NOT NULL DEFAULT 0,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uq_match_team_name (match_id, name),
  UNIQUE KEY uq_join_code (join_code),
  FOREIGN KEY (match_id) REFERENCES matches(id)
) ENGINE=InnoDB;

-- submissions (one per team per question)
CREATE TABLE submissions (
  id INT AUTO_INCREMENT PRIMARY KEY,
  match_question_id INT NOT NULL,
  team_id INT NOT NULL,
  answer_text TEXT NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  verdict ENUM('pending','accepted','rejected') NOT NULL DEFAULT 'pending',
  verdict_by INT NULL,                         -- teacher user id
  verdict_at DATETIME NULL,
  FOREIGN KEY (match_question_id) REFERENCES match_questions(id),
  FOREIGN KEY (team_id) REFERENCES teams(id),
  FOREIGN KEY (verdict_by) REFERENCES users(id),
  UNIQUE KEY uq_team_once (match_question_id, team_id),
  KEY idx_mq_created (match_question_id, created_at)
) ENGINE=InnoDB;

-- per-question score ledger (derived but stored for performance)
CREATE TABLE scores (
  id INT AUTO_INCREMENT PRIMARY KEY,
  match_question_id INT NOT NULL,
  team_id INT NOT NULL,
  points INT NOT NULL,
  rank INT NOT NULL,                           -- 1,2,3 for accepted order
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (match_question_id) REFERENCES match_questions(id),
  FOREIGN KEY (team_id) REFERENCES teams(id),
  UNIQUE KEY uq_score_once (match_question_id, team_id)
) ENGINE=InnoDB;
```

## **6) Scoring logic (deterministic)**

- Compute accepted submissions for the current question ordered by <span class="s1">created\_at ASC</span>.
- Award points by order: index 1 → 6 pts, 2 → 3 pts, 3 → 1 pt. Others: 0.
- Recompute ledger whenever teacher changes a verdict <span class="s1">**until**</span> the question is closed. On close, persist <span class="s2">scores</span> rows and update <span class="s2">teams.total\_score</span>.
- If a previously accepted answer is revoked while <span class="s1">**open**</span>, recalc order and points.

Pseudocode:

```
$accepted = SELECT * FROM submissions
  WHERE match_question_id=? AND verdict='accepted'
  ORDER BY created_at ASC;
$pointsMap = [6,3,1];
for each team in accepted (i=0..2) assign $pointsMap[$i]; others 0.
```

## **7) HTTP routes (MVP)**

### **Auth**

- <span class="s1">POST /auth/login</span> (teacher) → session
- POST /auth/logout

### **Teacher – Matches &amp; Questions**

- GET /dashboard<span class="s1"> (HTML)</span>
- POST /matches<span class="s1"> create</span>
- POST /matches/:id/activate<span class="s1"> → status=active</span>
- POST /matches/:id/finish<span class="s1"> → status=finished</span>
- GET /matches/:id<span class="s1"> (manage view)</span>
- <span class="s1">POST /matches/:id/teams</span> (bulk create with names; server generates join codes)
- <span class="s1">POST /matches/:id/questions</span> (attach question\_id + position)
- <span class="s1">POST /match\_questions/:id/open</span> → set status=open, opened\_at=NOW(), clear pending scores
- <span class="s1">POST /match\_questions/:id/close</span> → status=closed, persist scores → update teams.total\_score

### **Teacher – Moderation &amp; Live**

- GET /api/match\_questions/:id/submissions<span class="s1"> (JSON, order by created\_at)</span>
- POST /api/submissions/:id/verdict<span class="s1"> {accepted|rejected}</span>
- GET /api/matches/:id/leaderboard<span class="s1"> (JSON)</span>

### **Question Bank**

- GET /questions<span class="s1"> (HTML list)</span>
- GET /questions/new<span class="s1">, </span>POST /questions
- GET /questions/:id/edit<span class="s1">, </span>POST /questions/:id

### **Student – Join &amp; Play**

- GET /join<span class="s1"> (HTML)</span>
- <span class="s1">POST /join</span> (join\_code + team\_name optional lock)
- GET /play<span class="s1"> (HTML)</span>
- <span class="s1">GET /api/current</span> (JSON: current match\_question, sanitized question fields, status)
- <span class="s1">POST /api/submit</span> (team session must exist; if already submitted for this question → 409)
- <span class="s1">GET /api/my-status</span> (has\_submitted, verdict if any)

### **Public Leaderboard**

- GET /leaderboard/:matchId<span class="s1"> (HTML)</span>
- `GET /api/leaderboard

Team