A 3D, browser-based physics simulator for the FIRST Tech Challenge 2024–25 season, Into The Deep. Write a Pedro-Pathing-style autonomous path, run it on a physically simulated mecanum robot, get it graded by a deterministic rule engine, and share it with other teams on the community hub.
Try the live demo → (the demo is static, so its hub saves paths in your browser; run the server to share them)
- Real physics. Rapier rigid bodies at 120 Hz: a mecanum drivetrain with a motor torque curve and a traction circle, a submersible that tilts on spring hinges, and samples and specimens you can push, pick, shoot and clip.
- Pedro-Pathing-style paths. Lines and Béziers with any number of control points, heading interpolation, velocity and acceleration limits, waits and actions. The editor takes JSON, relaxed JSON or a pasted data string.
- Simulation Advocate. A deterministic grader that scores legality, efficiency, expected points and defense exposure against a scripted opponent, and marks problems on the field.
- Community hub. Publish paths with your team number, search, filter, upvote, and execute anyone's path in one click. Only the browser that published a path can edit or delete it.
- Driver mode. Drive the robot yourself with the keyboard to feel what your auto is asking for.
Needs Node.js 22 or newer.
git clone https://github.com/ishaanjayaswal-dotcom/deepsim-ftc.git
cd deepsim-ftc
npm install
npm run dev # web app on http://localhost:5188, API on :8787No database to install: the server creates a SQLite file in data/ on first start and seeds a few starter paths.
| Command | What it does |
|---|---|
npm run dev |
Vite dev server and the API server with reload |
npm run build |
Typecheck everything, build the web app to dist/ and the server to server/dist/ |
npm start |
Serve the built app and the API on one port (default http://127.0.0.1:8787) |
npm test |
Path compiler, follower, evaluator and API tests |
npm run typecheck |
Typecheck the web app and the server |
npm run db:generate |
Generate a new migration after editing server/src/db/schema.ts |
docker build -t deepsim .
docker run -p 8787:8787 -v deepsim-data:/app/data deepsimnpm run build also produces a plain static site in dist/. Hosted without the server (GitHub Pages, Netlify, any
CDN), the app notices there is no API and keeps the hub in the browser's local storage. Build with
npx vite build --base=/your-sub-path/ when it is served from a sub-path.
All settings are environment variables and all are optional. See .env.example.
| Variable | Default | Meaning |
|---|---|---|
PORT |
8787 |
Port to listen on |
HOST |
127.0.0.1 |
Bind address (0.0.0.0 in Docker) |
DATABASE_URL |
file:./data/deepsim.db |
libSQL URL: a local SQLite file, or a libsql:// Turso database |
DATABASE_AUTH_TOKEN |
– | Turso auth token |
ADMIN_KEY |
– | Moderator key (16+ characters): Authorization: Bearer <key> can edit or delete any path |
CORS_ORIGIN |
– | Comma-separated origins allowed to call the API from another site (* is not supported) |
TRUST_PROXY |
false |
Take the client IP from the last X-Forwarded-For entry. Set it only when the server sits behind exactly one reverse proxy you control |
SEED |
true |
Add the starter paths when the database is empty |
STATIC_DIR |
dist |
Built web app to serve |
The web app finds the API by itself when both are served from the same origin. To point it at an API elsewhere, build
with VITE_PATHS_API=https://your-host/api.
| Region | What it does |
|---|---|
| Left sidebar → Path Editor | Highlighted JSON editor with inline errors, examples, Format, chain breakdown, Run (⌘/Ctrl+Enter) and Publish Path |
| Left sidebar → Community Hub | Search, filter by strategy, sort by top or new, upvote, copy the data string, Execute in Simulator |
| Centre | The 3D field. Camera presets (Broadcast / Top / Driver / Chase), overlay toggles, opponent picker |
| Right rail → Simulation Advocate | Overall grade, four metric cards with findings, live telemetry, event log |
Keys: P pause, R reset, C cycle camera. In Driver mode: WASD drive (relative to the camera), Q/E turn,
Shift precision, Space intake or drop, 1/2 high or low basket, 3/4 high or low chamber.
Paste a bare array of waypoints or an object. The parser also accepts comments, trailing commas, single quotes,
unquoted keys, a const path = …; wrapper, and base64 data strings copied from the hub.
- Coordinates follow Pedro Pathing: inches, origin in a field corner, x and y from 0 to 144, heading in degrees
counter-clockwise from +x (add
"headingUnits": "rad"for radians). The red alliance wall is x = 0; red's net zone and baskets are at (0, 144) and its observation zone at (0, 0). The field is rotationally symmetric. - Segments:
typeislineorbezier. A Bézier takes any number ofcontrolPoints([x, y]or{x, y}). Control points without a type mean Bézier. - Heading:
headingInterpolationislinear(default, shortest direction),tangent,reverseTangentorconstant. - Per waypoint:
maxVelcaps speed on the segment ending there;action,wait(seconds) andstopforce a full stop;extendsets the intake reach in inches. - Actions:
intake,intake_sample,intake_specimen,score_high,score_low,specimen_high,specimen_low,park,wait.
Waypoints between two stops form a chain (Pedro's PathChain) that the robot flows through without stopping. Each chain is sampled every 0.5 in and speed-profiled with a forward/backward pass under velocity, acceleration, deceleration, lateral-acceleration (curvature) and heading-rate limits.
- Robot: an 18 × 18 in, 14 kg body that only yaws. Wheel friction is zero and the drivetrain is modelled
explicitly (
src/sim/drivetrain.ts): a velocity loop, a motor torque–speed curve, mecanum strafe efficiency, and a traction circle at μ·g that drops to kinetic friction once the wheels slip. The follower (src/path/follower.ts) works like Pedro's: it projects onto the closest point of the path, drives along the tangent at the profiled speed, and adds translational, centripetal and heading correction. It only asks for a velocity; the physics decides what the wheels deliver, so aggressive profiles or low μ give real slip and overshoot (amber trail, cyan ghost). - Submersible: a dynamic body on two spring-loaded revolute hinges (pitch and roll, ±2.6°) that rocks when hit or loaded and settles back to level.
- Game elements: instanced rigid bodies with continuous collision detection. Held elements become kinematic and non-colliding; shots fly a ballistic arc with a deterministic aim error; basket sensors count points; clipped specimens ride the tilting submersible.
- Opponent: a kinematic replay of a deterministic path with infinite mass, so it really does shove you.
A pure function of (path, robot, opponent): the same input always gives the same report.
| Metric | How it's computed |
|---|---|
| Legality | The robot's footprint (an oriented box) checked at every sample against the field bounds and structures with the separating axis test, plus the starting pose and the 30 s limit |
| Efficiency | Planned time against the hardware minimum; flags curvature-capped distance, detours, idle stops and profiles that ask for more traction than the tiles have |
| Scoring yield | Replays the action timeline against the real element layout with the runtime's reach rules (src/lib/rules.ts) |
| Defense | Steps both robots through time to find contacts, contested time (within 8 in), pinch points and lane crossings |
The overall grade weights legality 30%, efficiency 20%, yield 30% and defense 20%. Hub cards are always graded against the Raider opponent so they compare like for like.
The server is Hono + Drizzle ORM on libSQL. Every route lives under
/api; bodies are JSON; errors are { "error": "message" }. Wire types are in src/repo/types.ts.
| Method | Route | Notes |
|---|---|---|
| GET | /api/health |
{ ok: true, version } |
| GET | /api/paths?q=&category=&sort=new|top&limit=&offset= |
PathSummary[]: every field except the path source data |
| GET | /api/paths/:id |
PathRecord, including data |
| POST | /api/paths |
Body: name, teamNumber, category, description, data. Returns 201 with the record and a one-time editKey |
| PATCH | /api/paths/:id |
Header X-Edit-Key. Any of the create fields |
| DELETE | /api/paths/:id |
Header X-Edit-Key. 204 |
| POST | /api/paths/:id/upvote |
Header X-Voter-Id (an anonymous per-browser id). One vote per voter |
The server recomputes each path's thumbnail, length, duration and grade from its source, so cards can't be faked. Paths must stay near the field (coordinates from −72 to 216 in) and under 5,000 in long. Edit keys are stored only as hashes. Requests are rate limited per IP. Lists return 50 paths by default and at most 100 per page.
src/config field geometry, robot parameters, points
src/path parser, Bézier maths, compiler and profiler, follower, presets and opponents
src/eval geometry (SAT) and the Simulation Advocate
src/lib shared game rules (intake reach, basket shot, chamber, park)
src/sim React Three Fiber scene, Rapier bodies, drivetrain model, score keeper, overlays
src/repo hub client (HTTP + browser-local), shared derivation and seed paths
src/store app state (zustand)
src/ui dashboard components
server/src Hono app, routes, Drizzle schema, rate limiting
server/drizzle SQL migrations
- Field dimensions follow the game manual closely but not exactly: the submersible is 44.5 × 29 in, chambers at 26 / 13 in, rungs at 20 / 36 in, baskets at 43 / 25.75 in. Baskets sit in the net-zone corners as in the real game.
- The submersible's tilt is a deliberate exaggeration; the real one is rigid.
- Starter hub paths use placeholder team numbers.
Issues and pull requests are welcome. Please run npm run typecheck and npm test before opening a PR.
MIT. Inspired by DSIM from Offset Robotics. DeepSim is an independent project, not affiliated with or endorsed by FIRST® or Offset Robotics. FIRST® Tech Challenge and Into The Deep are trademarks of FIRST.

{ "name": "4 Sample Auto · Red", "alliance": "red", // red | blue "preload": "sample", // sample | specimen | none "constraints": { "maxVel": 62, "maxAccel": 75, "maxDecel": 65, "maxAngVel": 260, "lateralAccel": 90 }, "path": [ { "x": 9, "y": 111, "heading": 270 }, // start pose { "x": 15, "y": 128, "heading": 315, "type": "bezier", "controlPoints": [[21, 115]], "action": "score_high" }, { "x": 36, "y": 121, "heading": 0, "type": "line", "action": "intake" } ] }