Mirage — AR Treasure Hunt
Completed
Society · Web

Mirage — AR Treasure Hunt

Creative Computing Society · 2023

Campus-wide geolocated AR treasure hunt, hand-rolled in PHP with its own autoloader and database layer. index.php sniffs the user agent and sends phones to an AR.js / A-Frame camera hunt where PHP injects each team's allotted GPS coordinates into the scene, while desktops get the organizer dashboard. Behind it sits an admin console for question allotment, teams, locations and the leaderboard, plus a pin-drop tool for authoring the map.

SCALE

~2,900 players over two editions

Built with
PHPPHP
MySQLMySQL
AR.jsAR.js
A-FrameA-Frame
JavaScriptJavaScript
Geolocation APIGeolocation API
Project Details

STATUS
Completed
ORGANISATION

Creative Computing Society

YEAR

2023

SCALE

~2,900 players over two editions

TYPE

Society · Web

TAGS
AR
Events
Gamification

Most campus treasure hunts are a quiz with walking attached. Mirage inverted that: the clue does not exist until you are physically standing at the right GPS coordinate, at which point it appears floating in your phone's camera feed. Two editions ran on this — roughly 2,900 players in total, 445 in 2023 and about 2,400 in 2024 — on hand-rolled PHP and MySQL with no framework anywhere in it.

The constraint that shaped everything was the deploy target. This ran on shared PHP hosting with no Composer, no shell, no build step and no ability to install anything. Whatever the game needed had to be a file you could FTP up. That rules out most of the obvious answers, and it is why a university society event ended up with its own database layer, its own autoloader and a shutdown procedure that consists of renaming a file.

The AR is geo-AR, not marker-AR

This is the part worth explaining. Marker AR needs a printed target; location AR needs nothing but the phone's GPS and compass. tokyo/camera.php builds an <a-scene> with arjs="sourceType: webcam; videoTexture: true" and an <a-camera gps-new-camera="gpsMinDistance: 10">, then PHP injects that team's allotted coordinates into the scene server-side before the page ever reaches the browser. Each coordinate becomes an <a-entity gps-new-entity-place> wrapping a ten-unit <a-box> whose texture is the question image itself, served as questions/<ques_id>.png. So the thing floating over the quad is the clue. Tapping it fires a registered A-Frame component that pops a fixed overlay reading SCAN QR, which is how the physical checkpoint and the digital one were tied together on the ground.

The whole thing is Money Heist themed, and index.php sniffs the user agent to route it: phones go to /tokyo for the field game, desktops go to /professor for the team's control panel. The camera page repeats the same sniff and refuses to render at all on desktop, because a location-AR scene without a GPS fix is a black rectangle and a confused player.

tokyo/cam.php is the prototype that came first: three hardcoded campus coordinates, a placeholder box texture on a CDN, and gpsMinDistance at 20 rather than 10. Halving that threshold for the real game is a straight trade. A larger minimum distance means fewer scene recomputations and a steadier picture; a smaller one means the box stops drifting when a player circles a checkpoint looking for it. Players circling checkpoints is the entire game, so 10 won.

Full surface area

  • Question allocation. admin/allot_questions.php truncates the allocation table, then per team draws 4 easy, 4 medium and 3 hard questions with ORDER BY RAND(), shuffles them, and pairs them against 10 randomly chosen locations — so no two teams walk the same route.
  • Scoring. Easy 5, medium 10, hard 20, with the team's timestamp written on every solve so ties break on who got there first.
  • Player API. login.php, logout.php, get_locations.php, unlock_question.php (unlocking on arrival awards the points) and submit_answer.php (case-insensitive comparison, then marks the allocation solved). Every one of them opens with the same session guard and dies silently if user and t_id are not both set.
  • Organizer dashboard. professor/dashboard.php renders one card per allotted question showing the location hint and difficulty, revealing the question text only once the field player has unlocked it, with jQuery AJAX submission, toastr feedback and a live countdown to the deadline.
  • Admin console. CRUD for questions and locations, team management, password tooling and two leaderboard views.
  • Two leaderboards, not one. ldrb.php and leader.php run the same query — teams ordered by score descending, then timestamp ascending — but leader.php adds a large countdown clock above it, hardcoded to the 2023 finish time. One is for the projector in the room, one is for the desk. Both self-refresh every 30 seconds rather than polling.
  • Team registration. register/index.php takes a team, a password and up to a full roster in one POST, hashes the team credential, inserts the team, then loops the members into users. If any member fails validation it deletes the team it just created and re-renders the form with the error. That is a hand-written rollback, because the query layer exposes insert, update, delete and select and nothing else, so a transaction was never on the table.
  • Map authoring tool. capture/saveloc.php plus a mobile page that reads navigator.geolocation and pins a named coordinate — that is how the campus checkpoints were surveyed in the first place.
  • 50 question images, a six-table schema (users, teams, questions, alloted_questions, locations, game), and a bulk mail template for reaching registrants.

The data model, and where it splits

teams is the scoring unit and the credential holder: name, password hash, leader contact details, team size, score and a last-solve timestamp. users are the individual members, each carrying a t_id back to their team, which is why login accepts any member's email and resolves the password against the team. questions holds the text, the answer and a difficulty level; locations holds a name, latitude, longitude and the hint that the desk player sees. alloted_questions is the join that makes the game work, one row per team per checkpoint carrying t_id, loc_id, ques_id and two flags, unlocked and solved. Splitting unlock from solve is the whole design: arriving is worth points and is what the phone reports, answering is worth progress and is what the desktop reports, and the two halves of a team can only see their half.

On writing it without a framework

Everything database-side is custom: Database/Connect.php, Handler.php and Validator.php behind a hand-written autoloader.php. The handler takes a table name, an associative array and an optional datatype map, validates each field against a type list and returns collected errors rather than throwing. That was a deliberate call — the deploy target was shared PHP hosting with no Composer, and a validating query layer written once covered every admin screen that followed.

The internals are more careful than the surface suggests. Connect opens PDO with ATTR_EMULATE_PREPARES off, so those are real server-side prepared statements and every value in a generated INSERT, UPDATE or SELECT is bound rather than interpolated. Validator dispatches on a convention, turning a declared type into a v-prefixed method call, so adding a new rule is adding one method. The counter-example is sitting in the same repo: database_connect.php and database_get_data.php are procedural mysqli leftovers with credentials in the file and query strings built by concatenation. Having both in one tree makes the argument for the rewrite better than any commit message could.

How it shipped, and how it stopped

Credentials live in Config/config.php, which is gitignored, so the repo is deployable but not runnable as cloned. Ending the event is the best part: the 2023 tree contains a file called Gameover.htaccess whose first line is a comment reading "Rename this file to .htaccess to end game", and it 301s the entire host to a static Glitch page. The 2024 tree has exactly that file, in place, as .htaccess. The 2024 edition is otherwise the 2023 codebase unchanged except for a single commit that raises the easy question allocation from three to four, which is what running the game once and watching the difficulty curve teaches you.

Honest limitations

Passwords are MD5 with no salt, on both teams and users. The admin console is gated by one shared literal password compared in plaintext against $_POST['pass'], and behind that gate admin/pass.php interpolates the search term and the team id straight into SQL, which is the one place the careful handler is bypassed entirely. There is no CSRF token anywhere, and answer submission has no rate limit against a case-insensitive string compare, so a team with a laptop and a wordlist could have brute-forced a short answer. The handler recovers field names with array_search on values, which returns the wrong key the moment two fields in one row hold the same value. The point-award helper assigns the question's value instead of adding it, and writes to a points column while both leaderboards read score. And both leaderboards unconditionally skip their first result row, a hardcoded fix for a seeded test team that nobody went back to remove. The four AR libraries are vendored into tokyo/dist/ — the sensible move for an event running on campus Wi-Fi — but the page that actually shipped loads A-Frame, ar-threex-location-only.js, aframe-ar and aframe-gui from three different CDNs, with the local A-Frame line commented out directly above the remote one. The intent is in the repo; the switch never got flipped.

Project Details

STATUS
Completed
ORGANISATION

Creative Computing Society

YEAR

2023

SCALE

~2,900 players over two editions

TYPE

Society · Web

TAGS
AR
Events
Gamification