More Projects
59 in total
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.
~2,900 players over two editions
Project Details
Creative Computing Society
2023
~2,900 players over two editions
Society · Web
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.phptruncates the allocation table, then per team draws 4 easy, 4 medium and 3 hard questions withORDER 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) andsubmit_answer.php(case-insensitive comparison, then marks the allocation solved). Every one of them opens with the same session guard and dies silently ifuserandt_idare not both set. - Organizer dashboard.
professor/dashboard.phprenders 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.phpandleader.phprun the same query — teams ordered by score descending, then timestamp ascending — butleader.phpadds 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.phptakes a team, a password and up to a full roster in one POST, hashes the team credential, inserts the team, then loops the members intousers. 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.phpplus a mobile page that readsnavigator.geolocationand 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
Creative Computing Society
2023
~2,900 players over two editions
Society · Web