1510 lines
46 KiB
Markdown
1510 lines
46 KiB
Markdown
|
||
#### Failure 1: Competitive Multiplayer Butterfly Effect (StarCraft AI Desync)
|
||
|
||
**Scenario**: Competitive RTS with 16 deterministic units per player. Same simulation, same inputs, same 60fps.
|
||
|
||
**What They Did**:
|
||
```cpp
|
||
// Deterministic fixed-timestep physics
|
||
void update_unit(Unit& u, float dt = 1/60.0f) {
|
||
u.velocity += u.acceleration * dt;
|
||
u.position += u.velocity * dt;
|
||
|
||
// Collision response
|
||
for(Unit& other : nearby_units) {
|
||
if(distance(u, other) < collision_radius) {
|
||
u.velocity = bounce(u.velocity, other.velocity);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Deterministic pathfinding
|
||
update_all_units(dt);
|
||
```
|
||
|
||
**What Went Wrong**:
|
||
- Player A: units move in specific pattern, collision happens at frame 4523
|
||
- Player B: units move identically, collision at frame 4523
|
||
- Player C (watching both): sees desync at frame 4525
|
||
- Floating-point rounding: 0.999999 vs 1.000001 unit positions
|
||
- Collision check: `distance < 1.0` is true on one machine, false on another
|
||
- Unit velocities diverge by 0.0001 per collision
|
||
- At frame 5000: positions differ by 0.5 units
|
||
- At frame 6000: completely different unit formations
|
||
- One player sees enemy army, other sees it 2 tiles away
|
||
- Multiplayer match becomes unplayable
|
||
|
||
**Why No One Predicted It**:
|
||
- "It's deterministic" ≠ "It stays synchronized"
|
||
- Determinism + floating-point arithmetic = butterfly effect
|
||
- Tiny initial differences amplify every frame
|
||
- No sensitivity analysis of physics system
|
||
|
||
**What Chaos Analysis Would Have Shown**:
|
||
```
|
||
Unit collision system is CHAOTIC:
|
||
- Two trajectories, separated by ε = 10^-6 in initial position
|
||
- After 1000 frames: separation grows to ε' ≈ 0.001
|
||
- After 2000 frames: separation ≈ 0.1 (units in different tiles)
|
||
- After 3000 frames: separation ≈ 1.0 (different formations)
|
||
|
||
Lyapunov exponent λ ≈ 0.0001 per frame
|
||
→ divergence rate: ε(t) ≈ ε₀ * e^(λ*t)
|
||
→ after t=4000 frames, initial error of 10^-6 grows to 10^0
|
||
|
||
Deterministic ≠ Synchronizable without exact state transmission
|
||
```
|
||
|
||
|
||
#### Failure 2: Weather Simulation Diverges Instantly (Climatebase Forecast Mismatch)
|
||
|
||
**Scenario**: Procedural world generation using weather simulation. Two servers, same world seed.
|
||
|
||
**What They Did**:
|
||
```python
|
||
# Lorenz equations for atmospheric convection (simplified weather)
|
||
def weather_update(x, y, z, dt=0.01):
|
||
sigma, rho, beta = 10.0, 28.0, 8/3
|
||
dx = sigma * (y - x)
|
||
dy = x * (rho - z) - y
|
||
dz = x * y - beta * z
|
||
|
||
x_new = x + dx * dt
|
||
y_new = y + dy * dt
|
||
z_new = z + dz * dt
|
||
return x_new, y_new, z_new
|
||
|
||
# Same seed on both servers
|
||
x, y, z = 1.0, 1.0, 1.0
|
||
for frame in range(10000):
|
||
x, y, z = weather_update(x, y, z)
|
||
broadcast_weather(x, y, z)
|
||
```
|
||
|
||
**What Went Wrong**:
|
||
- Server A: float precision = IEEE 754 single
|
||
- Server B: double precision for intermediate calculations
|
||
- Frame 1: identical results
|
||
- Frame 10: difference in 7th decimal place
|
||
- Frame 100: difference in 3rd decimal place
|
||
- Frame 500: temperature differs by 2 degrees
|
||
- Frame 1000: completely different storm patterns
|
||
- Players on different servers experience different weather
|
||
- Crops die in one region, thrive in another
|
||
- Economy becomes unbalanced
|
||
- "Bug reports" flood in: "My farm is flooded but my friend's isn't"
|
||
|
||
**Why No One Predicted It**:
|
||
- Assumed: "same seed = same weather"
|
||
- The Lorenz system has Lyapunov exponent λ ≈ 0.9 (highly chaotic)
|
||
- Even 10^-7 precision differences grow to 1.0 in ~40 timesteps
|
||
- No sensitivity testing across platforms/compilers
|
||
|
||
**What Chaos Analysis Would Have Shown**:
|
||
```
|
||
Lorenz system (ρ=28, σ=10, β=8/3):
|
||
Lyapunov exponents: [0.906, 0, -14.572]
|
||
→ System is CHAOTIC (largest exponent > 0)
|
||
→ Initial separation grows as ε(t) ≈ ε₀ * e^(0.906 * t)
|
||
|
||
With ε₀ = 10^-7 (single vs double precision):
|
||
At t = 16: ε(t) ≈ 10^-5 (measurable difference)
|
||
At t = 40: ε(t) ≈ 1.0 (completely different trajectory)
|
||
|
||
Synchronization window: ~30 timesteps before divergence
|
||
Solution: Broadcast full state every 20 frames, not just seed
|
||
```
|
||
|
||
|
||
#### Failure 3: Procedural Generation Varies Per Machine (Minecraft Performance Island)
|
||
|
||
**Scenario**: Procedural terrain generation using noise-based chaos. Players with different hardware see different terrain.
|
||
|
||
**What They Did**:
|
||
```python
|
||
import random
|
||
|
||
def generate_terrain(seed):
|
||
random.seed(seed)
|
||
perlin_offset = random.random() # float64
|
||
|
||
for chunk_x in range(16):
|
||
for chunk_z in range(16):
|
||
# Chaos in floating-point noise
|
||
noise_val = perlin(chunk_x + perlin_offset, chunk_z + perlin_offset)
|
||
height = int(noise_val * 255)
|
||
generate_chunk(height)
|
||
|
||
return terrain
|
||
|
||
# Same seed, different clients
|
||
client_a = generate_terrain(12345)
|
||
client_b = generate_terrain(12345)
|
||
```
|
||
|
||
**What Went Wrong**:
|
||
- Python on Windows: uses system's math library
|
||
- Python on Linux: uses different math library
|
||
- `perlin(1.5, 1.5)` returns 0.5000001 on Windows
|
||
- Same call returns 0.4999999 on Linux
|
||
- Height differs by 1 block
|
||
- Player stands on block, another player's client says it's air
|
||
- Falls through terrain, takes damage, calls it a "collision bug"
|
||
- Multiplayer cave exploration: different cave systems on different machines
|
||
- Treasure spawns at different locations
|
||
- Same seed ≠ same world across platforms
|
||
|
||
**Why No One Predicted It**:
|
||
- Assumption: "Deterministic noise = same everywhere"
|
||
- Floating-point math is platform-dependent
|
||
- Perlin noise is mathematically sensitive to initialization
|
||
- No cross-platform testing
|
||
|
||
**What Chaos Analysis Would Have Shown**:
|
||
```
|
||
Perlin noise is "chaotic" in sensitivity:
|
||
Two noise tables initialized with ε difference in gradient values
|
||
→ noise output differs by ~0.01-0.1 for same input
|
||
→ height values differ by 5-30 blocks
|
||
|
||
Solution: Use integer-only noise (or fixed-point arithmetic)
|
||
Deterministic noise requires platform-independent implementation
|
||
|
||
Example: Simplex noise with integer gradients (no floating-point):
|
||
Guarantees ε₀ = 0 (bit-identical across machines)
|
||
```
|
||
|
||
|
||
#### Failure 4: Three-Body Simulation Prediction Failure (Celestial Sandbox)
|
||
|
||
**Scenario**: Celestial sandbox where players watch planets orbit in real-time.
|
||
|
||
**What They Did**:
|
||
```cpp
|
||
// Newton's n-body simulation
|
||
void simulate_gravity(vector<Body>& bodies, float dt) {
|
||
for(int i = 0; i < bodies.size(); i++) {
|
||
Vec3 accel = {0, 0, 0};
|
||
for(int j = 0; j < bodies.size(); j++) {
|
||
if(i == j) continue;
|
||
Vec3 delta = bodies[j].pos - bodies[i].pos;
|
||
float dist_sq = dot(delta, delta);
|
||
accel += (G * bodies[j].mass / dist_sq) * normalize(delta);
|
||
}
|
||
bodies[i].velocity += accel * dt;
|
||
bodies[i].pos += bodies[i].velocity * dt;
|
||
}
|
||
}
|
||
|
||
// Runs fine for hours, then breaks
|
||
void main_loop() {
|
||
while(running) {
|
||
simulate_gravity(bodies, 0.016f); // 60fps
|
||
}
|
||
}
|
||
```
|
||
|
||
**What Went Wrong**:
|
||
- Two-body system: stable, predictable orbits
|
||
- Three-body system: chaotic, sensitive to initial conditions
|
||
- Player places planet at position (100.0, 0.0, 0.0)
|
||
- Different floating-point path (multiply vs divide) gives 100.00000001
|
||
- Initial velocity 30 m/s vs 29.9999999
|
||
- System exhibits unpredictable behavior
|
||
- Planets collide when they shouldn't (by math)
|
||
- Orbits become "weird" and unstable
|
||
- Player thinks: "Game physics is broken"
|
||
- Actually: Three-body problem is mathematically unpredictable
|
||
|
||
**Why No One Predicted It**:
|
||
- Didn't realize: more than 2 bodies = potential chaos
|
||
- No Lyapunov exponent calculation for the system
|
||
- Assumed "good physics engine" = "stable simulation"
|
||
- No testing with slightly perturbed initial conditions
|
||
|
||
**What Chaos Analysis Would Have Shown**:
|
||
```
|
||
Three-body problem with Earth-Moon-Sun-like masses:
|
||
Lyapunov exponent λ ≈ 0.5 per year (HIGHLY CHAOTIC)
|
||
|
||
Initial condition error: ε₀ = 10^-8 m (floating-point rounding)
|
||
After 1 year (simulated): ε(t) ≈ 10^-8 * e^(0.5*1) ≈ 10^-8 * 1.65 ≈ 1.65e-8
|
||
After 100 years: ε(t) ≈ 10^-8 * e^(50) ≈ 10^13 (completely wrong)
|
||
|
||
Useful prediction horizon: ~20-30 years, then simulation meaningless
|
||
Solution: Use higher precision (double) or smaller timesteps
|
||
Accept unpredictability and plan systems around it
|
||
```
|
||
|
||
|
||
#### Failure 5: Multiplayer Desyncs From Floating-Point Accumulation (Rust Server)
|
||
|
||
**Scenario**: Physics-based multiplayer game (MOBA arena combat).
|
||
|
||
**What They Did**:
|
||
```cpp
|
||
// Player positions synchronized by replaying inputs
|
||
struct Player {
|
||
Vec3 pos, vel;
|
||
float health;
|
||
};
|
||
|
||
void client_simulate(Player& p, Input input, float dt) {
|
||
// Apply input, integrate physics
|
||
if(input.forward) p.vel.z += 500 * dt;
|
||
p.pos += p.vel * dt;
|
||
p.vel *= 0.95; // Drag
|
||
}
|
||
|
||
// Same code on server and client
|
||
// Send inputs, not positions
|
||
```
|
||
|
||
**What Went Wrong**:
|
||
- Client A: presses forward, position becomes (0.0, 0.0, 1.000001)
|
||
- Server: same input, position becomes (0.0, 0.0, 0.999999)
|
||
- Frame 1: positions match (difference undetectable)
|
||
- Frame 100: difference grows to 0.01
|
||
- Frame 1000: player appears 0.5 units away on server vs client
|
||
- Client sees self at position A, server sees client at position B
|
||
- Attacks hit on one machine, miss on other
|
||
- Competitive players: "Game is unplayable, desyncs every game"
|
||
|
||
**Why No One Predicted It**:
|
||
- Assumed: "Same code + same inputs = same position"
|
||
- Didn't account for cumulative floating-point error
|
||
- Each frame adds ~ε error, errors don't cancel (butterfly effect)
|
||
- No state reconciliation between client and server
|
||
|
||
**What Chaos Analysis Would Have Shown**:
|
||
```
|
||
Physics accumulation system has Lyapunov exponent λ ≈ 0.001-0.01
|
||
(modest chaos, but still exponential divergence)
|
||
|
||
Client and server start with ε₀ = 0 (deterministic)
|
||
But floating-point rounding gives ε_actual = 10^-7 per frame
|
||
After 1000 frames: ε(1000) ≈ 10^-7 * e^(0.005 * 1000) ≈ 10^-7 * 148 ≈ 1.48e-5
|
||
After 10000 frames: ε(10000) ≈ 10^-7 * e^(50) ≈ 10^13 (diverged)
|
||
|
||
Window of trust: ~100-200 frames before desync is visible
|
||
Solution: Periodic state correction from server
|
||
Or: Use fixed-point arithmetic (no floating-point error)
|
||
```
|
||
|
||
|
||
#### Failure 6: Procedural Generation Butterfly Effect (Dungeon Generation Regression)
|
||
|
||
**Scenario**: Dungeon generation uses seeded chaos for room placement.
|
||
|
||
**What They Did**:
|
||
```python
|
||
def generate_dungeon(seed, width, height):
|
||
random.seed(seed)
|
||
|
||
# Chaotic room placement
|
||
rooms = []
|
||
for i in range(20):
|
||
x = random.randint(0, width)
|
||
y = random.randint(0, height)
|
||
w = random.randint(5, 15)
|
||
h = random.randint(5, 15)
|
||
|
||
if not overlaps(rooms, Rect(x, y, w, h)):
|
||
rooms.append(Rect(x, y, w, h))
|
||
|
||
return rooms
|
||
|
||
# Version 1.0: works great
|
||
# Version 1.01: add_new_feature() inserted before random.seed()
|
||
# Now same seed generates different dungeons!
|
||
# Players: "Why is my dungeon different?"
|
||
```
|
||
|
||
**What Went Wrong**:
|
||
- Initialization order matters in chaotic systems
|
||
- One extra `random.random()` call changes all subsequent generations
|
||
- Seed 12345 now generates completely different dungeon
|
||
- Players who shared seed "12345 for cool dungeon" get different dungeon
|
||
- Online communities break: "This seed doesn't work anymore"
|
||
- Gameplay balance broken: one seed is balanced, other is unplayable
|
||
|
||
**Why No One Predicted It**:
|
||
- Assumed: "Same seed = same generation"
|
||
- Didn't realize: chaotic algorithms are order-sensitive
|
||
- One extra random call shifts entire stream
|
||
- No regression testing on procedural generation
|
||
|
||
**What Chaos Analysis Would Have Shown**:
|
||
```
|
||
Chaotic random stream generation:
|
||
LCG (Linear Congruential Generator): x_{n+1} = (a*x_n + c) mod m
|
||
|
||
Each call: x_{n+1} = f(x_n)
|
||
Two sequences:
|
||
Sequence A: x_0 = 12345, then call f(x) once more than sequence B
|
||
Sequence B: x_0 = 12345
|
||
|
||
After calling f() k times:
|
||
Both diverge from the moment one calls f() one extra time
|
||
All subsequent values completely uncorrelated
|
||
|
||
Sensitivity to input order:
|
||
One extra call = chaos in output
|
||
|
||
Solution: Increment RNG once per unique operation
|
||
Or: Separate RNG streams for different generation steps
|
||
Or: Accept that generation is order-sensitive
|
||
```
|
||
|
||
|
||
## GREEN Phase: Understanding Chaos Scientifically
|
||
|
||
### 1. Introduction to Chaos: Three Myths
|
||
|
||
**Myth 1: "Chaotic = Random"**
|
||
|
||
Reality: Chaos is fully deterministic but unpredictable. A system can be 100% deterministic yet chaotic.
|
||
|
||
```python
|
||
# Chaotic but NOT random - completely deterministic
|
||
def chaotic_map(x):
|
||
return 4 * x * (1 - x) # Logistic map at r=4
|
||
|
||
x = 0.1
|
||
for i in range(10):
|
||
x = chaotic_map(x)
|
||
print(f"{i}: {x:.10f}")
|
||
|
||
# Output:
|
||
# 0: 0.3600000000
|
||
# 1: 0.9216000000
|
||
# 2: 0.2890399999
|
||
# 3: 0.8199482560
|
||
# 4: 0.5904968192
|
||
# 5: 0.9702458556
|
||
# 6: 0.1152926817
|
||
# 7: 0.4093697097
|
||
# 8: 0.9316390272
|
||
# 9: 0.2538937563
|
||
|
||
# Try x = 0.1000001 (tiny difference)
|
||
x = 0.1000001
|
||
for i in range(10):
|
||
x = chaotic_map(x)
|
||
print(f"{i}: {x:.10f}")
|
||
|
||
# Output:
|
||
# 0: 0.3600036000
|
||
# 1: 0.9215968256
|
||
# 2: 0.2890651946
|
||
# 3: 0.8198632635
|
||
# 4: 0.5906768633
|
||
# 5: 0.9701184960
|
||
# 6: 0.1157095754
|
||
# 7: 0.4088159297
|
||
# 8: 0.9321299357
|
||
# 9: 0.2525868195
|
||
|
||
# Different after 1 iteration! Tiny ε₀ becomes diverged.
|
||
```
|
||
|
||
**Myth 2: "Chaos Can't Be Harnessed"**
|
||
|
||
Reality: Chaos is predictable over short timescales, chaotic only at long timescales.
|
||
|
||
```cpp
|
||
// Short-term prediction: valid for ~50 timesteps
|
||
// Long-term behavior: bounded in strange attractor (predictable statistically)
|
||
class ChaoticWeather {
|
||
Vec3 state = {1, 1, 1}; // Lorenz system
|
||
|
||
public:
|
||
void update(float dt) {
|
||
float x = state.x, y = state.y, z = state.z;
|
||
float dx = 10 * (y - x);
|
||
float dy = x * (28 - z) - y;
|
||
float dz = x * y - (8/3) * z;
|
||
|
||
state = {x + dx*dt, y + dy*dt, z + dz*dt};
|
||
}
|
||
|
||
Vec3 predict_near_term(int steps) {
|
||
// Valid for ~50 steps - chaos grows exponentially
|
||
Vec3 prediction = state;
|
||
for(int i = 0; i < steps; i++) {
|
||
Vec3 temp = prediction;
|
||
float dt = 0.01;
|
||
float dx = 10 * (temp.y - temp.x);
|
||
float dy = temp.x * (28 - temp.z) - temp.y;
|
||
float dz = temp.x * temp.y - (8/3) * temp.z;
|
||
|
||
prediction = {temp.x + dx*dt, temp.y + dy*dt, temp.z + dz*dt};
|
||
}
|
||
return prediction; // Valid only for steps < 50
|
||
}
|
||
|
||
Bounds get_bounds() {
|
||
// ALWAYS bounded - will stay in strange attractor
|
||
// Can predict: "will be between -25 and 25"
|
||
// Can't predict: "will be at 3.2, 4.5, 1.1"
|
||
return {{-25, -25, 0}, {25, 25, 50}};
|
||
}
|
||
};
|
||
```
|
||
|
||
**Myth 3: "Determinism Prevents Desync"**
|
||
|
||
Reality: Determinism + floating-point arithmetic = butterfly effect = desync.
|
||
|
||
```python
|
||
# Both servers run identical code, same inputs
|
||
# But floating-point rounding causes inevitable desync
|
||
|
||
class DeterministicPhysics:
|
||
def __init__(self, pos):
|
||
self.pos = float(pos) # Floating-point
|
||
|
||
def update(self, force, dt):
|
||
# Both servers do this with same inputs
|
||
accel = force / 1.0 # Mass = 1
|
||
self.pos += accel * dt
|
||
|
||
def client_update(self):
|
||
# Client A: uses single precision
|
||
f32_pos = numpy.float32(self.pos) # Rounds to nearest float32
|
||
# Client B: uses double precision
|
||
f64_pos = float(self.pos)
|
||
# If pos = 0.1, these differ in 24th+ decimal place
|
||
|
||
# After 1000 updates, tiny differences compound
|
||
# Butterfly effect: 10^-7 → 10^-1 in ~100 iterations
|
||
|
||
# Solution: NOT "use determinism"
|
||
# BUT "use determinism + periodic state sync"
|
||
# OR "use determinism + fixed-point arithmetic"
|
||
```
|
||
|
||
|
||
### 2. The Butterfly Effect: Initial Condition Sensitivity
|
||
|
||
**Definition**: A system exhibits butterfly effect if arbitrarily small differences in initial conditions lead to exponentially diverging trajectories.
|
||
|
||
```cpp
|
||
// Classic example: Lorenz system (atmospheric convection)
|
||
struct LorentzSystem {
|
||
float x, y, z;
|
||
|
||
LorentzSystem(float x0, float y0, float z0) : x(x0), y(y0), z(z0) {}
|
||
|
||
void step(float dt) {
|
||
float sigma = 10.0f;
|
||
float rho = 28.0f;
|
||
float beta = 8.0f / 3.0f;
|
||
|
||
float dx = sigma * (y - x);
|
||
float dy = x * (rho - z) - y;
|
||
float dz = x * y - beta * z;
|
||
|
||
x += dx * dt;
|
||
y += dy * dt;
|
||
z += dz * dt;
|
||
}
|
||
|
||
float distance_to(const LorentzSystem& other) const {
|
||
float dx = x - other.x;
|
||
float dy = y - other.y;
|
||
float dz = z - other.z;
|
||
return sqrt(dx*dx + dy*dy + dz*dz);
|
||
}
|
||
};
|
||
|
||
int main() {
|
||
LorentzSystem sys1(1.0f, 1.0f, 1.0f);
|
||
LorentzSystem sys2(1.0f, 1.0f, 1.0f + 0.00001f); // Difference: 10^-5
|
||
|
||
float epsilon_0 = sys1.distance_to(sys2); // ~0.00001
|
||
cout << "Initial separation: " << epsilon_0 << endl;
|
||
|
||
for(int step = 0; step < 100; step++) {
|
||
sys1.step(0.01f);
|
||
sys2.step(0.01f);
|
||
|
||
float epsilon = sys1.distance_to(sys2);
|
||
float growth_rate = log(epsilon / epsilon_0) / (step * 0.01f);
|
||
|
||
cout << "Step " << step << ": separation = " << epsilon
|
||
<< ", growth rate = " << growth_rate << endl;
|
||
|
||
if(epsilon > 1.0f) {
|
||
cout << "Trajectories completely diverged!" << endl;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// Output:
|
||
// Initial separation: 1e-05
|
||
// Step 1: separation = 0.000015, growth_rate = 0.405
|
||
// Step 5: separation = 0.00014, growth_rate = 0.405
|
||
// Step 10: separation = 0.0024, growth_rate = 0.405
|
||
// Step 20: separation = 0.067, growth_rate = 0.405
|
||
// Step 30: separation = 1.9, growth_rate = 0.405
|
||
// Trajectories completely diverged!
|
||
```
|
||
|
||
|
||
### 3. Lyapunov Exponents: Measuring Divergence Rate
|
||
|
||
**Definition**: Lyapunov exponent λ measures how fast nearby trajectories diverge: ε(t) ≈ ε₀ * e^(λ*t)
|
||
|
||
```python
|
||
def calculate_lyapunov_exponent(system_func, initial_state, dt, iterations=10000):
|
||
"""
|
||
Approximate largest Lyapunov exponent
|
||
system_func: function that returns next state
|
||
initial_state: starting point
|
||
dt: timestep
|
||
"""
|
||
epsilon = 1e-8 # Small perturbation
|
||
|
||
state1 = np.array(initial_state, dtype=float)
|
||
state2 = state1.copy()
|
||
state2[0] += epsilon
|
||
|
||
lyapunov_sum = 0.0
|
||
|
||
for i in range(iterations):
|
||
# Evolve both trajectories
|
||
state1 = system_func(state1, dt)
|
||
state2 = system_func(state2, dt)
|
||
|
||
# Calculate separation
|
||
delta = state2 - state1
|
||
separation = np.linalg.norm(delta)
|
||
|
||
if separation > 0:
|
||
lyapunov_sum += np.log(separation / epsilon)
|
||
|
||
# Renormalize to avoid numerical issues
|
||
state2 = state1 + (delta / separation) * epsilon
|
||
|
||
# Average Lyapunov exponent
|
||
return lyapunov_sum / (iterations * dt)
|
||
|
||
# Example: Logistic map
|
||
def logistic_map_step(x, dt):
|
||
return np.array([4 * x[0] * (1 - x[0])])
|
||
|
||
lambda_logistic = calculate_lyapunov_exponent(logistic_map_step, [0.1], 1.0)
|
||
print(f"Logistic map Lyapunov exponent: {lambda_logistic:.3f}")
|
||
# Output: Logistic map Lyapunov exponent: 1.386
|
||
|
||
# Interpretation:
|
||
# λ > 0: CHAOTIC (trajectories diverge exponentially)
|
||
# λ = 0: BIFURCATION (boundary between order and chaos)
|
||
# λ < 0: STABLE (trajectories converge)
|
||
|
||
# For weather (Lorenz): λ ≈ 0.9
|
||
# For logistic map at r=4: λ ≈ 1.386
|
||
# For multiplayer physics: λ ≈ 0.001 (slow chaos, but inevitable)
|
||
```
|
||
|
||
**Game-Relevant Interpretation**:
|
||
|
||
```cpp
|
||
struct SystemCharacterization {
|
||
float lyapunov_exponent;
|
||
float prediction_horizon; // In seconds or frames
|
||
|
||
// Calculate how long before small errors become visible
|
||
float time_until_visible_error(float error_threshold = 0.1f) {
|
||
if(lyapunov_exponent <= 0) return INFINITY; // Not chaotic
|
||
|
||
// ε(t) = ε₀ * e^(λ*t) = error_threshold
|
||
// ln(error_threshold / ε₀) = λ*t
|
||
// t = ln(error_threshold / ε₀) / λ
|
||
|
||
float epsilon_0 = 1e-7f; // Floating-point precision
|
||
return logf(error_threshold / epsilon_0) / lyapunov_exponent;
|
||
}
|
||
};
|
||
|
||
// Examples
|
||
void main() {
|
||
// Multiplayer physics (modest chaos)
|
||
SystemCharacterization phys_system{0.005f, 0};
|
||
phys_system.prediction_horizon = phys_system.time_until_visible_error(0.5f);
|
||
cout << "Physics desync window: " << phys_system.prediction_horizon << " frames\n";
|
||
// Output: ~3300 frames @ 60fps = 55 seconds before visible desync
|
||
|
||
// Weather (high chaos)
|
||
SystemCharacterization weather_system{0.9f, 0};
|
||
weather_system.prediction_horizon = weather_system.time_until_visible_error(1.0f);
|
||
cout << "Weather forecast window: " << weather_system.prediction_horizon << " timesteps\n";
|
||
// Output: ~18 timesteps before complete divergence (if dt=1 second, ~18 seconds)
|
||
|
||
// Logistic map (extreme chaos)
|
||
SystemCharacterization logistic{1.386f, 0};
|
||
logistic.prediction_horizon = logistic.time_until_visible_error(0.1f);
|
||
cout << "Logistic map prediction: " << logistic.prediction_horizon << " iterations\n";
|
||
// Output: ~5 iterations before completely wrong
|
||
}
|
||
```
|
||
|
||
|
||
### 4. Bounded Chaos: Strange Attractors
|
||
|
||
**Definition**: Despite chaotic motion, trajectories never leave a bounded region (strange attractor). Chaos is bounded but unpredictable.
|
||
|
||
```python
|
||
class StrangeAttractor:
|
||
"""
|
||
Lorenz system exhibits bounded chaos:
|
||
- Never leaves (-30, -30, 0) to (30, 30, 50) region
|
||
- Within region: motion is chaotic, unpredictable
|
||
- Can predict: "will be in region"
|
||
- Can't predict: "will be at exact point"
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.x, self.y, self.z = 1.0, 1.0, 1.0
|
||
|
||
def step(self, dt=0.01):
|
||
sigma, rho, beta = 10, 28, 8/3
|
||
dx = sigma * (self.y - self.x)
|
||
dy = self.x * (rho - self.z) - self.y
|
||
dz = self.x * self.y - beta * self.z
|
||
|
||
self.x += dx * dt
|
||
self.y += dy * dt
|
||
self.z += dz * dt
|
||
|
||
# Always stays bounded
|
||
assert -30 <= self.x <= 30, "x diverged!"
|
||
assert -30 <= self.y <= 30, "y diverged!"
|
||
assert 0 <= self.z <= 50, "z diverged!"
|
||
|
||
def is_in_attractor(self):
|
||
return (-30 <= self.x <= 30 and
|
||
-30 <= self.y <= 30 and
|
||
0 <= self.z <= 50)
|
||
|
||
# Generate attractor shape
|
||
attractor = StrangeAttractor()
|
||
points = []
|
||
|
||
for _ in range(100000):
|
||
attractor.step()
|
||
points.append((attractor.x, attractor.y, attractor.z))
|
||
|
||
# Visualize: shows beautiful fractal structure
|
||
# All 100k points stay in bounded region despite chaotic motion
|
||
# But no two points are exactly the same (chaotic)
|
||
```
|
||
|
||
**Game Application: Bounded Chaos for Procedural Generation**
|
||
|
||
```cpp
|
||
class ProceduralBiome {
|
||
// Use chaotic system to generate varied but bounded terrain
|
||
|
||
struct ChaoticTerrain {
|
||
float height_field[256][256];
|
||
|
||
void generate_with_bounded_chaos(int seed) {
|
||
float x = 0.1f, y = 0.1f, z = 0.1f;
|
||
srand(seed);
|
||
|
||
// Add initial random perturbation (bounded)
|
||
x += (rand() % 1000) / 10000.0f; // Within [0, 1]
|
||
y += (rand() % 1000) / 10000.0f;
|
||
z += (rand() % 1000) / 10000.0f;
|
||
|
||
// Evolve chaotic system, map to height
|
||
for(int i = 0; i < 256; i++) {
|
||
for(int j = 0; j < 256; j++) {
|
||
// 1000 iterations of Lorenz for this tile
|
||
for(int k = 0; k < 1000; k++) {
|
||
float dx = 10 * (y - x);
|
||
float dy = x * (28 - z) - y;
|
||
float dz = x * y - (8/3) * z;
|
||
|
||
x += dx * 0.001f;
|
||
y += dy * 0.001f;
|
||
z += dz * 0.001f;
|
||
}
|
||
|
||
// Map z ∈ [0, 50] to height ∈ [0, 255]
|
||
// Guaranteed to be in valid range (bounded)
|
||
height_field[i][j] = (z / 50.0f) * 255;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
};
|
||
|
||
// Result: naturally varied terrain (chaotic generation)
|
||
// but always valid heights (bounded by attractor)
|
||
```
|
||
|
||
|
||
### 5. Determinism in Games: The Hard Truth
|
||
|
||
**Determinism ≠ Synchronization**
|
||
|
||
```cpp
|
||
class MultiplayerPhysicsEngine {
|
||
// Myth: "Same code + same inputs = same result"
|
||
// Reality: Floating-point rounding breaks this
|
||
|
||
void deterministic_but_not_synchronized() {
|
||
// Both servers run identical code
|
||
// Both servers execute identical inputs
|
||
// But floating-point arithmetic gives slightly different results
|
||
|
||
Vec3 pos1 = Vec3(0.1f, 0.2f, 0.3f);
|
||
Vec3 pos2 = Vec3(0.1f, 0.2f, 0.3f);
|
||
|
||
for(int frame = 0; frame < 10000; frame++) {
|
||
// Identical physics code
|
||
pos1 += Vec3(0.1f, 0.2f, 0.3f) * 0.016f;
|
||
pos2 += Vec3(0.1f, 0.2f, 0.3f) * 0.016f;
|
||
}
|
||
|
||
// pos1 ≠ pos2 (floating-point rounding accumulated)
|
||
assert(pos1 == pos2); // FAILS!
|
||
}
|
||
|
||
void truly_deterministic_solution() {
|
||
// Option 1: Fixed-point arithmetic (no floating-point)
|
||
int32_t pos1 = 100; // Fixed-point: 1.0 = 100 units
|
||
int32_t pos2 = 100;
|
||
|
||
// Deterministic integer math
|
||
pos1 += (1 + 2 + 3) * 16;
|
||
pos2 += (1 + 2 + 3) * 16;
|
||
|
||
assert(pos1 == pos2); // PASSES
|
||
|
||
// Option 2: Periodic state reconciliation
|
||
// Server broadcasts full state every 60 frames
|
||
// Clients correct position from authoritative server state
|
||
|
||
// Option 3: Client-side prediction with server correction
|
||
// Client predicts locally (may diverge slightly)
|
||
// Server sends correction: "actually at position X"
|
||
// Client smoothly transitions to correction
|
||
}
|
||
};
|
||
```
|
||
|
||
|
||
### 6. Multiplayer Implications: Desync Prevention
|
||
|
||
```cpp
|
||
class DesyncsAndSolutions {
|
||
enum SyncStrategy {
|
||
// WRONG: Deterministic simulation + floating-point
|
||
DETERMINISM_ONLY,
|
||
|
||
// CORRECT: Determinism + state sync
|
||
DETERMINISM_WITH_PERIODIC_STATE_BROADCAST,
|
||
|
||
// CORRECT: Fixed-point arithmetic
|
||
FIXED_POINT_DETERMINISM,
|
||
|
||
// CORRECT: Rollback + resimulation
|
||
DETERMINISM_WITH_ROLLBACK,
|
||
};
|
||
|
||
void calculate_sync_frequency(float lyapunov_exponent,
|
||
float visible_error_threshold,
|
||
float dt,
|
||
float& broadcast_interval) {
|
||
// Formula: error grows as ε(t) = ε₀ * e^(λ*t)
|
||
// When does ε(t) reach visible_error_threshold?
|
||
|
||
float epsilon_0 = 1e-7f; // Floating-point precision
|
||
float t_diverge = logf(visible_error_threshold / epsilon_0) / lyapunov_exponent;
|
||
|
||
// Be conservative: sync at t_diverge / 2
|
||
broadcast_interval = t_diverge / 2.0f * dt;
|
||
|
||
// Example: multiplayer physics with λ = 0.005, visible threshold = 0.1m
|
||
// epsilon_0 = 1e-7
|
||
// t_diverge = ln(0.1 / 1e-7) / 0.005 ≈ ln(1e6) / 0.005 ≈ 2762 frames
|
||
// broadcast_interval = 2762 / 2 = 1381 frames ≈ 23 seconds @ 60fps
|
||
// Safe choice: broadcast every 10 seconds
|
||
}
|
||
|
||
void example_multiplayer_sync() {
|
||
// Deterministic tick: Physics runs on fixed 60Hz
|
||
// Broadcast: Every 30 ticks (0.5 seconds)
|
||
|
||
for(int tick = 0; tick < total_ticks; tick++) {
|
||
// Execute player inputs (deterministic on both client/server)
|
||
simulate_physics(0.016f);
|
||
|
||
// Every 30 ticks, broadcast state
|
||
if(tick % 30 == 0) {
|
||
serialize_and_broadcast_player_positions();
|
||
}
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
|
||
### 7. Implementation Patterns: Handling Chaos
|
||
|
||
#### Pattern 1: Prediction Horizon Tracking
|
||
|
||
```python
|
||
class ChaoticSystemSimulator:
|
||
def __init__(self, lyapunov_exp):
|
||
self.lyapunov = lyapunov_exp
|
||
self.max_reliable_steps = None
|
||
|
||
def set_error_tolerance(self, tolerance):
|
||
# Calculate how many steps before error exceeds tolerance
|
||
if self.lyapunov > 0:
|
||
epsilon_0 = 1e-7
|
||
self.max_reliable_steps = np.log(tolerance / epsilon_0) / self.lyapunov
|
||
else:
|
||
self.max_reliable_steps = float('inf')
|
||
|
||
def can_extrapolate(self, current_step):
|
||
if self.max_reliable_steps is None:
|
||
return True
|
||
return current_step < self.max_reliable_steps
|
||
|
||
def should_resync(self, current_step):
|
||
if self.max_reliable_steps is None:
|
||
return False
|
||
# Resync at 80% of max reliable time (safety margin)
|
||
return current_step > self.max_reliable_steps * 0.8
|
||
|
||
# Usage in game
|
||
simulator = ChaoticSystemSimulator(lyapunov_exp=0.005)
|
||
simulator.set_error_tolerance(0.5) # 50cm error threshold
|
||
|
||
for step in range(10000):
|
||
if simulator.should_resync(step):
|
||
request_authoritative_state_from_server()
|
||
|
||
simulate_local_physics()
|
||
```
|
||
|
||
#### Pattern 2: State Bracketing for Prediction
|
||
|
||
```cpp
|
||
template<typename State>
|
||
class ChaoticPredictor {
|
||
// Keep history of states to bound prediction error
|
||
|
||
struct StateSnapshot {
|
||
State state;
|
||
int frame;
|
||
float lyapunov_accumulated; // Cumulative chaos measure
|
||
};
|
||
|
||
vector<StateSnapshot> history;
|
||
float lyapunov_exponent;
|
||
|
||
public:
|
||
void add_state(const State& state, int frame) {
|
||
float prev_error = 1e-7f;
|
||
|
||
if(!history.empty()) {
|
||
StateSnapshot& prev = history.back();
|
||
float time_elapsed = (frame - prev.frame) * dt;
|
||
prev_error *= expf(lyapunov_exponent * time_elapsed);
|
||
}
|
||
|
||
history.push_back({state, frame, prev_error});
|
||
|
||
// Keep only recent history (within prediction horizon)
|
||
while(history.size() > 50) {
|
||
history.erase(history.begin());
|
||
}
|
||
}
|
||
|
||
State predict_at_frame(int target_frame) {
|
||
// Find bracketing states
|
||
auto it = lower_bound(history.begin(), history.end(), target_frame,
|
||
[](const StateSnapshot& s, int f) { return s.frame < f; });
|
||
|
||
if(it == history.end()) {
|
||
return history.back().state; // Extrapolate from last known
|
||
}
|
||
|
||
// Check error has not grown too much
|
||
float time_since_last = (target_frame - it->frame) * dt;
|
||
float error_at_target = it->lyapunov_accumulated *
|
||
expf(lyapunov_exponent * time_since_last);
|
||
|
||
if(error_at_target > 0.1f) { // 10cm error
|
||
return State::UNRELIABLE; // Can't predict this far
|
||
}
|
||
|
||
return it->state; // Safe to extrapolate
|
||
}
|
||
};
|
||
```
|
||
|
||
#### Pattern 3: Chaos Budgeting
|
||
|
||
```rust
|
||
struct ChaossBudget {
|
||
frames_until_resync: i32,
|
||
error_threshold: f32,
|
||
current_accumulated_error: f32,
|
||
lyapunov: f32,
|
||
}
|
||
|
||
impl ChaosBudget {
|
||
fn new(lyapunov: f32, error_threshold: f32, dt: f32) -> Self {
|
||
let frames = ((error_threshold / 1e-7).ln() / lyapunov / dt) as i32;
|
||
ChaosBudget {
|
||
frames_until_resync: frames / 2, // Safety margin
|
||
error_threshold,
|
||
current_accumulated_error: 1e-7,
|
||
lyapunov,
|
||
}
|
||
}
|
||
|
||
fn step(&mut self) {
|
||
self.frames_until_resync -= 1;
|
||
self.current_accumulated_error *= (self.lyapunov / 60.0).exp();
|
||
}
|
||
|
||
fn needs_resync(&self) -> bool {
|
||
self.frames_until_resync <= 0 ||
|
||
self.current_accumulated_error > self.error_threshold
|
||
}
|
||
|
||
fn reset(&mut self) {
|
||
self.frames_until_resync = self.frames_until_resync * 2;
|
||
self.current_accumulated_error = 1e-7;
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
### 8. Decision Framework: When to Worry About Chaos
|
||
|
||
```
|
||
┌─ Is system chaotic? (λ > 0?)
|
||
│
|
||
├─ NO (λ ≤ 0): Stable system
|
||
│ └─ Proceed normally, no special handling needed
|
||
│
|
||
└─ YES (λ > 0): Chaotic system
|
||
│
|
||
├─ Calculate prediction horizon: t = ln(threshold / ε₀) / λ
|
||
│
|
||
├─ t > game duration?
|
||
│ ├─ YES: Don't worry, prediction stays accurate
|
||
│ └─ NO: Need sync strategy
|
||
│
|
||
├─ Is multiplayer?
|
||
│ ├─ YES:
|
||
│ │ ├─ Use fixed-point arithmetic, OR
|
||
│ │ ├─ Sync state every t/2 frames, OR
|
||
│ │ ├─ Use rollback netcode
|
||
│ │ └─ Test desyncs at scale
|
||
│ │
|
||
│ └─ NO: Single-player, no desync possible
|
||
│ └─ Use any simulation method
|
||
│
|
||
├─ Is procedural generation?
|
||
│ ├─ YES:
|
||
│ │ ├─ Use integer-only noise (no floating-point), OR
|
||
│ │ ├─ Store seed → generated content (immutable), OR
|
||
│ │ ├─ Accept platform differences and make content data-driven
|
||
│ │ └─ Test generation on all target platforms
|
||
│ │
|
||
│ └─ NO: Real-time simulation
|
||
│ └─ Follow multiplayer rules above
|
||
│
|
||
└─ Physics simulation?
|
||
├─ YES: Especially multiplayer → HIGH PRIORITY for sync
|
||
└─ NO: Procedural generation might be OK without perfect sync
|
||
```
|
||
|
||
|
||
### 9. Common Pitfalls
|
||
|
||
**Pitfall 1: "Deterministic Code = Synchronized Results"**
|
||
|
||
Wrong. Floating-point math is non-associative:
|
||
```cpp
|
||
// These don't give the same result
|
||
float a = (0.1 + 0.2) + 0.3;
|
||
float b = 0.1 + (0.2 + 0.3);
|
||
// a ≠ b (floating-point rounding)
|
||
|
||
// In simulation: order of force application matters
|
||
pos += (force_a + force_b) * dt; // Different result than
|
||
pos += force_a * dt;
|
||
pos += force_b * dt;
|
||
```
|
||
|
||
**Pitfall 2: "More Precision = More Sync"**
|
||
|
||
Wrong. Higher precision delays divergence but doesn't prevent it:
|
||
```cpp
|
||
double precise_pos = /* exact calculation */;
|
||
float approx_pos = /* same calculation */;
|
||
|
||
// precise ≠ approx after many frames
|
||
// double just delays divergence by ~2x
|
||
// Still eventually desync
|
||
|
||
// Correct: use periodic sync + higher precision
|
||
```
|
||
|
||
**Pitfall 3: "Random Seed = Reproducible"**
|
||
|
||
Wrong. RNG order matters:
|
||
```python
|
||
# Same seed, different generation order
|
||
random.seed(12345)
|
||
a = random.random() # Gets first value
|
||
b = random.random() # Gets second value
|
||
|
||
random.seed(12345)
|
||
c = random.random() # Might be different if RNG was called once more before
|
||
```
|
||
|
||
**Pitfall 4: "Slow Simulations Don't Need Sync"**
|
||
|
||
Wrong. Slow simulations have MORE time for chaos to grow:
|
||
```
|
||
10 frames @ 60Hz = 0.167 seconds (minimal chaos)
|
||
1000 frames @ 60Hz = 16.7 seconds (significant divergence for λ > 0.1)
|
||
|
||
Lower framerate ≠ lower chaos
|
||
Just fewer chances to resync
|
||
```
|
||
|
||
|
||
### 10. Testing Chaotic Systems
|
||
|
||
```python
|
||
class ChaosTestSuite:
|
||
|
||
@staticmethod
|
||
def test_divergence_rate(system_func, initial_state, dt, iterations=1000):
|
||
"""Verify Lyapunov exponent matches theoretical prediction"""
|
||
epsilon = 1e-8
|
||
state1 = initial_state.copy()
|
||
state2 = initial_state.copy()
|
||
state2[0] += epsilon
|
||
|
||
separations = []
|
||
for i in range(iterations):
|
||
state1 = system_func(state1, dt)
|
||
state2 = system_func(state2, dt)
|
||
sep = np.linalg.norm(state2 - state1)
|
||
separations.append(sep)
|
||
|
||
# Check exponential growth
|
||
log_seps = np.log(separations)
|
||
expected_growth = (log_seps[-1] - log_seps[0]) / (iterations * dt)
|
||
print(f"Measured divergence rate: {expected_growth}")
|
||
return expected_growth
|
||
|
||
@staticmethod
|
||
def test_floating_point_sensitivity(system_func, initial_state):
|
||
"""Verify floating-point precision causes divergence"""
|
||
# Run with float32 vs float64
|
||
state32 = np.array(initial_state, dtype=np.float32)
|
||
state64 = np.array(initial_state, dtype=np.float64)
|
||
|
||
for _ in range(100):
|
||
state32 = system_func(state32, 0.01)
|
||
state64 = system_func(state64, 0.01)
|
||
|
||
# Should diverge
|
||
diff = np.linalg.norm(state32 - state64)
|
||
assert diff > 1e-6, "Floating-point sensitivity test failed"
|
||
print(f"Float32/64 divergence after 100 steps: {diff}")
|
||
|
||
@staticmethod
|
||
def test_desync_in_multiplayer(client_code, server_code, shared_inputs, frames=1000):
|
||
"""Simulate client/server divergence"""
|
||
client_state = [0, 0, 0]
|
||
server_state = [0, 0, 0]
|
||
|
||
max_divergence = 0
|
||
for frame in range(frames):
|
||
input_frame = shared_inputs[frame % len(shared_inputs)]
|
||
|
||
# Both run same code, may get different floating-point results
|
||
client_state = client_code(client_state, input_frame, 0.016)
|
||
server_state = server_code(server_state, input_frame, 0.016)
|
||
|
||
divergence = np.linalg.norm(np.array(client_state) - np.array(server_state))
|
||
max_divergence = max(max_divergence, divergence)
|
||
|
||
print(f"Max divergence over {frames} frames: {max_divergence}")
|
||
return max_divergence
|
||
|
||
@staticmethod
|
||
def test_generation_reproducibility(generator, seed, num_runs=5):
|
||
"""Check if procedural generation gives same output"""
|
||
outputs = []
|
||
for _ in range(num_runs):
|
||
output = generator(seed)
|
||
outputs.append(output)
|
||
|
||
for i in range(1, num_runs):
|
||
if outputs[i] != outputs[0]:
|
||
print(f"ERROR: Seed {seed} produces different output")
|
||
return False
|
||
|
||
print(f"Seed {seed} reproducible across {num_runs} runs")
|
||
return True
|
||
```
|
||
|
||
|
||
## REFACTOR Phase: 6 Scenarios and Solutions
|
||
|
||
### Scenario 1: Weather Simulation (Lorenz System)
|
||
|
||
**Problem**: Multiplayer game with synchronized weather. Players on different servers see different storms.
|
||
|
||
**Analysis**:
|
||
- Lorenz system: λ ≈ 0.9 (highly chaotic)
|
||
- Initial floating-point error: ε₀ ≈ 10^-7
|
||
- Time to visible divergence: t ≈ ln(1.0 / 10^-7) / 0.9 ≈ 18 timesteps
|
||
- At 1 timestep/second: ~18 seconds before complete divergence
|
||
|
||
**Solution**:
|
||
```cpp
|
||
class SynchronizedWeather {
|
||
struct WeatherState {
|
||
float temperature, humidity, pressure;
|
||
int seed;
|
||
int last_sync_frame;
|
||
};
|
||
|
||
void update_and_sync(int frame, float dt) {
|
||
// Simulate locally
|
||
update_lorenz(dt);
|
||
|
||
// Broadcast full state every 15 timesteps (90% of divergence horizon)
|
||
if(frame % 15 == 0) {
|
||
broadcast_weather_state();
|
||
}
|
||
|
||
// Receive state from other servers
|
||
WeatherState remote = receive_weather_state();
|
||
if(remote.seed == my_seed) {
|
||
// Correct if diverged
|
||
if(distance(temperature, humidity, pressure,
|
||
remote.temperature, remote.humidity, remote.pressure) > 0.1) {
|
||
temperature = remote.temperature;
|
||
humidity = remote.humidity;
|
||
pressure = remote.pressure;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
### Scenario 2: Double Pendulum Physics
|
||
|
||
**Problem**: Physics demo with two connected pendulums. Tiny player input differences cause completely different final states.
|
||
|
||
**Analysis**:
|
||
- Double pendulum: λ ≈ 0.5-1.0 (chaotic)
|
||
- Player swings pendulum slightly differently each time
|
||
- Visual divergence happens after ~20-50 swings
|
||
|
||
**Solution**:
|
||
```cpp
|
||
class StablePendulumDemo {
|
||
// Solution 1: Discrete input quantization
|
||
void update(PlayerInput input) {
|
||
// Round input to discrete levels
|
||
float quantized_force = roundf(input.force * 10.0f) / 10.0f;
|
||
|
||
// Apply quantized input
|
||
apply_torque(quantized_force);
|
||
update_physics(0.016f);
|
||
}
|
||
|
||
// Solution 2: Prediction tolerance display
|
||
void render_with_uncertainty() {
|
||
// Show "uncertainty cone" around predicted trajectory
|
||
float uncertainty_radius = 0.05f * frame_number; // Grows with time
|
||
|
||
draw_pendulum_trajectory_with_band(uncertainty_radius);
|
||
draw_text("Prediction reliable for next 50 frames");
|
||
}
|
||
};
|
||
```
|
||
|
||
### Scenario 3: Multiplayer Desyncs (RTS Game)
|
||
|
||
**Problem**: RTS units diverge position after a few minutes of gameplay.
|
||
|
||
**Analysis**:
|
||
- Physics + collision: λ ≈ 0.001-0.01
|
||
- Window before visible desync: ~100-1000 frames
|
||
- At 60fps: 2-17 seconds
|
||
|
||
**Solution**:
|
||
```cpp
|
||
class DeterministicRTSWithSync {
|
||
vector<Unit> units;
|
||
int frame_counter;
|
||
|
||
void tick() {
|
||
frame_counter++;
|
||
|
||
// Simulate physics
|
||
for(Unit& u : units) {
|
||
u.update_position(0.016f);
|
||
u.check_collisions_deterministic();
|
||
}
|
||
|
||
// Periodic state broadcast
|
||
if(frame_counter % 120 == 0) { // Every 2 seconds @ 60fps
|
||
serialize_unit_positions();
|
||
network.broadcast_state();
|
||
}
|
||
|
||
// Receive corrections from other players
|
||
if(auto correction = network.receive_state_correction()) {
|
||
apply_correction(correction);
|
||
}
|
||
}
|
||
|
||
void apply_correction(StateCorrection corr) {
|
||
for(const auto& corrected_unit : corr.units) {
|
||
Unit& local = find_unit(corrected_unit.id);
|
||
|
||
// Smoothly interpolate to corrected position
|
||
local.target_pos = corrected_unit.pos;
|
||
local.correction_in_progress = true;
|
||
local.correction_frames_remaining = 4; // Smooth over 4 frames
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
### Scenario 4: Procedural Generation Desync
|
||
|
||
**Problem**: Dungeon generator uses float-based Perlin noise. Windows PC generates different dungeons than Linux server.
|
||
|
||
**Analysis**:
|
||
- Float-based noise: Platform-dependent math library
|
||
- Initialization differences cause immediate divergence (λ effectively infinite for fractional results)
|
||
|
||
**Solution**:
|
||
```cpp
|
||
class PlatformIndependentNoiseGenerator {
|
||
|
||
// Option 1: Integer-only Simplex noise
|
||
int32_t integer_simplex_noise(int x, int y, int z) {
|
||
// Uses only integer operations - identical on all platforms
|
||
|
||
int g[512][3]; // Precomputed integer gradient table
|
||
|
||
int xi = x & 255;
|
||
int yi = y & 255;
|
||
int zi = z & 255;
|
||
|
||
int gi = perlin_permutation[xi + perlin_permutation[yi + perlin_permutation[zi]]] % 12;
|
||
|
||
return gi * 100; // Integer result, bit-identical across platforms
|
||
}
|
||
|
||
// Option 2: Store pre-computed generation data
|
||
struct DungeonTemplate {
|
||
vector<Room> rooms;
|
||
vector<Corridor> corridors;
|
||
|
||
static DungeonTemplate generate_once(int seed) {
|
||
// Generate once on server with highest precision
|
||
// Store result in asset file
|
||
// All clients load same file
|
||
// Zero desync
|
||
}
|
||
};
|
||
|
||
// Option 3: Client caches generated content
|
||
LRUCache<int, Terrain> generated_cache;
|
||
|
||
Terrain get_terrain(int seed) {
|
||
if(generated_cache.contains(seed)) {
|
||
return generated_cache[seed]; // Guaranteed same as server
|
||
}
|
||
|
||
// Request from server
|
||
Terrain t = server.request_terrain(seed);
|
||
generated_cache.insert(seed, t);
|
||
return t;
|
||
}
|
||
};
|
||
```
|
||
|
||
### Scenario 5: Three-Body Celestial Sandbox
|
||
|
||
**Problem**: Players simulate three-star system. Tiny precision differences cause different outcomes.
|
||
|
||
**Analysis**:
|
||
- Three-body: λ ≈ 0.5-2.0 (extremely chaotic)
|
||
- Prediction horizon: t ≈ 2-10 timesteps (depending on initial config)
|
||
- After that: chaos wins, completely unpredictable
|
||
|
||
**Solution**:
|
||
```cpp
|
||
class ThreeBodySandbox {
|
||
struct Star {
|
||
double x, y, z; // Use double not float!
|
||
double vx, vy, vz;
|
||
double mass;
|
||
};
|
||
|
||
vector<Star> stars;
|
||
|
||
void update(double dt) {
|
||
// Use double precision throughout
|
||
// This extends prediction horizon by ~10x vs float
|
||
|
||
for(Star& star : stars) {
|
||
double ax = 0, ay = 0, az = 0;
|
||
|
||
for(const Star& other : stars) {
|
||
if(&star == &other) continue;
|
||
|
||
double dx = other.x - star.x;
|
||
double dy = other.y - star.y;
|
||
double dz = other.z - star.z;
|
||
|
||
double r = sqrt(dx*dx + dy*dy + dz*dz);
|
||
double r3 = r * r * r;
|
||
|
||
double accel = G * other.mass / (r3 + 1e-10);
|
||
ax += dx * accel;
|
||
ay += dy * accel;
|
||
az += dz * accel;
|
||
}
|
||
|
||
star.vx += ax * dt;
|
||
star.vy += ay * dt;
|
||
star.vz += az * dt;
|
||
|
||
star.x += star.vx * dt;
|
||
star.y += star.vy * dt;
|
||
star.z += star.vz * dt;
|
||
}
|
||
}
|
||
|
||
void render_with_prediction_limits() {
|
||
// Show prediction reliability
|
||
float lyapunov = 1.0f; // Rough estimate for 3-body
|
||
float time_to_diverge = logf(1.0f / 1e-15) / lyapunov;
|
||
|
||
draw_text("Prediction reliable for: %.1f time units", time_to_diverge);
|
||
draw_text("(After that: chaos dominates)");
|
||
}
|
||
};
|
||
```
|
||
|
||
### Scenario 6: Chaos Bounds and Strange Attractors
|
||
|
||
**Problem**: Game needs unpredictable but bounded behavior (e.g., enemy AI movement).
|
||
|
||
**Analysis**:
|
||
- Use chaotic attractor: bounded but unpredictable
|
||
- Examples: Lorenz system, Hénon map, logistic map
|
||
- AI behavior varies each encounter but stays in valid range
|
||
|
||
**Solution**:
|
||
```cpp
|
||
class ChaoticAIBehavior {
|
||
|
||
struct StrangeAttractorAI {
|
||
float x, y, z; // Chaotic state
|
||
float mood_min = -1, mood_max = 1;
|
||
|
||
void step() {
|
||
// Lorenz equations - chaotic but bounded
|
||
float sigma = 10.0f, rho = 28.0f, beta = 8.0f/3.0f;
|
||
float dt = 0.01f;
|
||
|
||
float dx = sigma * (y - x);
|
||
float dy = x * (rho - z) - y;
|
||
float dz = x * y - beta * z;
|
||
|
||
x += dx * dt;
|
||
y += dy * dt;
|
||
z += dz * dt;
|
||
|
||
// Normalize to [-1, 1] range
|
||
float mood = tanh(x / 25.0f); // Always in [-1, 1]
|
||
assert(mood >= -1 && mood <= 1);
|
||
}
|
||
|
||
float get_aggression() {
|
||
// Normalize z to [0, 1]
|
||
return (z / 50.0f); // Always [0, 1] due to strange attractor
|
||
}
|
||
|
||
float get_confidence() {
|
||
// Normalize y to [0, 1]
|
||
float c = (y + 30.0f) / 60.0f; // y ∈ [-30, 30]
|
||
return clamp(c, 0.0f, 1.0f);
|
||
}
|
||
};
|
||
|
||
StrangeAttractorAI enemy_ai;
|
||
|
||
void update_enemy() {
|
||
enemy_ai.step();
|
||
|
||
float agg = enemy_ai.get_aggression();
|
||
float conf = enemy_ai.get_confidence();
|
||
|
||
// Use these values to drive AI decisions
|
||
if(agg > 0.7f && conf > 0.5f) {
|
||
enemy_attack();
|
||
} else if(agg < 0.3f) {
|
||
enemy_wander();
|
||
} else {
|
||
enemy_observe();
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
|
||
## Summary
|
||
|
||
### Key Takeaways
|
||
|
||
1. **Determinism ≠ Synchronization**: Deterministic systems can diverge via floating-point rounding + chaos
|
||
|
||
2. **Measure Chaos**: Use Lyapunov exponents to quantify sensitivity to initial conditions
|
||
|
||
3. **Calculate Windows**: Prediction horizon = ln(error_threshold / initial_error) / λ
|
||
|
||
4. **Sync Strategies**:
|
||
- Multiplayer: Periodic state broadcast every t_horizon/2
|
||
- Procedural: Integer-only algorithms or data-driven content
|
||
- Physics: Fixed-point arithmetic or periodic correction
|
||
|
||
5. **Bounded Chaos is Useful**: Chaotic attractors give natural variation within bounds
|
||
|
||
6. **Test at Scale**: Desyncs appear at 100+ units, not 10-unit tests
|
||
|
||
### File Paths for Reference
|
||
- `/home/john/skillpacks/source/yzmir/simulation-foundations/chaos-and-sensitivity/SKILL.md`
|