diff --git a/README.md b/README.md index 21ba406..f2845d1 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ give either to also gets the game data. - The original title picture. - 1 or 2 players (split screen) plus up to 7 computer drivers. - Power-slides, jumps, hills that slow you going up and speed you up coming - down, car-to-car bumping, and skid marks burnt into the track. + down, spinning bumps against other cars, and skid marks burnt into the + track. - Lap and position tracking, lap times, and a results table. - Engine note pitched with revs, tyre screech, crashes, backfires, a horn and start beeps, all from the original samples. @@ -57,12 +58,14 @@ were decoded from its machine code and work the same way here: - the meaning of every `.set` value, and the handling presets - the walls along the road edges - how the world is projected onto the screen, including height +- the computer drivers' path and how they choose their target and throttle +- collisions between cars. As in the original, only human players' cars + collide: computer cars pass through each other and through caravans. +- towing, including caravans jack-knifing The rest is new code written to feel like the original: -- how computer drivers choose their line (they steer and drive as the - original's do) -- car-to-car collisions -- lap counting and the rescue of stuck cars +- lap counting +- the rescue of stuck cars (the original warps them back instead) [docs/FORMATS.md](docs/FORMATS.md) covers everything reverse engineered so far. diff --git a/assets/README.md b/assets/README.md index ac81c55..cd0683f 100644 --- a/assets/README.md +++ b/assets/README.md @@ -103,7 +103,6 @@ The display name, driver and handling: | `driver` | driver's name, shown next to it | | `trailer` | `yes` for something towed behind cars (the caravan) rather than driven | | `handling`| the 16 values from the original `.set` file, used as the original game uses them (below) | -| `mass` | weight when cars collide; not from the original data | The `handling` values, counting from 1: @@ -113,8 +112,8 @@ The `handling` values, counting from 1: | 9, 10, 11 | human drivers: drag, grip, power | | 3 | steering: 16 = 1/64 of a turn per frame at full lock | | 5 | gravity: 8 = 1 pixel per frame² | -| 7 | tow hitch distance behind the car: 2 = 1 world unit | -| 4, 8, 12–16 | unused | +| 7, 8 | half the car's length and width, for collisions: 2 = 1 world unit. The tow hitch is at the back. | +| 4, 12–16 | unused | What the driving values do: - **Drag** (below 64): how much of the engine's speed survives each frame. @@ -128,9 +127,9 @@ What the driving values do: The menu's **Handling** option can replace every car's values with one of the original's presets. -Older files with `original`, `top`, `accel`, `brake`, `grip`, `turn` and -`offroad` lines still load: `original` is read as `handling`, and the -others are ignored. To add a car, add a new `.png` / `.txt` pair. +Older files with `original`, `top`, `accel`, `brake`, `grip`, `turn`, +`offroad` and `mass` lines still load: `original` is read as `handling`, +and the others are ignored. To add a car, add a new `.png` / `.txt` pair. ## Sounds: `sounds/*.wav` diff --git a/assets/vehicles/camaro.txt b/assets/vehicles/camaro.txt index 3b0ba0b..cbda439 100644 --- a/assets/vehicles/camaro.txt +++ b/assets/vehicles/camaro.txt @@ -9,9 +9,8 @@ trailer no # original game does. Counting from 1: # 1 2 6 computer drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power -# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. +# 3 steering, 5 gravity, 7 and 8 half the car's length and width +# (for collisions; the tow hitch is at the back). The rest are unused. # Top speed = power * drag / (64 - drag) / 32 world units per frame, # 50 frames a second (drag below 64). Higher grip, less sliding. handling 54 35 32 8 7 22 22 16 49 14 53 0 0 0 0 0 -# weight when cars collide (not from the original data) -mass 1 diff --git a/assets/vehicles/caravan.txt b/assets/vehicles/caravan.txt index 3a57ca0..d6951d8 100644 --- a/assets/vehicles/caravan.txt +++ b/assets/vehicles/caravan.txt @@ -9,9 +9,8 @@ trailer yes # original game does. Counting from 1: # 1 2 6 computer drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power -# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. +# 3 steering, 5 gravity, 7 and 8 half the car's length and width +# (for collisions; the tow hitch is at the back). The rest are unused. # Top speed = power * drag / (64 - drag) / 32 world units per frame, # 50 frames a second (drag below 64). Higher grip, less sliding. handling 2 65532 65532 1 12 10 16 14 0 0 0 0 0 0 0 0 -# weight when cars collide (not from the original data) -mass 1 diff --git a/assets/vehicles/cow.txt b/assets/vehicles/cow.txt index 4143a13..0bb7369 100644 --- a/assets/vehicles/cow.txt +++ b/assets/vehicles/cow.txt @@ -9,9 +9,8 @@ trailer no # original game does. Counting from 1: # 1 2 6 computer drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power -# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. +# 3 steering, 5 gravity, 7 and 8 half the car's length and width +# (for collisions; the tow hitch is at the back). The rest are unused. # Top speed = power * drag / (64 - drag) / 32 world units per frame, # 50 frames a second (drag below 64). Higher grip, less sliding. handling 15 14 34 8 4 598 15 12 13 4 2003 0 0 0 0 0 -# weight when cars collide (not from the original data) -mass 1.4 diff --git a/assets/vehicles/f1.txt b/assets/vehicles/f1.txt index 3a00199..ca309b0 100644 --- a/assets/vehicles/f1.txt +++ b/assets/vehicles/f1.txt @@ -9,9 +9,8 @@ trailer no # original game does. Counting from 1: # 1 2 6 computer drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power -# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. +# 3 steering, 5 gravity, 7 and 8 half the car's length and width +# (for collisions; the tow hitch is at the back). The rest are unused. # Top speed = power * drag / (64 - drag) / 32 world units per frame, # 50 frames a second (drag below 64). Higher grip, less sliding. handling 60 71 30 0 6 16 24 12 58 42 29 0 0 0 0 0 -# weight when cars collide (not from the original data) -mass 0.9 diff --git a/assets/vehicles/midget.txt b/assets/vehicles/midget.txt index bf68c27..b506d7d 100644 --- a/assets/vehicles/midget.txt +++ b/assets/vehicles/midget.txt @@ -9,9 +9,8 @@ trailer no # original game does. Counting from 1: # 1 2 6 computer drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power -# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. +# 3 steering, 5 gravity, 7 and 8 half the car's length and width +# (for collisions; the tow hitch is at the back). The rest are unused. # Top speed = power * drag / (64 - drag) / 32 world units per frame, # 50 frames a second (drag below 64). Higher grip, less sliding. handling 51 21 30 16 3 45 16 13 44 13 93 0 0 0 0 0 -# weight when cars collide (not from the original data) -mass 0.85 diff --git a/assets/vehicles/mini.txt b/assets/vehicles/mini.txt index 0ab0bae..484c26b 100644 --- a/assets/vehicles/mini.txt +++ b/assets/vehicles/mini.txt @@ -9,9 +9,8 @@ trailer no # original game does. Counting from 1: # 1 2 6 computer drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power -# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. +# 3 steering, 5 gravity, 7 and 8 half the car's length and width +# (for collisions; the tow hitch is at the back). The rest are unused. # Top speed = power * drag / (64 - drag) / 32 world units per frame, # 50 frames a second (drag below 64). Higher grip, less sliding. handling 56 24 30 12 3 24 20 12 46 10 100 0 0 0 0 0 -# weight when cars collide (not from the original data) -mass 0.8 diff --git a/assets/vehicles/police.txt b/assets/vehicles/police.txt index 5cbf896..1f3edd3 100644 --- a/assets/vehicles/police.txt +++ b/assets/vehicles/police.txt @@ -9,9 +9,8 @@ trailer no # original game does. Counting from 1: # 1 2 6 computer drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power -# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. +# 3 steering, 5 gravity, 7 and 8 half the car's length and width +# (for collisions; the tow hitch is at the back). The rest are unused. # Top speed = power * drag / (64 - drag) / 32 world units per frame, # 50 frames a second (drag below 64). Higher grip, less sliding. handling 56 34 22 6 2 15 19 11 49 14 44 0 0 0 0 0 -# weight when cars collide (not from the original data) -mass 1.2 diff --git a/assets/vehicles/porsche.txt b/assets/vehicles/porsche.txt index a756202..35fc0d5 100644 --- a/assets/vehicles/porsche.txt +++ b/assets/vehicles/porsche.txt @@ -9,9 +9,8 @@ trailer no # original game does. Counting from 1: # 1 2 6 computer drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power -# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. +# 3 steering, 5 gravity, 7 and 8 half the car's length and width +# (for collisions; the tow hitch is at the back). The rest are unused. # Top speed = power * drag / (64 - drag) / 32 world units per frame, # 50 frames a second (drag below 64). Higher grip, less sliding. handling 56 34 22 6 2 15 21 12 49 14 44 0 0 0 0 0 -# weight when cars collide (not from the original data) -mass 1 diff --git a/assets/vehicles/truck.txt b/assets/vehicles/truck.txt index 36f9b80..108d708 100644 --- a/assets/vehicles/truck.txt +++ b/assets/vehicles/truck.txt @@ -9,9 +9,8 @@ trailer no # original game does. Counting from 1: # 1 2 6 computer drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power -# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. +# 3 steering, 5 gravity, 7 and 8 half the car's length and width +# (for collisions; the tow hitch is at the back). The rest are unused. # Top speed = power * drag / (64 - drag) / 32 world units per frame, # 50 frames a second (drag below 64). Higher grip, less sliding. handling 55 26 28 18 2 29 20 17 54 19 35 0 0 0 0 0 -# weight when cars collide (not from the original data) -mass 1.8 diff --git a/assets/vehicles/vw.txt b/assets/vehicles/vw.txt index 0242fe5..1723865 100644 --- a/assets/vehicles/vw.txt +++ b/assets/vehicles/vw.txt @@ -9,9 +9,8 @@ trailer no # original game does. Counting from 1: # 1 2 6 computer drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power -# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. +# 3 steering, 5 gravity, 7 and 8 half the car's length and width +# (for collisions; the tow hitch is at the back). The rest are unused. # Top speed = power * drag / (64 - drag) / 32 world units per frame, # 50 frames a second (drag below 64). Higher grip, less sliding. handling 57 28 32 12 3 29 20 14 51 15 63 0 0 0 0 0 -# weight when cars collide (not from the original data) -mass 1 diff --git a/docs/FORMATS.md b/docs/FORMATS.md index dfd166f..6d271ac 100644 --- a/docs/FORMATS.md +++ b/docs/FORMATS.md @@ -194,12 +194,13 @@ with these shifts. Word `k` is the `k`th `u16`, counting from 0: | 3 | `$c0` | −w / 64 | stored but never read | | 4 | `$106` | w / 8 | gravity, pixels per frame² | | 5 | `$b8` | w / 512 | power (computer drivers) | -| 6 | (towing) | w / 32 | tow hitch distance behind the car, in game units | +| 6 | (collisions, towing) | w / 32 | half the car's length, in game units; the tow hitch is at the back | +| 7 | (collisions) | w / 32 | half the car's width | | 8 | `$bc` | w / 64 | drag (human drivers) | | 9 | `$e2` | w / 512 | grip (human drivers) | | 10 | `$b8` | w / 512 | power (human drivers) | -Words 7 and 11–15 aren't read. [Game logic](#game-logic) shows how drag, +Words 11–15 aren't read. [Game logic](#game-logic) shows how drag, grip and power work together. In short, top speed is `power × drag / (1 − drag)` game units per frame. For the Camaro that is 271 of this port's units a second for human drivers and 186 for computer @@ -226,6 +227,7 @@ For a preset `(a, t, g)`: - p = t/16 × (1/drag − 1), and power = p/8 - grip = g/256 / p / drag / 8 - steering = byte 6 / 32, gravity = 0.5 +- The car's size (words 6 and 7) still comes from its own settings. So top speed is simply t/128 game units per frame. @@ -300,6 +302,7 @@ Each car is a 266-byte (`$10a`) record: | `$a6` | engine speed | | `$aa` | forward speed | | `$b8`, `$bc`, `$e2`, `$e6`, `$106` | handling (above) | +| `$86` | active (negative) | | `$d2`, `$d6` | links to the car towing this one, and the one it tows | | `$ea` | zone (half-gate) progress | @@ -341,14 +344,77 @@ fits in one field. Then `$247ac` integrates every car. A warping car (state 1) instead flies straight at a point on the track: its velocity is the distance to it / 16. +Note that the heading vector points to the car's *rear*: pushing the +joystick forward gives a throttle of −1. This document flips that round +and talks about forward throttle as positive. + ### Computer drivers Computer cars use words 0, 1 and 5 for handling rather than 8, 9 and 10. -They steer with the sign of the cross product between their heading and -the direction to a target point, as if holding a joystick hard over. The -targets come from a table of points built along the road. The driver -picks one side of the road or the other depending on which way the road -bends, and the throttle comes from fixed values. + +**The path.** At set-up (`$1ea82` onwards) the game walks the middle of +the road, the average of the two edge curves. It drops a node every 2 +game units, adjusting each step by bisection until the gap squared is +between 3.8 and 4.2. Each 36-byte node (array `-$7e68(a5)`) holds: +- the centre point +- the unit vector across the road +- a point 1 unit in from each wall +- its position along the spline + +A second table maps each zone to its first node. + +**Each frame:** +1. The driver predicts its position four frames ahead, `pos + 4 × + velocity`. It finds the first node that point hasn't passed: only + nodes within −2 to +8 zones of the car count, and a node is passed + once the point is beyond the line across the road there. +2. Its default target is the centre of the node 4 on, at full throttle. +3. If the predicted point is beyond the line joining one side's inset + points at that node and the one before, it's running wide. It aims + at the other side's point 2 nodes on instead, at 0.7 throttle. The + constants are Motorola fast floating point values set at `$14886`: + 1.0 (negated), and that times `$b333`. +4. Steering is the sign of the cross product of its heading with the + direction from where it is now to the target, as if holding a + joystick hard over. + +### Collisions between cars + +Routine `$1b324` tests only the human players' cars. Each is tested +against every other active car and caravan except the caravan it tows. +So computer cars pass through each other and through caravans. + +For a pair (A = the player's car, B = the other): +1. **Proximity** (`$260ba`): skip unless |dx| ≤ 2 and |dy| ≤ 2 game units + and |dz| < 17 pixels. So cars on a bridge don't hit cars under it. +2. **Strength:** k = 64 × |velocity A − velocity B|² + 20, in game units + per frame. +3. **Points:** six points on A, rotated with A, come from a table built + for every heading out of words 6 and 7: the four corners, then one on + each side at 0.58 of the half-width. Each is tested against B's box, + which is the same for every vehicle: ±0.375 across and ±0.75 along + B's heading (`$25f3c`). +4. **Push:** for each point inside the box, B's velocity gains the + point's offset from A's centre × k / 256 and A's loses the same. The + side points use 2.5 k. +5. **Spin:** each corner inside the box also turns A by k / 16 (64ths of + a turn) on its turn rate, and twice that on its heading. The sign is + the one that swings that corner away from B. +6. **Sound:** any hit plays the crash sound for both cars. + +### Towing + +A caravan is a car record linked from its car (`$d6`), and it's dragged +in the per-car loop at `$1cf64`: +- **Hitch:** at the back of the car, half its length (word 6) behind the + centre. The game uses the car's position after this frame's move. +- **Swing:** the caravan turns to point at the hitch, and is placed 0.656 + game units behind it, by setting its velocity to the gap. +- **Jack-knife:** if the caravan ends up more than 90° from the car, both + are set to 45° either side of the angle halfway between them. So the + caravan steers the car too. +- **Warps:** when the car warps, its caravan is parked off the map until + it lands. ## Executables @@ -360,10 +426,7 @@ bends, and the throttle comes from fixed values. ## Gaps - **Game logic still to decode:** - - car-to-car collisions - - exactly how computer drivers pick target points and throttle - - the warp rules - - the towing physics, beyond the hitch distance + - the warp rules (when a car is warped back to the track) - the race rules and scoring - **Unknown data:** - the six per-track words after each track file's palette diff --git a/src/data/vehicle.rs b/src/data/vehicle.rs index 52b2189..bfea086 100644 --- a/src/data/vehicle.rs +++ b/src/data/vehicle.rs @@ -44,10 +44,10 @@ pub const SHEET_H: usize = SPR_H * TILTS; /// | 2 | steering rate | /// | 3 | read into the car but never used | /// | 4 | gravity | -/// | 6 | tow hitch distance behind the car | +/// | 6, 7 | half the car's length and width, for collisions; the tow hitch is at the back | /// | 8, 9, 10 | human drivers' drag, grip and power | /// -/// Words 7 and 11-15 aren't read. +/// Words 11-15 aren't read. pub type SetValues = [u16; 16]; /// The Camaro's values, for vehicles that come without any. @@ -80,15 +80,15 @@ pub struct Spec { pub turn: f32, /// Pixels per frame per frame. pub gravity: f32, - /// Tow hitch distance behind the car's centre, world units. - pub hitch: f32, - /// Weight in collisions between cars. Not from the original data. - pub mass: f32, + /// Half the car's length and width, world units. The tow hitch is + /// `half_length` behind the centre. + pub half_length: f32, + pub half_width: f32, } impl Spec { /// Decodes the values as the executable does (fixed-point shifts). - pub fn from_set(v: &SetValues, mass: f32) -> Spec { + pub fn from_set(v: &SetValues) -> Spec { let f = |k: usize, shift: u32| v[k] as i16 as f32 / (1u32 << shift) as f32; Spec { player: Handling { @@ -103,8 +103,8 @@ impl Spec { }, turn: f(2, 4), gravity: f(4, 3), - hitch: f(6, 5) * super::track::CELL, - mass, + half_length: f(6, 5) * super::track::CELL, + half_width: f(7, 5) * super::track::CELL, } } } @@ -153,20 +153,6 @@ impl Spec { } } -/// Collision weight. The original's car-to-car physics isn't decoded, so -/// these are guesses. -pub fn default_mass(id: &str) -> f32 { - match id { - "f1" => 0.9, - "mini" => 0.8, - "midget" => 0.85, - "truck" => 1.8, - "police" => 1.2, - "cow" => 1.4, - _ => 1.0, - } -} - pub struct Vehicle { pub id: String, pub name: String, @@ -212,7 +198,7 @@ impl Vehicle { trailer: id == "caravan", set: DEFAULT_SET, menu_picture: None, - spec: Spec::from_set(&DEFAULT_SET, default_mass(&id)), + spec: Spec::from_set(&DEFAULT_SET), id, frames, })) @@ -235,7 +221,7 @@ impl Vehicle { for (i, v) in self.set.iter_mut().enumerate() { *v = crate::formats::be16(body, i * 2); } - self.spec = Spec::from_set(&self.set, self.spec.mass); + self.spec = Spec::from_set(&self.set); let pic = &body[32..32 + 512]; let mut px = vec![0u8; SPR_W * SPR_H]; for (i, p) in px.iter_mut().enumerate() { @@ -293,7 +279,7 @@ impl Vehicle { trailer: id == "caravan", set: DEFAULT_SET, menu_picture: None, - spec: Spec::from_set(&DEFAULT_SET, default_mass(id)), + spec: Spec::from_set(&DEFAULT_SET), frames, }; if let Some(txt) = txt { @@ -329,12 +315,11 @@ impl Vehicle { # original game does. Counting from 1:\n\ # 1 2 6 computer drivers: drag, grip, power\n\ # 9 10 11 human drivers: drag, grip, power\n\ - # 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused.\n\ + # 3 steering, 5 gravity, 7 and 8 half the car's length and width\n\ + # (for collisions; the tow hitch is at the back). The rest are unused.\n\ # Top speed = power * drag / (64 - drag) / 32 world units per frame,\n\ # 50 frames a second (drag below 64). Higher grip, less sliding.\n\ - handling {set}\n\ - # weight when cars collide (not from the original data)\n\ - mass {mass}\n", + handling {set}\n", id = self.id, name = self.name, driver = self.driver, @@ -345,7 +330,6 @@ impl Vehicle { .map(|v| v.to_string()) .collect::>() .join(" "), - mass = self.spec.mass, ) } @@ -375,18 +359,12 @@ impl Vehicle { self.set = [0; 16]; self.set[..vals.len()].copy_from_slice(&vals); } - "mass" => { - self.spec.mass = val - .parse::() - .map_err(|_| bad(&format!("`{val}` is not a number")))? - .max(0.05) - } // Settings from before the original handling was decoded. - "top" | "accel" | "brake" | "grip" | "turn" | "offroad" => {} + "top" | "accel" | "brake" | "grip" | "turn" | "offroad" | "mass" => {} _ => return Err(bad(&format!("unknown setting `{key}`"))), } } - self.spec = Spec::from_set(&self.set, self.spec.mass); + self.spec = Spec::from_set(&self.set); Ok(()) } @@ -451,13 +429,13 @@ mod tests { trailer: false, set: DEFAULT_SET, menu_picture: None, - spec: Spec::from_set(&DEFAULT_SET, 1.0), + spec: Spec::from_set(&DEFAULT_SET), frames, }; let mut v = blank(vec![0; TILTS * HEADINGS * SPR_W * SPR_H]); v.driver = "SIMON".into(); v.set = [60, 71, 30, 0, 6, 16, 24, 12, 58, 42, 29, 0, 0, 0, 0, 0]; - v.spec = Spec::from_set(&v.set, 0.9); + v.spec = Spec::from_set(&v.set); v.name = "FORMULA 1".into(); let txt = v.settings_text(); let mut w = blank(Vec::new()); @@ -466,7 +444,7 @@ mod tests { assert_eq!(w.name, "FORMULA 1"); assert_eq!(w.driver, "SIMON"); assert_eq!(w.set, v.set); - assert!(w.apply_settings("mass heavy").is_err()); + assert!(w.apply_settings("handling 1 x").is_err()); // Older files with reconstructed handling still load. assert!(w.apply_settings("original 1 2 3\ntop 200").is_ok()); assert_eq!(&w.set[..4], &[1, 2, 3, 0]); @@ -474,11 +452,11 @@ mod tests { #[test] fn decodes_set_values() { - let s = Spec::from_set(&DEFAULT_SET, 1.0); + let s = Spec::from_set(&DEFAULT_SET); assert_eq!(s.player.drag, 49.0 / 64.0); assert_eq!(s.computer.power, 22.0 / 512.0); assert_eq!(s.turn, 2.0); - assert_eq!(s.hitch, 11.0); + assert_eq!((s.half_length, s.half_width), (11.0, 8.0)); let p = s.with_preset(1); let top = p.player.power * p.player.drag / (1.0 - p.player.drag); assert!((top - 26.0 / 128.0).abs() < 1e-5); diff --git a/src/game/ai.rs b/src/game/ai.rs index 48ce4b7..77d92c1 100644 --- a/src/game/ai.rs +++ b/src/game/ai.rs @@ -1,81 +1,165 @@ -//! Computer drivers. +//! Computer drivers, as decoded from the executable's move routine. //! -//! As in the executable, a computer car steers like a joystick: full lock -//! towards a target point ahead, or straight on when it is dead ahead, and -//! drives flat out. The cars' computer handling (`vehicle::Handling`) is -//! what makes them take bends at that. The target is a point a little way -//! along the road (`Track::edges`) at the driver's own line across it. A -//! driver that stops moving backs out. +//! At set-up the game lays a path of nodes along the middle of the road, +//! 2 game units (32 world units) apart. Each node also has a point on +//! either side, 1 game unit in from the wall. Each frame a computer +//! driver: +//! +//! 1. predicts where it will be in four frames, and finds the first node +//! that point hasn't reached; +//! 2. aims at the middle of the road four nodes on, at full throttle; +//! 3. but if the predicted point is beyond the line joining the side +//! points of that node and the one before (running wide on one side), +//! aims at the other side's point two nodes on, at 70% throttle; +//! 4. steers like a joystick: full lock towards the target, from where it +//! is now. +//! +//! The cars' computer handling (`vehicle::Handling`) is what lets them take +//! the bends at that. On top of the original, a driver that stops moving +//! backs out. -use super::car::{angle_diff, Car, Controls}; -use crate::data::track::Track; +use super::car::{angle_diff, Car, Controls, DT}; +use crate::data::track::Gate; -/// Heading error, in radians, below which the driver doesn't steer. -const DEAD_AHEAD: f32 = 0.06; +/// Nodes ahead for the usual target, and for the target across the road. +const AHEAD: usize = 4; +const ACROSS_AHEAD: usize = 2; +/// Throttle while cutting across the road. +const ACROSS_THROTTLE: f32 = 0.7; +/// Distance between nodes, and from each wall to the side points. +const NODE_SPACING: f32 = 32.0; +const SIDE_INSET: f32 = 16.0; -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] +pub struct Node { + pub centre: (f32, f32), + /// The side points, near edge `a` and near edge `b` of the road. + pub side_a: (f32, f32), + pub side_b: (f32, f32), +} + +/// The computer drivers' path round the road. +#[derive(Clone, Debug, Default)] +pub struct Path { + pub nodes: Vec, + /// For each road cross-section (`Track::edges`), the first node at or + /// after it. + first: Vec, +} + +impl Path { + pub fn new(edges: &[Gate]) -> Path { + let m = edges.len(); + let mut path = Path::default(); + if m < 3 { + return path; + } + let mut first = vec![usize::MAX; m]; + // Distance along the middle of the road to the next node. + let mut to_next = 0.0f32; + for k in 0..m { + let (g, h) = (edges[k], edges[(k + 1) % m]); + let (p, q) = (g.mid(), h.mid()); + let len = (q.0 - p.0).hypot(q.1 - p.1); + let mut at = to_next; + while at < len { + let f = at / len.max(1e-3); + let lerp = + |u: (f32, f32), v: (f32, f32)| (u.0 + (v.0 - u.0) * f, u.1 + (v.1 - u.1) * f); + let (a, b) = (lerp(g.a, h.a), lerp(g.b, h.b)); + let width = (b.0 - a.0).hypot(b.1 - a.1).max(1e-3); + let inset = SIDE_INSET.min(width * 0.5); + let (ux, uy) = ((b.0 - a.0) / width, (b.1 - a.1) / width); + if first[k] == usize::MAX { + first[k] = path.nodes.len(); + } + path.nodes.push(Node { + centre: lerp(p, q), + side_a: (a.0 + ux * inset, a.1 + uy * inset), + side_b: (b.0 - ux * inset, b.1 - uy * inset), + }); + at += NODE_SPACING; + } + to_next = at - len; + } + // Sections without a node of their own point at the next one. + let mut next = 0; + for k in (0..m).rev() { + if first[k] == usize::MAX { + first[k] = next; + } else { + next = first[k]; + } + } + path.first = first; + path + } + + fn node(&self, i: usize) -> Node { + self.nodes[i % self.nodes.len()] + } + + /// Whether `p` has reached node `i`: it is past the line across the + /// road there. + fn reached(&self, i: usize, p: (f32, f32)) -> bool { + let (here, next) = (self.node(i), self.node(i + 1)); + let (fx, fy) = (next.centre.0 - here.centre.0, next.centre.1 - here.centre.1); + (p.0 - here.centre.0) * fx + (p.1 - here.centre.1) * fy > 0.0 + } +} + +#[derive(Clone, Debug, Default)] pub struct Ai { - /// Where across the road this driver aims (0 = one edge, 1 = the other). - pub line: f32, - /// Fraction of full throttle this driver uses. - pub pace: f32, stuck_for: f32, reversing: f32, } impl Ai { - pub fn new(line: f32, pace: f32) -> Ai { - Ai { - line, - pace, - stuck_for: 0.0, - reversing: 0.0, - } + pub fn new() -> Ai { + Ai::default() } - /// The point `dist` along the road from the car's cross-section, at - /// this driver's line. - pub fn target(&self, car: &Car, track: &Track, dist: f32) -> (f32, f32) { - let e = &track.edges; - let m = e.len(); - let at = |k: usize| { - let g = e[k % m]; - ( - g.a.0 + (g.b.0 - g.a.0) * self.line, - g.a.1 + (g.b.1 - g.a.1) * self.line, - ) - }; - let mut k = car.section + 1; - let mut p = at(k); - let mut left = dist - (p.0 - car.x).hypot(p.1 - car.y); - while left > 0.0 && k < car.section + m { - let q = at(k + 1); - let len = (q.0 - p.0).hypot(q.1 - p.1); - if len >= left { - let f = left / len; - return (p.0 + (q.0 - p.0) * f, p.1 + (q.1 - p.1) * f); + /// Where the driver aims, and how much throttle it uses. + pub fn target(&self, car: &Car, path: &Path) -> ((f32, f32), f32) { + let p = (car.x + 4.0 * car.vx * DT, car.y + 4.0 * car.vy * DT); + let n = path.nodes.len(); + // Start from the car's own place on the road (offset by a lap so + // stepping back can't underflow), then find the first node the + // predicted point hasn't reached. + let mut i = path.first.get(car.section).copied().unwrap_or(0) + n; + for _ in 0..2 { + if path.reached(i - 1, p) { + break; } - left -= len; - p = q; - k += 1; + i -= 1; } - p + for _ in 0..8 { + if !path.reached(i, p) { + break; + } + i += 1; + } + let (prev, here) = (path.node(i - 1), path.node(i)); + let across = path.node(i + ACROSS_AHEAD); + let mut aim = (path.node(i + AHEAD).centre, 1.0); + if beyond(prev.side_a, here.side_a, here.side_b, p) { + aim = (across.side_b, ACROSS_THROTTLE); + } + if beyond(prev.side_b, here.side_b, here.side_a, p) { + aim = (across.side_a, ACROSS_THROTTLE); + } + aim } - pub fn drive(&mut self, car: &Car, track: &Track, dt: f32, racing: bool) -> Controls { - if !racing || track.edges.len() < 3 { + pub fn drive(&mut self, car: &Car, path: &Path, dt: f32, racing: bool) -> Controls { + if !racing || path.nodes.len() < AHEAD + 2 { return Controls::default(); } - let speed = car.speed(); - let look = 30.0 + speed * 0.2; - let (tx, ty) = self.target(car, track, look); + let ((tx, ty), throttle) = self.target(car, path); let diff = angle_diff((ty - car.y).atan2(tx - car.x), car.heading); - let steer = if diff.abs() < DEAD_AHEAD { - 0.0 - } else { - diff.signum() - }; + let steer = if diff == 0.0 { 0.0 } else { diff.signum() }; + let speed = car.speed(); if self.reversing > 0.0 { self.reversing -= dt; return Controls { @@ -93,18 +177,39 @@ impl Ai { } else { self.stuck_for = 0.0; } - if diff.abs() > 2.3 && speed < 35.0 { - // Facing the wrong way: reverse-turn round. - return Controls { - throttle: 0.0, - brake: 1.0, - steer: -steer, - }; - } Controls { - throttle: self.pace, + throttle, brake: 0.0, steer, } } } + +/// Whether `p` is on the far side of the line `a`-`b` from `inside`. +fn beyond(a: (f32, f32), b: (f32, f32), inside: (f32, f32), p: (f32, f32)) -> bool { + let side = |q: (f32, f32)| (b.0 - a.0) * (q.1 - a.1) - (b.1 - a.1) * (q.0 - a.0); + side(p) * side(inside) < 0.0 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn path_nodes_are_evenly_spaced_and_inset() { + // A straight road 40 units wide along x, as a loop of sections. + let edges: Vec = (0..40) + .map(|i| Gate { + a: (i as f32 * 10.0, 0.0), + b: (i as f32 * 10.0, 40.0), + }) + .collect(); + let path = Path::new(&edges[..20]); + let n = &path.nodes; + assert!(n.len() >= 5); + let d = (n[1].centre.0 - n[0].centre.0).hypot(n[1].centre.1 - n[0].centre.1); + assert!((d - NODE_SPACING).abs() < 1e-3); + assert_eq!(n[0].side_a, (0.0, 16.0)); + assert_eq!(n[0].side_b, (0.0, 24.0)); + } +} diff --git a/src/game/car.rs b/src/game/car.rs index a5de1e7..e614049 100644 --- a/src/game/car.rs +++ b/src/game/car.rs @@ -17,16 +17,16 @@ //! - **Walls:** the road edges (`Track::edges`) are walls. A car that //! reaches one loses the part of its velocity into the wall and bounces //! back with half of it. +//! - **Other cars:** see `bump`. //! //! Human and computer drivers use separate handling from the vehicle's //! `.set` values. -use super::DT; +pub use super::DT; use crate::data::track::{self, Gate, Track, CELL}; use crate::data::vehicle::{self, Handling, Spec}; use std::f32::consts::{PI, TAU}; -pub const RADIUS: f32 = 11.0; /// How close the car's centre may come to a wall, in world units. const WALL_MARGIN: f32 = 4.0; /// The executable samples the slope 0.2 cells from the car. @@ -335,41 +335,95 @@ pub fn nearest_section(e: &[Gate], p: (f32, f32)) -> usize { } } -/// Pushes overlapping cars apart and exchanges momentum. -pub fn collide(a: &mut Car, b: &mut Car) { - let (dx, dy) = (b.x - a.x, b.y - a.y); - let d2 = dx * dx + dy * dy; - let min = RADIUS * 2.0; - if d2 >= min * min || d2 < 1e-6 { - return; - } - let d = d2.sqrt(); - let (nx, ny) = (dx / d, dy / d); - let overlap = min - d; - let (ia, ib) = (1.0 / a.spec.mass, 1.0 / b.spec.mass); - let push = overlap / (ia + ib); - a.x -= nx * push * ia; - a.y -= ny * push * ia; - b.x += nx * push * ib; - b.y += ny * push * ib; +/// Something a car can run into: another car or a caravan. +#[derive(Clone, Copy, Debug)] +pub struct Body { + pub x: f32, + pub y: f32, + pub z: f32, + pub vx: f32, + pub vy: f32, + pub heading: f32, +} - let vn = (b.vx - a.vx) * nx + (b.vy - a.vy) * ny; - if vn < 0.0 { - let j = -(1.0 + 0.45) * vn / (ia + ib); - a.vx -= j * ia * nx; - a.vy -= j * ia * ny; - b.vx += j * ib * nx; - b.vy += j * ib * ny; - let hit = (-vn / 150.0).min(1.0); - a.bump = a.bump.max(hit); - b.bump = b.bump.max(hit); - // A glancing knock spins the cars a little. - let spin = (nx * a.forward().1 - ny * a.forward().0) * hit * 0.25; - a.heading = (a.heading + spin).rem_euclid(TAU); - b.heading = (b.heading - spin).rem_euclid(TAU); +impl Car { + pub fn body(&self) -> Body { + Body { + x: self.x, + y: self.y, + z: self.z, + vx: self.vx, + vy: self.vy, + heading: self.heading, + } } } +/// Half-width and half-length of the box that `bump` tests points against, +/// world units. The executable uses the same box for every vehicle. +const TARGET: (f32, f32) = (6.0, 12.0); + +/// The executable's collision (`$25e64`) between car `a` and `b`. +/// +/// Six points on `a` are tested against `b`'s box: its four corners and a +/// point on each side, 0.58 of the way out. Each point inside the box +/// pushes the two apart along that point's offset from `a`'s centre, by +/// k = 64 |relative speed|^2 + 20 (game units); the side points push 2.5 +/// times as hard. Each corner inside also spins `a` so that corner swings +/// away. Returns the velocity change for `b` (world units per second); +/// `a` has already received the opposite. +pub fn bump(a: &mut Car, b: &Body) -> Option<(f32, f32)> { + // Cars more than two cells apart, or at different heights (on and + // under a bridge), can't touch. + if (a.x - b.x).abs() > 2.0 * CELL || (a.y - b.y).abs() > 2.0 * CELL || (a.z - b.z).abs() >= 17.0 + { + return None; + } + let (dvx, dvy) = ((a.vx - b.vx) * DT / CELL, (a.vy - b.vy) * DT / CELL); + let k = 64.0 * (dvx * dvx + dvy * dvy) + 20.0; + let (fx, fy) = a.forward(); + let (ux, uy) = (-fy, fx); + let (bfx, bfy) = (b.heading.cos(), b.heading.sin()); + let (l, w) = (a.spec.half_length, a.spec.half_width); + let side = w * 0.58; + // (along, across, push factor) + let points = [ + (l, w, 1.0), + (l, -w, 1.0), + (-l, -w, 1.0), + (-l, w, 1.0), + (0.0, side, 2.5), + (0.0, -side, 2.5), + ]; + let (mut ix, mut iy, mut turn, mut hit) = (0.0, 0.0, 0.0, false); + for (along, across, factor) in points { + let (px, py) = (fx * along + ux * across, fy * along + uy * across); + let (rx, ry) = (a.x + px - b.x, a.y + py - b.y); + let b_along = rx * bfx + ry * bfy; + let b_across = ry * bfx - rx * bfy; + if b_across.abs() >= TARGET.0 || b_along.abs() >= TARGET.1 { + continue; + } + hit = true; + let push = k * factor / 256.0 / DT; + ix += px * push; + iy += py * push; + if factor == 1.0 { + turn -= (along * across).signum() * k / 16.0; + } + } + if !hit { + return None; + } + a.vx -= ix; + a.vy -= iy; + a.spin += turn; + a.heading = (a.heading + 2.0 * turn * TAU / 64.0).rem_euclid(TAU); + let knock = (dvx.hypot(dvy) * CELL / DT / 150.0).clamp(0.2, 1.0); + a.bump = a.bump.max(knock); + Some((ix, iy)) +} + pub fn angle_diff(to: f32, from: f32) -> f32 { (to - from + PI).rem_euclid(TAU) - PI } diff --git a/src/game/mod.rs b/src/game/mod.rs index cbe3f50..e18266f 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -5,8 +5,8 @@ pub mod car; use crate::data::track::{Gate, Track}; use crate::data::vehicle::Vehicle; -use ai::Ai; -use car::{Car, Controls}; +use ai::{Ai, Path}; +use car::{Body, Car, Controls}; pub const DT: f32 = 1.0 / 50.0; const COUNTDOWN: f32 = 3.0; @@ -43,13 +43,18 @@ pub struct Trailer { pub vehicle: usize, pub x: f32, pub y: f32, + /// World units per second, from how far it was dragged this frame. + pub vx: f32, + pub vy: f32, + /// Pointing at the hitch. pub heading: f32, } -/// Tow bar length from the car's hitch (`Spec::hitch`) to the trailer's -/// centre, in world units. -const TOW_BAR: f32 = 20.0; -const TRAILER_RADIUS: f32 = 10.0; +/// From the car's hitch (at the back of the car, `Spec::half_length` +/// behind its centre) to the trailer's centre: 0.656 game units. +const TOW_BAR: f32 = 10.5; +/// The most a trailer can swing round from its car before both bend. +const JACKKNIFE: f32 = std::f32::consts::FRAC_PI_2; /// Seconds without crossing a gate before a computer car is rescued. const RESCUE_AFTER: f32 = 8.0; @@ -65,7 +70,7 @@ fn rescue(e: &mut Entrant, track: &Track) { c.heading = heading.rem_euclid(std::f32::consts::TAU); c.place(track); if let Some(t) = &mut e.trailer { - let back = c.spec.hitch + TOW_BAR; + let back = c.spec.half_length + TOW_BAR; t.x = from.0 - heading.cos() * back; t.y = from.1 - heading.sin() * back; t.heading = heading; @@ -73,16 +78,40 @@ fn rescue(e: &mut Entrant, track: &Track) { } impl Trailer { - /// Drags the trailer after its car: it keeps its distance from the - /// hitch and swings to point at it. - fn follow(&mut self, car: &Car) { + /// Drags the trailer after its car, as the executable does: it turns + /// to point at the hitch and sits a tow bar's length behind it. If it + /// ends up more than 90 degrees from the car, both are bent back to 45 + /// degrees either side of the angle between them. + fn follow(&mut self, car: &mut Car) { let (fx, fy) = car.forward(); - let (hx, hy) = (car.x - fx * car.spec.hitch, car.y - fy * car.spec.hitch); - let (dx, dy) = (self.x - hx, self.y - hy); - let d = dx.hypot(dy).max(1e-3); - self.x = hx + dx / d * TOW_BAR; - self.y = hy + dy / d * TOW_BAR; + let (hx, hy) = ( + car.x - fx * car.spec.half_length, + car.y - fy * car.spec.half_length, + ); self.heading = (hy - self.y).atan2(hx - self.x); + let d = car::angle_diff(self.heading, car.heading); + if d.abs() > JACKKNIFE { + let mid = car.heading + d * 0.5; + let half = JACKKNIFE * 0.5 * d.signum(); + self.heading = mid + half; + car.heading = (mid - half).rem_euclid(std::f32::consts::TAU); + } + let (ox, oy) = (self.x, self.y); + self.x = hx - self.heading.cos() * TOW_BAR; + self.y = hy - self.heading.sin() * TOW_BAR; + self.vx = (self.x - ox) / DT; + self.vy = (self.y - oy) / DT; + } + + fn body(&self, track: &Track) -> Body { + Body { + x: self.x, + y: self.y, + z: track.ground(self.x, self.y), + vx: self.vx, + vy: self.vy, + heading: self.heading, + } } } @@ -101,6 +130,8 @@ pub enum Phase { pub struct Race { pub track: Track, + /// The computer drivers' path. + pub path: Path, pub entrants: Vec, pub laps: usize, pub time: f32, @@ -146,7 +177,7 @@ impl Race { let start_gate = 0; let path: Vec<(f32, f32)> = gates.iter().map(Gate::mid).collect(); - let mut rng = seed | 1; + let rng = seed | 1; let mut entrants = Vec::new(); for (i, s) in setup.iter().enumerate() { // Two-wide grid, rows 30 units apart, following the road back @@ -156,7 +187,7 @@ impl Race { // Leave room for a caravan between rows when towing. let spec = vehicles[s.vehicle].spec.with_preset(s.preset); let spacing = if s.trailer.is_some() { - 30.0 + spec.hitch + TOW_BAR + TRAILER_RADIUS + 30.0 + spec.half_length + TOW_BAR + 10.0 } else { 30.0 }; @@ -166,12 +197,10 @@ impl Race { let mut car = Car::new(s.vehicle, spec, s.team, x, y, heading); car.computer = s.driver == Driver::Cpu; car.place(&track); - let line = 0.3 + 0.4 * rand01(&mut rng); - let pace = 0.86 + 0.12 * rand01(&mut rng); entrants.push(Entrant { car, driver: s.driver, - ai: Ai::new(line, pace), + ai: Ai::new(), name: s.name.clone(), passed: 0, next_gate: start_gate, @@ -182,11 +211,13 @@ impl Race { progress_at: 0.0, trailer: s.trailer.map(|t| { let (fx, fy) = (heading.cos(), heading.sin()); - let back = spec.hitch + TOW_BAR; + let back = spec.half_length + TOW_BAR; Trailer { vehicle: t, x: x - fx * back, y: y - fy * back, + vx: 0.0, + vy: 0.0, heading, } }), @@ -194,6 +225,7 @@ impl Race { } let ground = track.rgb(); Race { + path: Path::new(&track.edges), track, entrants, laps, @@ -235,10 +267,10 @@ impl Race { for e in self.entrants.iter_mut() { let controls = match (e.driver, e.finished) { - (_, Some(_)) => e.ai.drive(&e.car, &self.track, DT, racing), + (_, Some(_)) => e.ai.drive(&e.car, &self.path, DT, racing), (Driver::Human(p), None) if racing => human.get(p).copied().unwrap_or_default(), (Driver::Human(_), None) => Controls::default(), - (Driver::Cpu, None) => e.ai.drive(&e.car, &self.track, DT, racing), + (Driver::Cpu, None) => e.ai.drive(&e.car, &self.path, DT, racing), }; e.car.controls = controls; // Finished cars cruise round on autopilot at reduced pace. @@ -246,24 +278,34 @@ impl Race { e.car.step(&self.track, pace); } - for i in 0..self.entrants.len() { - for j in i + 1..self.entrants.len() { - let (a, b) = self.entrants.split_at_mut(j); - car::collide(&mut a[i].car, &mut b[0].car); + for e in self.entrants.iter_mut() { + if let Some(t) = &mut e.trailer { + t.follow(&mut e.car); } } - // Caravans follow their cars and shove other cars out of the way. + // As in the executable, only the human players' cars collide: each + // is tested against every other car and caravan but its own. + // Computer cars pass through each other and through caravans. for i in 0..self.entrants.len() { - let Some(mut t) = self.entrants[i].trailer else { + if !matches!(self.entrants[i].driver, Driver::Human(_)) { continue; - }; - t.follow(&self.entrants[i].car); - for (j, other) in self.entrants.iter_mut().enumerate() { - if j != i { - push_off(&mut other.car, t.x, t.y); + } + for j in 0..self.entrants.len() { + if j == i { + continue; + } + let other = self.entrants[j].car.body(); + if let Some((dx, dy)) = car::bump(&mut self.entrants[i].car, &other) { + let knock = self.entrants[i].car.bump; + let b = &mut self.entrants[j].car; + b.vx += dx; + b.vy += dy; + b.bump = b.bump.max(knock); + } + if let Some(t) = self.entrants[j].trailer { + car::bump(&mut self.entrants[i].car, &t.body(&self.track)); } } - self.entrants[i].trailer = Some(t); } for (e, &(px, py)) in self.entrants.iter_mut().zip(&starts) { @@ -385,26 +427,6 @@ impl Car { } } -/// Moves a car out of a caravan it has run into, killing the speed it had -/// into it. -fn push_off(c: &mut Car, tx: f32, ty: f32) { - let (dx, dy) = (c.x - tx, c.y - ty); - let d = dx.hypot(dy); - let min = car::RADIUS + TRAILER_RADIUS; - if d >= min || d < 1e-3 { - return; - } - let (nx, ny) = (dx / d, dy / d); - c.x += nx * (min - d); - c.y += ny * (min - d); - let vn = c.vx * nx + c.vy * ny; - if vn < 0.0 { - c.vx -= vn * nx * 1.3; - c.vy -= vn * ny * 1.3; - c.bump = c.bump.max((-vn / 150.0).min(1.0)); - } -} - fn burn(ground: &mut [u32], sx: f32, sy: f32, strength: f32) { use crate::data::track::{H, W}; let (x, y) = (sx as isize, sy as isize); @@ -511,7 +533,7 @@ mod tests { start: (0.0, 0.0), flags: 0, }; - let spec = crate::data::vehicle::Spec::from_set(&crate::data::vehicle::DEFAULT_SET, 1.0); + let spec = crate::data::vehicle::Spec::from_set(&crate::data::vehicle::DEFAULT_SET); let mut c = Car::new(0, spec, 0, 512.0 + 180.0, 512.0, 0.0); c.place(&track); // Flat out in a straight line: the outer wall turns the car back. @@ -522,4 +544,48 @@ mod tests { assert!(r > 140.0 && r < 215.0, "left the road at radius {r}"); } } + + fn test_car(x: f32, y: f32, heading: f32) -> Car { + let spec = crate::data::vehicle::Spec::from_set(&crate::data::vehicle::DEFAULT_SET); + Car::new(0, spec, 0, x, y, heading) + } + + #[test] + fn bump_pushes_cars_apart() { + // Side by side, overlapping: b is pushed away along +y, a along -y. + let mut a = test_car(100.0, 100.0, 0.0); + let b = test_car(100.0, 110.0, 0.0); + let (dx, dy) = car::bump(&mut a, &b.body()).expect("they overlap"); + assert!(dy > 0.0 && dx.abs() < 1e-3); + assert!(a.vy < 0.0); + // Too far apart, or one on a bridge above the other. + let mut a = test_car(100.0, 100.0, 0.0); + assert!(car::bump(&mut a, &test_car(100.0, 140.0, 0.0).body()).is_none()); + let mut high = test_car(100.0, 110.0, 0.0); + high.z = 20.0; + assert!(car::bump(&mut a, &high.body()).is_none()); + } + + #[test] + fn trailer_jackknife_bends_both() { + let mut c = test_car(100.0, 100.0, 0.0); + // A trailer straight out to the side of the car. + let mut t = Trailer { + vehicle: 0, + x: 89.0, + y: 130.0, + vx: 0.0, + vy: 0.0, + heading: 0.0, + }; + t.follow(&mut c); + let d = car::angle_diff(t.heading, c.heading).abs(); + assert!((d - JACKKNIFE).abs() < 1e-4, "{d}"); + // It sits a tow bar's length from the hitch. + let (hx, hy) = ( + c.x - c.heading.cos() * c.spec.half_length, + c.y - c.heading.sin() * c.spec.half_length, + ); + assert!(((t.x - hx).hypot(t.y - hy) - TOW_BAR).abs() < 1e-3); + } } diff --git a/src/main.rs b/src/main.rs index 57bd658..8365026 100644 --- a/src/main.rs +++ b/src/main.rs @@ -411,7 +411,7 @@ fn render_shot(data: GameData, args: &Args) -> Result<(), String> { .entrants .iter_mut() .filter(|e| matches!(e.driver, game::Driver::Human(_))) - .map(|e| e.ai.drive(&e.car, &race.track, game::DT, true)) + .map(|e| e.ai.drive(&e.car, &race.path, game::DT, true)) .collect(); race.step(&controls); app::draw_race(&mut frame, &app.data, &race, &mut cams, players);