microduck-electric-slide-policy / code /simple_choreography.py
Histochemichael's picture
Refine simple-v3 pivot and replace validated final render
39b3d48 verified
Raw History Blame Contribute Delete
6.64 kB
"""Ultra-simple 16-count Electric Slide retargeted for Microduck.
The sequence follows the morphology-specific choreography supplied by the
user: three travelling steps and a tap/bob in each direction, three steps
back and a tap, then rock forward/back, step forward, and pivot left. The
official 50 Hz walking policy remains active throughout; these functions only
provide dance commands and small additive joint-space targets. The free root
is never written after reset.
"""
from __future__ import annotations
from dataclasses import dataclass
import math
import numpy as np
from choreography import BEAT_SECONDS
COUNTS_PER_WALL = 16
WALLS = 4
SHOW_BEATS = COUNTS_PER_WALL * WALLS
SHOW_SECONDS = SHOW_BEATS * BEAT_SECONDS
@dataclass(frozen=True)
class SimpleCount:
label: str
swing: str
placement: str
weight_transfer: bool = True
SIMPLE_COUNTS = (
SimpleCount("right step right", "right", "side_right"),
SimpleCount("left follows right", "left", "follow_right"),
SimpleCount("right step right", "right", "side_right"),
SimpleCount("left tap + beak-snap head bob", "left", "touch_bob", False),
SimpleCount("left step left", "left", "side_left"),
SimpleCount("right follows left", "right", "follow_left"),
SimpleCount("left step left", "left", "side_left"),
SimpleCount("right tap + beak-snap head bob", "right", "touch_bob", False),
SimpleCount("right step back", "right", "back"),
SimpleCount("left step back", "left", "back"),
SimpleCount("right step back", "right", "back"),
SimpleCount("left tap", "left", "touch", False),
SimpleCount("rock forward", "none", "lean_forward", False),
SimpleCount("rock back", "none", "lean_back", False),
SimpleCount("left step forward", "left", "forward"),
SimpleCount("right scuff + pivot 90 degrees left", "right", "turn_scuff"),
)
def _smoothstep(value: float) -> float:
value = max(0.0, min(1.0, float(value)))
return value * value * (3.0 - 2.0 * value)
def count_phase(time_seconds: float) -> tuple[int, int, float]:
beat = max(0.0, time_seconds) / BEAT_SECONDS
whole = int(math.floor(beat))
return whole // COUNTS_PER_WALL, whole % COUNTS_PER_WALL, beat - whole
def policy_for_time(time_seconds: float) -> str:
"""Keep a single continuous balance policy for the complete phrase."""
return "walk"
def command_vector(time_seconds: float) -> np.ndarray:
"""Return the official walking policy's 13D command block."""
wall, count, phase = count_phase(time_seconds)
spec = SIMPLE_COUNTS[count]
command = np.zeros(13, dtype=np.float32)
groove = math.sin(2.0 * math.pi * phase)
pulse = math.sin(math.pi * phase)
command[3] = 0.030 * groove
command[4] = -0.025 * groove
command[5] = 0.070 * groove
command[6] = 0.055 * groove
command[9] = -0.004 * abs(groove)
command[10] = 0.020 * groove
if spec.placement == "turn_scuff":
command[2] = 4.9
elif spec.placement == "forward":
command[0] = 0.32
elif spec.placement == "lean_forward":
amount = _smoothstep(min(1.0, phase / 0.55)) * _smoothstep(min(1.0, (1.0 - phase) / 0.22))
command[11] += 0.075 * amount
command[3] += 0.080 * amount
command[4] -= 0.060 * amount
command[9] -= 0.008 * amount
elif spec.placement == "lean_back":
amount = _smoothstep(min(1.0, phase / 0.55)) * _smoothstep(min(1.0, (1.0 - phase) / 0.22))
command[11] -= 0.070 * amount
command[3] -= 0.060 * amount
command[4] += 0.050 * amount
if spec.placement == "touch_bob":
snap = math.sin(math.pi * min(1.0, phase / 0.52)) ** 2
command[3] += 0.145 * snap
command[4] -= 0.125 * snap
command[9] -= 0.010 * snap
if spec.weight_transfer and spec.placement != "turn_scuff":
stance_side = 1.0 if spec.swing == "right" else -1.0
transition_scale = 0.80 if count == 0 and wall > 0 else 1.0
command[8] = 0.020 * stance_side * pulse * transition_scale
command[10] += 0.045 * stance_side * pulse * transition_scale
return command
def joint_bias(time_seconds: float) -> np.ndarray:
"""Return a 14D morphology-aware additive joint target."""
wall, count, phase = count_phase(time_seconds)
spec = SIMPLE_COUNTS[count]
bias = np.zeros(14, dtype=np.float32)
pulse = math.sin(math.pi * max(0.0, min(1.0, phase)))
accent = math.sin(math.pi * _smoothstep(min(1.0, phase / 0.82)))
transition_scale = 0.80 if count == 0 and wall > 0 else 1.0
p = pulse * transition_scale
if spec.swing != "none" and spec.placement != "turn_scuff":
lift = 0.30 if spec.placement in ("touch", "touch_bob") else 0.75
if spec.swing == "left":
bias[3] += lift * pulse
bias[4] += lift * 0.80 * pulse
else:
bias[12] -= lift * pulse
bias[13] -= lift * 0.80 * pulse
if spec.placement == "side_right":
bias[10] -= 0.20 * p
bias[9] -= 0.05 * p
elif spec.placement == "follow_right":
bias[0] -= 0.12 * p
bias[1] -= 0.15 * p
elif spec.placement == "side_left":
bias[1] += 0.20 * p
bias[0] += 0.05 * p
elif spec.placement == "follow_left":
bias[9] += 0.12 * p
bias[10] += 0.15 * p
elif spec.placement == "back":
if spec.swing == "left":
bias[0] -= 0.08 * p
bias[2] += 0.30 * p
else:
bias[9] += 0.08 * p
bias[11] -= 0.30 * p
elif spec.placement == "forward":
bias[0] += 0.05 * p
bias[2] -= 0.24 * p
elif spec.placement in ("touch", "touch_bob"):
if spec.swing == "left":
bias[2] += 0.10 * accent
bias[3] += 0.10 * accent
bias[4] += 0.08 * accent
else:
bias[11] -= 0.10 * accent
bias[12] -= 0.10 * accent
bias[13] -= 0.08 * accent
elif spec.placement == "lean_forward":
bias[2] -= 0.055 * pulse
bias[11] += 0.055 * pulse
bias[4] += 0.030 * pulse
bias[13] -= 0.030 * pulse
elif spec.placement == "lean_back":
bias[2] += 0.050 * pulse
bias[11] -= 0.050 * pulse
bias[4] -= 0.025 * pulse
bias[13] += 0.025 * pulse
elif spec.placement == "turn_scuff":
bias[12] -= 0.04 * pulse
bias[13] -= 0.03 * pulse
bias[7] += 0.22 * pulse
return bias
__all__ = [
"COUNTS_PER_WALL", "SHOW_BEATS", "SHOW_SECONDS", "SIMPLE_COUNTS",
"SimpleCount", "command_vector", "count_phase", "joint_bias", "policy_for_time",
]