Merge pull request #6 from andydixon/ai-collisions-towing

Use the original computer drivers, car collisions and towing
This commit is contained in:
Andy Dixon
2026-10-20 11:38:49 +01:00
committed by GitHub
18 changed files with 511 additions and 253 deletions
+8 -5
View File
@@ -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.
+5 -6
View File
@@ -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 `<id>.png` / `<id>.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 `<id>.png` / `<id>.txt` pair.
## Sounds: `sounds/*.wav`
+2 -3
View File
@@ -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
+2 -3
View File
@@ -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
+2 -3
View File
@@ -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
+2 -3
View File
@@ -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
+2 -3
View File
@@ -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
+2 -3
View File
@@ -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
+2 -3
View File
@@ -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
+2 -3
View File
@@ -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
+2 -3
View File
@@ -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
+2 -3
View File
@@ -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
+74 -11
View File
@@ -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
+22 -44
View File
@@ -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::<Vec<_>>()
.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::<f32>()
.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);
+172 -67
View File
@@ -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<Node>,
/// For each road cross-section (`Track::edges`), the first node at or
/// after it.
first: Vec<usize>,
}
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<Gate> = (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));
}
}
+87 -33
View File
@@ -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,39 +335,93 @@ 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 {
+122 -56
View File
@@ -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<Entrant>,
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);
}
}
+1 -1
View File
@@ -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);