PlayerPuzzle

Turn-based Match-3 RPG gamified activity for Moodle — full documentation.

English | Português

View on GitHub Download .zip

Moodle License Status Latest Release PlayerGames Ecosystem Author

Moodle Plugin CI Last Commit Open Issues

⚠️ This plugin is under active development. It is not yet published on the Moodle Plugin Directory. Some features described below are planned and not yet implemented — see Features for exactly which is which.

PlayerPuzzle (mod_playerpuzzle) is a turn-based Match-3 RPG gamified activity for Moodle: the student combines pieces on an 8×8 board to attack a boss controlled by a simple AI, answering questions pulled from the course’s own Moodle Question Bank to land critical hits. The teacher chooses between a self-contained Single Match or a Campaign of up to 10 levels (10 phases each); optional permanent progression (coins, sword/shield level) is handled entirely by the companion block_playerhud when it’s present in the course — PlayerPuzzle keeps no economy of its own.

👈 Use the sidebar to jump to any section on this page.


🖼️ Screenshots

Browse with the arrows, the dots, or the left/right keys. Click the image to zoom.

✨ Features

✅ Implemented

⏳ In Development / Planned

The plugin is Alpha-stage software: everything in "Implemented" above works today and is covered by the automated test suite; everything in "Planned" is designed (see the project's internal roadmap) but not yet built.

⚔️ How Combat Works

Combat runs on an 8×8 Match-3 board (Phaser 3), portrait-first (540×960), adapting to a landscape layout on wider screens.

Turn Structure

  1. The student makes a move on the board.
  2. Combos and cascades keep the student’s turn going.
  3. Once the board settles, it becomes the Boss’s turn: a simple AI scans the board and executes one valid swap. Whatever it matches resolves through the exact same seven pieces and effects as the student’s own turn (see below) — the boss can deal damage, build up a multiplier, poison the student, shield itself, heal, and bank gold, exactly like the student does on their own turn.

The Seven Pieces

Every effect is symmetric: a piece does the same thing no matter which side is playing it — only who benefits changes (whoever’s turn it currently is).

Piece Effect
⭐ Star Adds +0.1 to a damage multiplier, stacking across turns; consumed on that side’s critical hit and reset to 1 when that side answers a question wrong
📖 Spellbook Fills a 0–100 poison meter of its own (+10 per piece); on reaching 100, arms 3 damage rounds against the opponent (one per their own turn) and resets, carrying over any overshoot
❓ Question Orb Fills a 100-point energy bar of its own for each side; reaching 100 pauses combat and opens the Question Challenge — the student answers for real, the boss “answers” by picking an option at random, validated the same way server-side
⚔️ Swords Direct damage to the opponent — no question needed. Scales with level/phase, and a 4- or 5-piece match deals 1.5x/2x the damage of a 3-piece match
🛡️ Shield Fills a 0–100 meter of its own (+10 per piece); on reaching 100, negates the next hit that side takes in full, no matter how large, then resets
🧪 Potion Heals that side’s own HP — scales the same way Sword damage does, at half the rate of a same-size Sword match
🪙 Coin Adds to that side’s own gold count for the match — see the combo formula and net compensation in PlayerHUD integration

The Question Challenge

When either side’s Question Orb bar reaches 100%, the match pauses and opens the question dialog:

This is the moment the plugin ties Match-3 skill to actual content knowledge: matching pieces well fills the bar and builds up the multiplier, but the real payoff only lands if the student also answers correctly.

Server-Side Truth

Every number that governs combat — the boss’s and student’s maximum HP, and the base damage figure Sword/Star/Spellbook/Potion all scale from — is computed once on the server for the current level and phase (see Game Modes for how these scale) and handed to the client as configuration; the client never recalculates any of it. Damage the client reports back is clamped server-side against that same phase-scaled boss HP before it is accepted — see Security & Anti-Cheat.

🎯 Question Engine

Source

PlayerPuzzle reads questions from Moodle’s own Question Bank (core_question) — the teacher picks an existing category in the activity’s settings, and any Multiple Choice or True/False question in it can be served during combat. This means a teacher who already maintains a question bank for Quiz can point PlayerPuzzle at the same category and reuse it immediately, with no content migration.

A second, PlayerPuzzle-owned question source (manual entry plus AI-assisted generation) is planned to coexist alongside the real Question Bank — see Features.

Blind JSON — the Answer Never Leaks

The question text and every answer option are sent to the browser, but the correct answer id is not. question_fetcher.php strips it server-side before the question ever reaches the client. When the student submits a choice, mod_playerpuzzle_validate_answer checks it against the real record in the database and returns only a boolean plus (on a wrong answer) the correct option — so nothing in the page source or network traffic ever reveals the answer in advance. This is the first of the plugin’s three anti-cheat pillars.

Minimum Questions Per Match

The activity settings let a teacher require a minimum number of questions to be answered before a match can end in victory, and the Lobby shows this requirement to the student before they start. The gameplay rule that enforces it — reviving the boss at half HP if the minimum hasn’t been reached yet — is not wired up yet; see Features.

🎮 Game Modes

The teacher picks one mode per activity instance.

Campaign

A continuous run through up to 10 levels, each split into 10 phases. Winning a phase never costs a new attempt — only losing (or timing out) does, and the configured Max Attempts limit only counts finished (lost/timeout/abandoned) attempts, never a phase won along the way.

Single Match

A single, self-contained round — no levels or phases. Grading reuses the grademethod pattern already established by the sibling mod_playerwords/mod_playercross activities: the teacher sets a Max Matches limit (or leaves it unlimited) via a 0–10 selector, matching the scale of a short, repeatable round rather than a long campaign.

Why Two Modes Instead of One

A Campaign “attempt” and a Single Match “attempt” are structurally different — a continuous run across many phases versus independent, repeatable rounds — so the plugin models them separately rather than forcing one grading/attempt-counting scheme to fit both. question_fetcher.php, save_progress.php and the Lobby all read the configured gamemode to know which rules apply.

💰 PlayerHUD Integration

PlayerPuzzle has no permanent economy of its own. Coins and upgrade levels live entirely in block_playerhud, and PlayerPuzzle only reads/writes to it when the block is present in the course — the plugin works standalone with zero configuration if it isn’t.

Teacher-Picked Items, Not Auto-Detection

For each course, the teacher explicitly picks which of the block’s own items represent:

Every one of these is a plain dropdown of the block’s own configured items — the same pattern already used by the sibling mod_playerwords/mod_playercross activities — so adding this integration never required a change to block_playerhud itself.

Coin Gain and the Boss’s Compensation

Matching the Coin piece (see How Combat Works) earns coins on a combo curve: a 3-piece match earns the configured “Coin gain” base value, a 4-piece match earns 1.5x that, and a 5-piece match earns 2x — the same curve Sword damage uses. Unlike every other combat value, Coin gain deliberately does not scale by level or phase, so a teacher can set fixed consumable prices (Potion, Shield, Quick Magic, Hint) that stay balanced for the whole campaign instead of getting proportionally cheaper as it progresses.

The boss also banks its own Coin total when it matches the piece on its own turn — it has no shop and never spends it. Its only purpose is to net against the student’s balance at the end of the match: the student’s final coin count is max(0, student's coins − boss's coins), banked via PlayerHUD only on victory. This gives the boss’s AI a reason to “compete” for the same piece the student wants, instead of it being inert whenever the AI happens to match it.

What’s Actually Wired Up Today

See Features for the full implemented/planned breakdown.

♿ Accessibility

The Design

Match-3 gameplay lives on a Canvas, which is opaque to screen readers by design. PlayerPuzzle’s accessibility approach is a parallel, always-present HTML layer rather than an afterthought bolt- on:

What’s Implemented Today

What’s Planned

speechSynthesis is never used as the default announcement channel, to avoid conflicting with whatever screen reader the student already has running.

🔐 Security & Anti-Cheat

Every combat number a student sees is a claim the client makes about itself. PlayerPuzzle treats none of it as trustworthy until the server confirms it, through three pillars:

1. Server as the Source of Truth

Boss and student HP for the current level/phase are computed once, server-side (combat::calculate_boss_hp()/calculate_student_hp()), and handed to the client as read-only configuration. The client never recalculates them.

2. Sanity-Checked Damage

Whatever damage the client reports at the end of a match is clamped against the real, phase- scaled boss HP before it’s accepted — not the base HP, and not whatever the client claims the cap should be. Advancing a phase (advance_phase) applies the same rule: the server requires the reported damage to actually clear that phase’s boss HP before allowing the transition.

A fuller sanity check — accounting for PlayerHUD upgrade multipliers and elapsed play time — is designed but not implemented yet; see Features.

3. Single-Use Anti-Replay Tokens

Every state-changing call (starting a match, resuming a campaign attempt, advancing a phase) issues a fresh token and invalidates the previous one. A captured request can’t be replayed to re-trigger the same reward twice — resuming a campaign mid-run rotates the token exactly like finishing one does, so the guarantee holds across a session that spans multiple page loads, not just within a single match.

Blind JSON

The correct answer id is never sent to the client — see Question Engine for how this is enforced at the data layer, not just the UI layer.

🎓 Educational Purpose

PlayerPuzzle is designed to:

Suitable for:

🕹️ PlayerGames Ecosystem

PlayerPuzzle is part of the PlayerGames gamification ecosystem for Moodle. None of the plugins below are required — PlayerPuzzle works standalone — but installing the ones relevant to a course unlocks the integrations documented throughout this page.

📦 Requirements

Component Version
Moodle 4.5 – 5.2
PHP 8.2+

block_playerhud is an optional dependency — see PlayerHUD Integration.

🛠️ Installation & Configuration

⚠️ This plugin is not yet published on the Moodle Plugin Directory. Install manually from this repository.

  1. Download the .zip file or clone this repository.
  2. Extract the folder into your Moodle mod/ directory.
  3. Rename the folder to playerpuzzle (if necessary). Final path: your-moodle/mod/playerpuzzle/
  4. Visit Site administration → Notifications to complete installation.
  5. Optional: install block_playerhud if you want the optional coin/upgrade economy — see PlayerHUD Integration.
  6. Add a PlayerPuzzle activity to any course, pick a Game Mode, a Question Bank category, and (optionally) which PlayerHUD items to use.

📖 Usage

For Teachers

  1. Add a PlayerPuzzle activity to a course.
  2. Choose a Game Mode — Campaign or Single Match — see Game Modes.
  3. Pick the Question Bank category the match will draw from.
  4. Optionally set a Minimum Questions per Match and whether wrong answers should count against the grade.
  5. Optionally configure the PlayerHUD Integration items, if block_playerhud is installed in the course.
  6. Set the Maximum Grade — both grading formulas scale to it, never to a fixed 100.

For Students

  1. Open the activity to reach the Lobby — it shows your current progress (Campaign mode), your PlayerHUD balance if configured, and any minimum-questions requirement.
  2. Select Play Game to start (or resume) a match.
  3. Match pieces to attack the boss, fill the energy bar to trigger a Question Challenge, and answer correctly for a critical hit.
  4. In Campaign mode, winning a phase never costs a new attempt — only losing does.

🧪 Automated Tests

PlayerPuzzle ships with an extensive PHPUnit suite covering the combat engine, the question pipeline, anti-cheat security, PlayerHUD integration, backup/restore, completion and privacy. Every CI push runs the full matrix (Moodle 4.5 → 5.3, PHP 8.1 → 8.4, PostgreSQL & MariaDB).

PHPUnit — Unit & Integration Tests (tests/)

Test file Cases
access_test.php 1
backup_restore_test.php 13
generator_test.php 1
lib_completion_info_test.php 6
lib_crud_test.php 8
lib_grade_item_update_test.php 4
lib_pluginfile_test.php 5
lib_supports_test.php 2
lib_update_grades_test.php 4
mod_form_test.php 11
phaser_loading_test.php 2
uninstall_test.php 2
Subtotal 59

Completion Tests (tests/completion/)

Test file Cases
custom_completion_test.php 11
Subtotal 11

Event Tests (tests/event/)

Test file Cases
game_completed_test.php 3
game_started_test.php 2
Subtotal 5

Web Services Tests (tests/external/)

Test file Cases
advance_phase_test.php 26
buy_stock_test.php 10
draw_question_test.php 8
generate_questions_test.php 2
get_phase_questionlog_test.php 5
save_combat_state_test.php 14
save_generated_questions_test.php 4
save_progress_test.php 29
set_sound_preference_test.php 5
transfer_hud_coins_test.php 8
use_stock_test.php 15
validate_answer_test.php 19
Subtotal 145

Form Tests (tests/form/)

Test file Cases
import_form_test.php 3
question_form_test.php 11
Subtotal 14

Local Business-Logic Tests (tests/local/)

Test file Cases
ai_question_generator_test.php 19
attempt_consumables_test.php 7
attempt_questions_test.php 1
coin_ledger_test.php 4
combat_state_test.php 5
game_page_service_test.php 27
grade_calculator_test.php 21
hud_service_test.php 18
lobby_page_service_test.php 22
move_log_test.php 20
question_bank_sync_test.php 13
question_editor_files_test.php 3
question_list_service_test.php 3
questions_repository_test.php 21
ranking_service_test.php 9
replay_credit_test.php 8
report_service_test.php 17
sound_preferences_test.php 7
user_stock_test.php 10
Subtotal 235

Combat Engine Tests (tests/local/engine/)

Test file Cases
board_engine_test.php 25
combat_engine_test.php 29
combat_test.php 9
golden_vectors_test.php 5
prng_test.php 5
question_fetcher_test.php 20
replay_test.php 28
security_test.php 43
Subtotal 164

Privacy Tests (tests/privacy/)

Test file Cases
provider_test.php 36
Subtotal 36
Grand Total 669
vendor/bin/phpunit --testsuite mod_playerpuzzle_testsuite

Coverage (moodle-coverage, PHPUnit + Xdebug): 86.79% lines, 74.03% methods (21/47 classes fully covered).

Full test-by-test breakdown and coverage table →

JavaScript — Unit Tests (tests/js/engine/)

The deterministic match-3/combat/PRNG math, run headless via Node’s own test runner (node --test) — no Jest, no browser. First plugin in the ecosystem with a JS test layer of its own.

Test file Cases
board_rules.test.js 25
combat_rules.test.js 29
prng.test.js 5
Subtotal 59
node --test tests/js/engine/board_rules.test.js tests/js/engine/combat_rules.test.js tests/js/engine/prng.test.js

Behat — Acceptance Tests

Feature file Scenarios
mod_playerpuzzle_accessibility.feature 4
mod_playerpuzzle_gameplay.feature 4
mod_playerpuzzle_match.feature 3
mod_playerpuzzle_ranking.feature 3
mod_playerpuzzle_settings.feature 1
mod_playerpuzzle_smoke.feature 1
Subtotal 16
php admin/tool/behat/cli/init.php
vendor/bin/behat --tags=@mod_playerpuzzle --profile=chrome

🔎 Third-party Service Disclosure

Not applicable today. PlayerPuzzle does not currently call any external service — every question comes from Moodle’s own Question Bank (see Question Engine), and no network request leaves the server as part of gameplay.

Planned

A second question source — manual entry plus optional AI-assisted generation via the companion local_aihub plugin, falling back to Moodle’s own core_ai — is designed for a future phase (see Features). When it ships, this section will disclose the exact providers, what data is transmitted, and any cost implications, following the same pattern already documented for local_playergames and local_aihub itself. As with those plugins, AI generation will be entirely opt-in and will never include student data in a prompt.

📄 License

This project is licensed under the GNU General Public License v3 (GPLv3).

Copyright: 2026 Jean Lúcio

Support

Found a bug or have a question? Open an issue on the issue tracker.

Maintainer

Maintained by Jean Lúcio.