Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:00:05 +08:00
commit b8b320e6b3
12 changed files with 12473 additions and 0 deletions

View File

@@ -0,0 +1,966 @@
### Failure 2: Turn-Based Combat with Real-Time Physics
**Scenario**: Turn-based strategy game, designer adds physics for "smoothness."
```csharp
// WRONG: Real-time physics in turn-based game
void DealDamage(float amount) {
// RK4 integration for continuous damage animation
current_health += IntegrateODE(damage_ode, amount, dt);
// But game is turn-based!
// Problems:
// - Damage amount depends on frame rate (bad)
// - Network desync (continuous simulation can't be deterministic)
// - Player can see partial damage, rewinds to previous turn
}
```
**What Happens**:
- Same damage amount produces different results at 30fps vs 60fps
- Networked multiplayer breaks (continuous models never perfectly sync)
- UI shows health dropping, but turn hasn't resolved yet
- Save file is inconsistent (which frame's state is correct?)
**Root Cause**: Mixing continuous physics with discrete turn resolution.
### Failure 3: Discrete Events as Continuous Flow
**Scenario**: RTS game with discrete worker units, developer makes them continuous.
```python
# WRONG: Treating discrete units as continuous flow
def harvest_resources():
# Modeling units as continuous population
population = 50.0 # Can be fractional!
resources_per_second = 2.5
for t in range(1000):
population += 0.001 * (population - 50) * dt # Logistic growth???
resources += population * resources_per_second * dt
# Problems:
# - 50.3 units harvesting doesn't make sense
# - Units are discrete (add/remove whole units)
# - Continuous model obscures discrete mechanics
```
**Result**: Inconsistent with game rules, hard to verify, players confused.
### Failure 4: Quantized Resources as Continuous
**Scenario**: Factory game with discrete items, uses continuous production.
```python
# WRONG: Continuous production of discrete items
class FactoryLine:
def __init__(self):
self.output = 0.0 # Fractional items???
self.production_rate = 2.5 # items/second
def update(self, dt):
self.output += self.production_rate * dt
# Every ~0.4 seconds, you get 1 item
# Problem: When do you ACTUALLY get the item?
# At 0.4s? Rounded? This is confusing.
# Discrete model handles this naturally.
```
## GREEN Phase: Correct Choices
### 1. Continuous Models: When and Why
**Use continuous models when:**
#### 1.1 Smooth, Time-Dependent Behavior
```python
# CORRECT: Camera smoothing (continuous movement)
class ContinuousCamera:
def __init__(self, target):
self.position = Vector2(0, 0)
self.velocity = Vector2(0, 0)
def update(self, target, dt):
# Spring-damper: smooth approach to target
spring_force = 50 * (target - self.position)
damping_force = -20 * self.velocity
acceleration = spring_force + damping_force
self.velocity += acceleration * dt
self.position += self.velocity * dt
```
**Why**: Camera position is fundamentally continuous. Even at discrete update rate, we want smooth interpolation between frames.
#### 1.2 Equilibrium Systems
```python
# CORRECT: Population dynamics with stable equilibrium
class EcosystemSimulation:
def __init__(self):
self.herbivores = 100.0 # OK to be fractional (population average)
self.predators = 20.0
def update(self, dt):
# Lotka-Volterra with carrying capacity
H = self.herbivores
P = self.predators
K = 200 # Carrying capacity
dH_dt = 0.1 * H * (1 - H/K) - 0.02 * H * P
dP_dt = 0.3 * 0.02 * H * P - 0.05 * P
self.herbivores += dH_dt * dt
self.predators += dP_dt * dt
# System naturally converges to equilibrium
# No manual balancing needed
```
**Why**: System has natural equilibrium. Continuous math tells us the system is stable before we ever run it.
#### 1.3 Physics Simulations
```cpp
// CORRECT: Real-time physics engine
class PhysicsBody {
Vector3 position;
Vector3 velocity;
float mass;
void integrate(const Vector3& force, float dt) {
// Newton's second law: F = ma
Vector3 acceleration = force / mass;
velocity += acceleration * dt;
position += velocity * dt;
// Continuous model natural for physics
// Small dt → smooth trajectory
}
};
```
**Why**: Physics are inherently continuous. Position changes smoothly over time, not in discrete jumps.
### 2. Discrete Models: When and Why
**Use discrete models when:**
#### 2.1 Turn-Based Mechanics
```python
# CORRECT: Turn-based combat
class TurnBasedCombat:
def __init__(self, attacker_hp, defender_hp):
self.attacker = Player(attacker_hp)
self.defender = Player(defender_hp)
self.turn_count = 0
def execute_turn(self, attacker_action):
# Discrete state change
damage = self.calculate_damage(attacker_action)
self.defender.take_damage(damage)
self.turn_count += 1
# Health is integer (discrete)
# Damage applied instantly, not over time
# Turn resolution is atomic
return {
'damage_dealt': damage,
'turn': self.turn_count,
'defender_health': self.defender.hp
}
```
**Why**: Combat is fundamentally discrete. Players take turns, damage applies instantly, no smooth interpolation needed.
#### 2.2 Cellular Automata
```python
# CORRECT: Game of Life style simulation
class CellularAutomata:
def __init__(self, width, height):
self.grid = [[0 for _ in range(width)] for _ in range(height)]
def update(self):
# Create new grid
new_grid = copy.deepcopy(self.grid)
for y in range(len(self.grid)):
for x in range(len(self.grid[0])):
# Count live neighbors
neighbors = self.count_neighbors(x, y)
# Apply rules (discrete transitions)
if self.grid[y][x] == 1: # Cell alive
if neighbors < 2 or neighbors > 3:
new_grid[y][x] = 0 # Dies
else: # Cell dead
if neighbors == 3:
new_grid[y][x] = 1 # Born
self.grid = new_grid
def count_neighbors(self, x, y):
count = 0
for dy in [-1, 0, 1]:
for dx in [-1, 0, 1]:
if dx == 0 and dy == 0:
continue
ny, nx = y + dy, x + dx
if 0 <= ny < len(self.grid) and 0 <= nx < len(self.grid[0]):
count += self.grid[ny][nx]
return count
```
**Why**: Grid is fundamentally discrete. Cellular automata are discrete by nature. No continuous interpolation possible or useful.
#### 2.3 Quantized Resources
```cpp
// CORRECT: Discrete item inventory
class Inventory {
std::map<ItemType, int> items; // Integers only
bool add_item(ItemType type, int count) {
// Discrete: you either have 5 swords or 6 swords
// No fractional items
items[type] += count;
return true;
}
bool remove_item(ItemType type, int count) {
if (items[type] >= count) {
items[type] -= count;
return true;
}
return false; // Not enough items
}
};
```
**Why**: Items are discrete. You can't have 0.3 swords. Discrete model matches reality.
#### 2.4 Event-Driven Systems
```python
# CORRECT: Event-driven AI in Rimworld-style game
class EventDrivenAI:
def __init__(self):
self.event_queue = []
self.current_time = 0
def schedule_event(self, time, event_type, data):
self.event_queue.append({
'time': time,
'type': event_type,
'data': data
})
self.event_queue.sort(key=lambda x: x['time'])
def update(self):
# Process only events that are due
while self.event_queue and self.event_queue[0]['time'] <= self.current_time:
event = self.event_queue.pop(0)
self.handle_event(event)
def handle_event(self, event):
if event['type'] == 'PAWN_HUNGER':
pawn = event['data']
pawn.hunger += 0.1
if pawn.hunger > 0.8:
self.schedule_event(self.current_time + 1, 'PAWN_SEEK_FOOD', pawn)
```
**Why**: Events are discrete points in time. Continuous model would waste compute evaluating system when nothing happens.
### 3. Discretization: Converting Continuous → Discrete
**When you need discrete but have continuous model:**
#### 3.1 Fixed Timestep Integration
```cpp
// Discretize continuous ODE
class DiscreteEcosystem {
private:
float herbivores;
float predators;
const float fixed_dt = 0.1f; // 100ms timestep
// Continuous dynamics
void continuous_update(float dt) {
float dH = 0.1f * herbivores * (1 - herbivores/100) - 0.02f * herbivores * predators;
float dP = 0.3f * 0.02f * herbivores * predators - 0.05f * predators;
herbivores += dH * dt;
predators += dP * dt;
}
public:
void tick() {
// Evaluate ODE at discrete timesteps
continuous_update(fixed_dt);
// Now it's discretized: state only changes every 100ms
// Perfect for deterministic networked games
}
};
```
**Why**: Take continuous ODE, evaluate it at fixed time intervals. Creates deterministic discrete behavior.
#### 3.2 Accumulated Resources
```python
# CORRECT: Discretize continuous production
class FactoryLine:
def __init__(self):
self.accumulator = 0.0 # Fractional overflow
self.inventory = 0 # Discrete items
self.production_rate = 2.5 # items/second
def update(self, dt):
# Continuous production accumulates
self.accumulator += self.production_rate * dt
# When enough accumulated, create discrete item
if self.accumulator >= 1.0:
items_to_create = int(self.accumulator)
self.inventory += items_to_create
self.accumulator -= items_to_create
def get_items(self):
result = self.inventory
self.inventory = 0
return result
```
**Pattern**:
1. Continuous production into accumulator
2. When threshold reached, create discrete item
3. Best of both worlds: smooth production, discrete items
#### 3.3 Event Generation from Continuous
```python
# CORRECT: Discretize continuous probability
class DiceRoller:
def __init__(self):
self.luck_accumulator = 0.0
self.crit_chance = 0.2 # 20% continuous probability
def should_crit(self, dt):
# Continuous luck accumulates
self.luck_accumulator += self.crit_chance * dt
# Discrete event when luck exceeds 1.0
if self.luck_accumulator >= 1.0:
self.luck_accumulator -= 1.0
return True
return False
# Over 5 seconds: guaranteed 1 crit (5 * 0.2 = 1.0)
# Much better than "random check every frame"
```
### 4. Hybrid Systems
**Complex games need both:**
```python
# Hybrid: Turn-based + continuous animation
class HybridCombatSystem:
def __init__(self):
self.turn_state = 'AWAITING_INPUT'
self.battle_log = []
# Discrete: turn resolution
self.current_turn = 0
self.damage_to_apply = 0
# Continuous: animation
self.damage_animation_timer = 0.0
self.damage_animation_duration = 0.5
def resolve_turn(self, action):
"""Discrete turn logic"""
damage = self.calculate_damage(action)
self.damage_to_apply = damage
self.damage_animation_timer = 0.0
self.turn_state = 'ANIMATING_DAMAGE'
def update(self, dt):
"""Continuous animation logic"""
if self.turn_state == 'ANIMATING_DAMAGE':
# Smooth damage animation
self.damage_animation_timer += dt
progress = self.damage_animation_timer / self.damage_animation_duration
if progress >= 1.0:
# Animation done, apply discrete damage
self.player.health -= self.damage_to_apply
self.turn_state = 'AWAITING_INPUT'
else:
# Show continuous animation
self.display_damage_number(progress)
```
**Best of both worlds**:
- Turn resolution is discrete (deterministic, networkable)
- Animation is continuous (smooth, responsive)
## 5. Performance Trade-Offs
### Continuous vs Discrete Cost Analysis
| Aspect | Continuous | Discrete |
|--------|-----------|----------|
| CPU per update | O(n) numerical integration | O(n) state transitions |
| Memory | Small (just state values) | Can be large (full grids) |
| Accuracy | Depends on timestep | Perfect (by definition) |
| Interactivity | Always responsive | Only on event boundaries |
| Network sync | Hard (floating point) | Easy (exact values) |
| Predictability | Need math analysis | Inherent |
### Continuous Example (3 body problem)
```python
# Expensive: High-precision integration needed
def nbody_simulation():
bodies = [create_body() for _ in range(1000)]
for frame in range(60000): # 1000 seconds at 60fps
# RK4 integration: 4 force calculations per body
for body in bodies:
forces = sum(gravitational_force(body, other) for other in bodies)
# O(n²) force calculation
# RK4 multiplies by 4
# Total: O(4n²) per frame
# 1000 bodies: 4 million force calculations per frame
```
**Cost**: Very high CPU. Not real-time without GPU.
### Discrete Example (Cellular Automata)
```python
# Cheaper: Simple grid updates
def cellular_automata():
grid = [[random.randint(0,1) for _ in range(512)] for _ in range(512)]
for generation in range(1000):
# Simple neighbor counting
new_grid = apply_rules(grid) # O(n) where n = grid cells
# Total: O(n) per generation
# 512×512 = 262k cells, ~0.1ms to update
```
**Cost**: Very low CPU. Real-time easily.
## 6. Implementation Patterns
### Pattern 1: Difference Equations (Discrete Analog of ODEs)
```python
# WRONG: Trying to use continuous ODE as difference equation
population = 100
growth_rate = 0.1 # 10% per year
# Bad discretization
for year in range(10):
population += growth_rate * population # This is wrong timestep
# CORRECT: Difference equation
# P_{n+1} = P_n + r * P_n = P_n * (1 + r)
for year in range(10):
population = population * (1 + growth_rate)
# After 10 years:
# Difference eq: P = 100 * (1.1)^10 = 259.4
# e^(r*t) = e^(0.1*10) = e^1 = 2.718 ← This is ODE solution
# They diverge!
```
**Key**: Difference equations are discrete analogs of ODEs, but not identical.
### Pattern 2: Turn-Based with Phase Ordering
```python
# CORRECT: Deterministic turn-based system
class PhaseBasedTurns:
def __init__(self):
self.entities = []
def resolve_turn(self):
# Phase 1: Input gathering (discrete)
actions = {}
for entity in self.entities:
actions[entity] = entity.decide_action()
# Phase 2: Movement resolution (discrete)
for entity in self.entities:
entity.move(actions[entity]['direction'])
# Phase 3: Combat resolution (discrete)
for entity in self.entities:
if actions[entity]['type'] == 'ATTACK':
self.resolve_attack(entity, actions[entity]['target'])
# Order matters! Same resolution every time.
```
### Pattern 3: Event Queue with Floating-Point Time
```cpp
// CORRECT: Event system with continuous time
struct Event {
float scheduled_time;
int priority;
std::function<void()> callback;
bool operator<(const Event& other) const {
if (abs(scheduled_time - other.scheduled_time) < 1e-6) {
return priority < other.priority;
}
return scheduled_time < other.scheduled_time;
}
};
class EventSimulator {
private:
std::priority_queue<Event> event_queue;
float current_time = 0.0f;
public:
void schedule(float delay, int priority, std::function<void()> callback) {
event_queue.push({current_time + delay, priority, callback});
}
void run_until(float end_time) {
while (!event_queue.empty() && event_queue.top().scheduled_time <= end_time) {
Event e = event_queue.top();
event_queue.pop();
current_time = e.scheduled_time;
e.callback();
}
current_time = end_time;
}
};
```
**Why**: Continuous time allows arbitrary-precision event scheduling. Discrete events at continuous times.
## 7. Decision Framework
### Decision Tree
```
Do you need smooth movement/interpolation?
├─ YES → Continuous (ODE)
│ ├─ Camera, animations, physics
│ └─ Smooth transitions over time
└─ NO → Is state fundamentally discrete?
├─ YES → Discrete
│ ├─ Turn-based, grid cells, inventory
│ └─ Discrete state changes
└─ MAYBE → Check these:
├─ Players expect predictable, deterministic behavior?
│ └─ Use DISCRETE (turn-based) + continuous animation
├─ System has natural equilibrium?
│ └─ Use CONTINUOUS (ODE), discretize with fixed timestep
└─ Performance critical with complex interactions?
└─ Use DISCRETE (simpler computation)
```
## 8. Common Pitfalls
### Pitfall 1: Framerate Dependence in Discrete Systems
```python
# WRONG: Framerate-dependent discrete update
def wrong_discrete_update():
for frame in range(60000):
# This runs every frame, regardless of time
if random.random() < 0.01: # 1% chance per frame
spawn_event()
# At 30fps: 0.01 * 30 = 0.3 events/second
# At 60fps: 0.01 * 60 = 0.6 events/second (2× difference!)
```
**Fix**:
```python
# CORRECT: Time-based discrete updates
def right_discrete_update(dt):
accumulated_time += dt
while accumulated_time >= 0.01: # Fixed 10ms ticks
accumulated_time -= 0.01
if random.random() < 0.01:
spawn_event()
# Same event rate regardless of frame rate
```
### Pitfall 2: Mixing Continuous and Discrete Inconsistently
```python
# WRONG: Some things continuous, some discrete, no clear boundary
class InconsistentGame:
def update(self, dt):
# Continuous
self.player.position += self.player.velocity * dt
# Discrete (but tied to frame rate)
if self.player.position.x > 100:
self.player.deal_damage(10) # When? Exactly at boundary?
# This is fragile: behavior changes if dt changes
```
**Fix**:
```python
# CORRECT: Clear boundary between continuous and discrete
class ConsistentGame:
def update(self, dt):
# Continuous
old_x = self.player.position.x
self.player.position += self.player.velocity * dt
new_x = self.player.position.x
# Discrete event
if old_x <= 100 < new_x: # Crossed boundary
self.player.deal_damage(10)
# Always triggers exactly once per crossing
```
### Pitfall 3: Rounding Errors in Discrete Quantities
```python
# WRONG: Rounding accumulator incorrectly
def wrong_discrete_accumulation():
accumulator = 0.0
for _ in range(100):
accumulator += 0.3 # 30% per step
if accumulator >= 1.0:
create_item()
accumulator = 0 # WRONG: Loses fractional part
# After 100 steps: lost ~3.3 items due to rounding
```
**Fix**:
```python
# CORRECT: Preserve fractional overflow
def right_discrete_accumulation():
accumulator = 0.0
for _ in range(100):
accumulator += 0.3
if accumulator >= 1.0:
items = int(accumulator)
create_items(items)
accumulator -= items # Keep fractional part
# After 100 steps: exactly 30 items, perfect
```
## 9. Testing Continuous vs Discrete
```python
# Test 1: Continuous system converges to equilibrium
def test_continuous_equilibrium():
sim = ContinuousSimulation()
for _ in range(10000):
sim.update(0.01)
assert abs(sim.population - sim.equilibrium()) < 1e-6
# Test 2: Discrete system is deterministic
def test_discrete_determinism():
game1 = DiscreteGame()
game2 = DiscreteGame()
actions = [('MOVE_NORTH', 'ATTACK'), ('MOVE_EAST', 'DEFEND')]
for action in actions:
game1.apply_action(action)
game2.apply_action(action)
assert game1.get_state() == game2.get_state()
# Test 3: Discretization preserves continuous behavior
def test_discretization_accuracy():
# Continuous ODE solution
y_exact = odeint(dy_dt, y0, t_continuous)
# Discretized version
y_discrete = []
y = y0
for dt in (t_continuous[1:] - t_continuous[:-1]):
y += dy_dt(y) * dt
y_discrete.append(y)
# Error should be small
error = np.max(np.abs(y_exact - y_discrete))
assert error < 0.01 # Less than 1% error
```
## Real Scenarios
### Scenario 1: Turn-Based Tactical Combat
```python
# Discrete turn resolution + continuous animation
class TacticalCombat:
def __init__(self):
self.turn_number = 0
self.animation_timer = 0
def player_action(self, action):
# Discrete: resolve immediately
damage = roll_damage(action)
self.enemy_hp -= damage
self.turn_number += 1
# Queue animation
self.animation_timer = 0.5
def update(self, dt):
# Continuous: show animation
if self.animation_timer > 0:
self.animation_timer -= dt
progress = 1 - (self.animation_timer / 0.5)
self.render_damage_popup(progress)
```
### Scenario 2: Rimworld-Style Events
```python
# Event-driven discrete system
class RimworldEventSystem:
def __init__(self):
self.event_queue = PriorityQueue()
self.current_day = 0
def schedule_raid(self, days_until_raid):
self.event_queue.put(self.current_day + days_until_raid, 'RAID')
def update_day(self):
self.current_day += 1
while self.event_queue.peek() and self.event_queue.peek()[0] <= self.current_day:
event = self.event_queue.pop()
self.handle_event(event)
def handle_event(self, event_type):
if event_type == 'RAID':
# Discrete: happens exactly on this day
raiders = generate_raid_group()
self.place_on_map(raiders)
```
### Scenario 3: Cellular Automata (Fire Spread)
```python
# Pure discrete: grid-based, turn-based
class WildFireSimulation:
def __init__(self, width, height):
self.grid = [[0 for _ in range(width)] for _ in range(height)]
def update_generation(self):
new_grid = copy.deepcopy(self.grid)
for y in range(len(self.grid)):
for x in range(len(self.grid[0])):
if self.grid[y][x] == 1: # Burning
# Spread to neighbors
for dy in [-1, 0, 1]:
for dx in [-1, 0, 1]:
if abs(dy) + abs(dx) <= 1: # Orthogonal
ny, nx = y + dy, x + dx
if self.grid[ny][nx] == 0: # Not burning
if random.random() < 0.3: # 30% spread chance
new_grid[ny][nx] = 1
self.grid = new_grid
```
### Scenario 4: Resource Production with Quantization
```python
# Hybrid: continuous accumulation → discrete items
class FactoryProduction:
def __init__(self):
self.ore_accumulator = 0.0
self.ore_inventory = 0
self.ore_production_rate = 2.5 # ore/second
def update(self, dt):
# Continuous: accumulate production
self.ore_accumulator += self.ore_production_rate * dt
# Discrete: when 1 ore accumulated, create it
if self.ore_accumulator >= 1.0:
items = int(self.ore_accumulator)
self.ore_inventory += items
self.ore_accumulator -= items
def craft_gears(self, ore_count):
# Discrete: exactly consume and produce
if self.ore_inventory >= ore_count * 2:
self.ore_inventory -= ore_count * 2
return ore_count # Gears
return 0
```
### Scenario 5: Cellular Automata vs Continuous Diffusion
```python
# Compare both approaches to fire spread
class CellularFireSpread:
"""Discrete cellular automaton"""
def __init__(self):
self.grid = [[0.0 for _ in range(100)] for _ in range(100)]
def update(self):
new_grid = copy.deepcopy(self.grid)
for y in range(100):
for x in range(100):
if self.grid[y][x] > 0: # Burning
# Spread to neighbors (discrete rule)
for dy, dx in [(-1,0), (1,0), (0,-1), (0,1)]:
ny, nx = y + dy, x + dx
if new_grid[ny][nx] < 0.9:
new_grid[ny][nx] = 1.0 # Instant ignition
self.grid = new_grid
class ContinuousFireDiffusion:
"""Continuous diffusion equation"""
def __init__(self):
self.grid = [[0.0 for _ in range(100)] for _ in range(100)]
self.dt = 0.01
def update(self):
new_grid = copy.deepcopy(self.grid)
for y in range(1, 99):
for x in range(1, 99):
# Laplacian (diffusion)
laplacian = (self.grid[y-1][x] + self.grid[y+1][x] +
self.grid[y][x-1] + self.grid[y][x+1] - 4*self.grid[y][x])
new_grid[y][x] += 0.1 * laplacian * self.dt
self.grid = new_grid
# Cellular automaton: Fast, discrete, simple rules
# Continuous: Smooth spread, need many iterations, harder to tune
```
## Conclusion
### Decision Summary
**Use Continuous When**:
- Smooth interpolation important (camera, animation)
- Equilibrium analysis needed
- Physics-based
- Real-time feedback critical
**Use Discrete When**:
- Fundamental discrete domain (grids, items, turns)
- Deterministic behavior required (multiplayer)
- Performance critical
- Simple state transitions
**Use Hybrid When**:
- Game has both continuous and discrete aspects
- Turn resolution discrete, animation continuous
- Event-driven with continuous accumulation
**Remember**: Wrong choice = 10× performance loss or 100× accuracy loss. Choose wisely.
## Appendix: Quick Reference
### Model Selection Table
| System | Model | Why |
|--------|-------|-----|
| Camera follow | Continuous | Smooth movement |
| Turn-based combat | Discrete | Atomic state changes |
| Population dynamics | Continuous | Equilibrium analysis |
| Inventory | Discrete | Items are integers |
| Physics | Continuous | Natural motion |
| Grid automata | Discrete | Grid is inherently discrete |
| Resource production | Hybrid | Accumulation → discrete items |
| AI director | Continuous | Smooth intensity changes |
### Implementation Checklist
- [ ] Identified continuous vs discrete requirements
- [ ] Designed system boundaries (where continuous becomes discrete)
- [ ] Chose appropriate timestep (if continuous)
- [ ] Implemented accumulation pattern (if hybrid)
- [ ] Tested determinism (if discrete multiplayer)
- [ ] Tested equilibrium (if continuous)
- [ ] Verified framerate independence
- [ ] Performance validated against budget
**End of Skill**
*Part of `yzmir/simulation-foundations`. See also: `differential-equations-for-games`, `stability-analysis`, `state-space-modeling`.*