Merge pull request #6 from andydixon/ai-collisions-towing
Use the original computer drivers, car collisions and towing
This commit is contained in:
@@ -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
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+86
-32
@@ -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,40 +335,94 @@ 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;
|
||||
/// 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 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;
|
||||
|
||||
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
|
||||
|
||||
+122
-56
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user