Submission for the UXHack 2026 Hackathon
demo.mp4
| Hackathons UK | UoS Techvision Society |
|---|---|
![]() |
![]() |
SnekUp is a counter-intuitive sign-up experience. You land on what looks like a completely normal registration form - but you quickly realize there's a gimmick, and the moment you try to submit it, the fields run away!
The only way to fill them in is to eat them.
You are a snake. Use the arrow/WASD keys. Chase the fields down and swallow them whole. Each capture freezes the game and drops an input prompt. Fill in the field correctly, and the snake speeds up, the timer ticks on, and the next field runs for cover.
- Miss the wall.
- Don't eat yourself.
- Don't let the clock hit zero. (You get punished for typing!)
- Complete the form and get on the leaderboard!
It's a sign-up form that's-- hear me out-- also a snake game!
- A normal-looking marketing page. Send this to someone who's clueless
/signup: a glossy, totally innocent sign-up card. One field active at a time. The card shakes when you type. Something is wrong.- Click the submit button -> The form dissolves. Fields scatter in all directions. The game begins.
- Steer the snake with arrow keys or WASD. Fields bounce like DVD screensavers and the password field runs away until you catch the other fields. You have to catch them to fill them in.
- Once Name, Email, and Password are captured, a "Verify Password" field spawns at max speed.
- Your time, deaths, and rank is saved to a persistent leaderboard. You can edit your display name once and pick a frame colour; the top 3 get an animated dual-gradient border and a rainbow name.
| Key | Action |
|---|---|
↑ ↓ ← → or WASD |
Move the snake |
Enter |
Confirm field input |
- Capture a field — steer the snake head into it; the game pauses and an input prompt appears
- Valid input → snake grows, game resumes at higher speed
- Invalid input → −5 seconds, try again
- Each character typed → −1 second + snake grows 1 segment (every keystroke costs you)
- Death (wall or self) → red flash, 3-2-1 countdown, fields scatter, death added to your count
- Time's up → counts as perma death, restart
Before the game starts, you fill in a "normal" sign-up form. It has a twist:
- One field active at a time — tabbing/clicking doesn't work, the form decides when you're ready
- Progress bar — fills with each keystroke; decays when you stop; hits 100% → field locks in, glitch flash, advance
- Card shake — the card shakes harder as you fill in more fields
- Fields:
- Name
- Verify Email
- Secret (choose your spirit animal — you'll need it for the password)
Spirit animals to choose from: Axolotl, Capybara, Cassowary, Dingo, Echidna, Narwhal, Okapi, Pangolin, Quokka, Tapir, Wombat
The password field is locked until Name and Email are both captured. Once unlocked it flees aggressively. When you catch it, the rules are revealed one at a time as each is satisfied:
| Step | Rule |
|---|---|
| 1 | Must contain your spirit animal, backwards |
| 2 | At least 8 characters |
| 3 | Contains an uppercase letter |
| 4 | Digits in your password must add up to 25 |
Example: Secret = "Narwhal" → password must contain "lahwran" (case-insensitive)
Runs are ranked by time (fastest first), then deaths (fewest first).
| Event | Effect on score |
|---|---|
| Character typed | −1 second |
| Failed validation | −5 seconds |
| Death (wall/self) | +1 death counter |
| Time expired | +1 death, full restart |
# From the repo root — install client + server deps
cd client && npm install && cd ..
cd server && npm install && cd ..
# Start both (client :5173, server :3001)
npm run devThe Vite dev server proxies /api to localhost:3001 automatically.
| Layer | Technology |
|---|---|
| Frontend | React 19 (Vite 7), DOM-based rendering |
| Styling | Tailwind CSS 4, shadcn/ui, custom pixel-art theme |
| Game Engine | Vanilla JS class (engine.js), 30 ms initial tick |
| State | React Context + hooks |
| Backend | Node.js + Express 5 |
| Database | SQLite via better-sqlite3 |
| Audio | Howler.js |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/health |
Health check — { status: "ok" } |
POST |
/api/submit |
Submit a run: { name, email, timeMs, deaths } → { rank, id } |
GET |
/api/leaderboard |
Top 10, sorted by time then deaths |
PATCH |
/api/submit/:id/name |
One-time display name edit |
PATCH |
/api/submit/:id/frame-color |
Set leaderboard frame colour(s) |
Email is SHA-256 hashed before storage. Raw email is never persisted.
/
├── client/
│ └── src/
│ ├── game/ # Pure JS — no React, no hooks
│ │ ├── engine.js # GameEngine class (tick, collision, callbacks)
│ │ ├── fields.js # Field class + flee AI + factory functions
│ │ └── constants.js # All magic numbers live here
│ ├── components/
│ │ ├── LandingPage.jsx # Marketing page (/)
│ │ ├── LoginPage.jsx # Pre-game form + inline post-game overlay (/signup)
│ │ ├── GameBoard.jsx # DOM-based snake + field renderer
│ │ ├── FireBorder.jsx # Animated fire trail + status bar
│ │ ├── InputOverlay.jsx # Pixel-art input modal on capture
│ │ └── LeaderboardModal.jsx # Leaderboard table (post-game + standalone)
│ ├── hooks/
│ │ ├── useSnakeGame.js # Consolidated game state + all handlers
│ │ ├── useGameLoop.js # Mounts GameEngine, exposes gameState
│ │ ├── useKeyboard.js # Arrow/WASD input → engine direction queue
│ │ ├── useTimer.js # 120s countdown, pauseable, penalize(ms)
│ │ └── useAudio.js # Howler.js lazy-init audio system
│ └── context/
│ └── GameContext.jsx # Captured form values (name, email, password)
└── server/
├── routes/
│ ├── submissions.js # POST /api/submit, 2× PATCH endpoints
│ └── leaderboard.js # GET /api/leaderboard (top 10)
├── db.js # SQLite connection + schema init
└── index.js # Express server entry point
| Constant | Value | Description |
|---|---|---|
TICK_RATE_MS |
30 ms | Initial snake speed |
TICK_RATE_INCREASE_MS |
5 ms | Speed gain per captured field |
VERIFY_TICK_RATE_MS |
15 ms | Tick rate during Verify Password phase |
LOCK_FLEE_RADIUS |
15 cells | Distance at which locked Password field flees |
LOCK_FLEE_SPEED |
4 cells/tick | Speed of locked field fleeing |
GRID_COLS × GRID_ROWS |
100 × 75 | Logical grid dimensions |
SCATTER_DELAY_MS |
3000 ms | Scatter animation duration |
| Timer | 120 s | Total time per run |
| Command | Description |
|---|---|
npm run dev |
Start client + server concurrently |
npm run dev:client |
Client (Vite) only |
npm run dev:server |
Server only |
cd client && npm run build |
Build client for production |
- Mobile support (on-screen D-pad or swipe controls)
- Update your submission with a new high score on replays
- Difficulty tiers (flee radius / speed scaling per capture)
- Multiplayer race via WebSockets
- Animated snake skin or per-field easter eggs
- CAPTCHA parody: second form post-completion with faster fields
Built for a UX hackathon. Theme: Counter-Intuitive.

