test: add zero-replan live pathing harness

This commit is contained in:
BruceChen 2026-04-13 15:36:39 +00:00
parent b0fab26c66
commit aefc7235ee
7 changed files with 3444 additions and 146 deletions

View file

@ -258,8 +258,57 @@ The new regression harness in `tools/test-pathing-template-regressions.sh` codif
4. A 3×1 no-run-up rejection to prevent non-executable plans from sneaking through.
5. Mixed ascend/descend/climb smoke cases so that both vertical transitions and ladder climbs respect the reliable support requirement.
## Deterministic live route contract
For the short-route and long-route `1.21.11-Vanilla` live harnesses, accepted routes must complete with all of the following:
- `A* result: Success`
- `0 replan`
- `0` template segment failures
- final position inside the intended goal support block
- `PathMgr` reporting `Navigation complete!`
For rejection scenarios, the requirement is stricter:
- `A* result: Failed` or `No path found`
- no navigation start
- no executor-driven `replan`
Residual speed carried from one movement to the next inside a route is expected and must not be normalized away just to satisfy the harness. The route is only considered reliable if that natural speed carry still produces `0 replan`.
## Baritone Reference Notes For Zero-Replan Work
MCC can borrow specific ideas from the local Baritone reference under `ThirdpartyReference/baritone/`, but not its looser success semantics.
Borrow:
- landing-aware completion, where movement logic keeps controlling after touchdown instead of failing immediately
- next-movement-aware descend and ascend handoff behavior
- conservative parkour admissibility, especially around run-up, overshoot, and blocked landing shapes
- executor timeout and movement-stuck heuristics as diagnostic input, not as acceptance criteria
Do not borrow:
- `GoalBlock` occupancy semantics as a substitute for deterministic execution quality
- executor repath tolerance as proof that a movement is reliable
- any behavior that lets accepted deterministic harness routes succeed only by falling back to `replan`
For this work, Baritone is a movement-control reference, not a correctness oracle. MCC's accepted live routes must still finish with `0 replan` in the deterministic harness.
Keeping the rule explicit here reminds future contributors that the planner should never promise a move that physically cannot finish with block contact.
## Regression Harness Workflow
The scripts in `tools/` now match the `mcc-dev-workflow` defaults: they call
`source tools/mcc-env.sh`, rely on a shared `mc-*` server running `1.21.11-Vanilla`,
and launch MCC through `mcc-build`, `mcc-debug`, and `mcc-cmd` wrappers. The
harnesses reuse the existing server session instead of stopping and restarting
it, which keeps shared test infrastructure stable and honors the instruction to
keep `mc-*` servers running unless another version or explicit reset is required.
When editing or extending the harness, preserve the `mcc-*` invocation pattern
and the existing log/tail helpers so the scripts stay compatible with the updated
workflow.
## References
- [Minecraft Parkour Wiki: Blip](https://www.mcpk.wiki/wiki/Blip)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,736 @@
# Zero Replan Live Pathing Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Eliminate executor-driven replans in deterministic live harness scenarios on `1.21.11-Vanilla`, and add longer-route stability coverage that proves accepted routes finish with `0 replan`.
**Architecture:** Treat any `replan` in accepted deterministic harness scenarios as a bug, not an acceptable recovery path. First lock down harness isolation and `0 replan` assertions so failures are explicit. Then remove per-move-family completion/failure gaps in `WalkTemplate`, `AscendTemplate`, and `DescendTemplate`, and finally add longer multi-segment live routes that combine operations without planner partials or executor retries.
**Tech Stack:** C# 14 / .NET 10, xUnit, MCC local `1.21.11-Vanilla` harness via `tools/mcc-env.sh`, tmux/RCON-driven live tests, MCC pathing core and execution templates.
---
## Current Facts
Latest live evidence from `/tmp/mcc-debug/*/mcc-debug.log`:
- `Flat final stop`: `2` replans
- `Traverse` failed at `(101.56, 80.00, 100.74)`
- `Traverse/FinalStop` failed at `(103.36, 80.00, 100.50)`
- `Parkour into turn`: `1` replan
- final `Traverse/FinalStop` failed around `(122.50, 80.00, 111.36)`
- `Corner ascend around wall`: `1` replan
- single `Ascend/FinalStop` failed around `(191.38, 81.00, 171.38)`
- `Wall-adjacent descend`: `1` replan
- single `Descend/FinalStop` failed at `(201.50, 80.00, 200.50)`
- `Ascend chain smoke`: `3` replans
- all failures occur on chained `Ascend` segments before the partial fallback stops at `(177.46, 83.00, 162.50)`
- `Rejected 3x1 no-run-up gap`: currently not a clean reject
- planner returns a `Partial` path, then execution replans before failing
- `Rejected 2x1 side-wall jump`: already correct, `0 replan`, direct `A* Failed`
## User Constraints
These constraints override earlier assumptions in this plan:
- MCC currently has no entity collision implementation that would make other players or mobs perturb these tests.
- Other players being online is not itself a movement interference source for this work.
- Residual yaw/pitch between independent scenarios is not a fix target for this plan; templates already steer every tick.
- Residual speed should be recorded for diagnosis, but not “normalized away” inside a route.
- For long accepted routes, residual speed between internal actions is expected and must not be treated as test interference. The route should still finish with `0 replan`.
## Interference Inventory
The harness already disables or controls the environmental variables that matter for this work:
- `difficulty peaceful`
- `gamerule doMobSpawning false`
- fixed test geometry with `fill`/`setblock`
- explicit `tp` before each scenario
Remaining sources of ambiguity that still matter before claiming `0 replan`:
1. Shared live server state
- The workflow uses shared `mc-*` tmux sessions.
- This matters for repeatability and logging, but not because of entity collision.
2. Reused MCC session state across scenarios
- The harness runs multiple scenarios in the same MCC session.
- Residual speed can carry across scenario boundaries if the next scenario starts too early.
- Yaw/pitch carryover is not considered a blocker for this plan.
3. Planner timeout / partial-path behavior
- Some “reject” or long-route scenarios currently hit `A* result: Partial`.
- Those cases cannot be used as `0 replan` executor proofs until the route size is kept below timeout, or the test is explicitly categorized as a planner-partial case.
4. Current live harness acceptance is weaker than target behavior
- The harness was updated to accept “already in goal block” completion.
- That is useful for keeping live validation running, but it currently masks the stronger requirement that accepted deterministic routes should need `0 replan`.
5. Residual-speed observability is currently weak
- The harness captures final location, but it does not systematically record pre-route speed and per-route terminal speed.
- We should measure speed, not try to zero it between internal actions.
## Zero-Replan Contract
For this plan, an accepted live pathing scenario passes only if all of the following are true:
- `A* result: Success`
- no `A* result: Partial`
- no `Replan #`
- no `Segment .* FAILED`
- no `Replan failed -- no path found`
- final MCC location is inside the intended goal support block
- `PathMgr` reaches `Navigation complete!`
For rejection scenarios, pass only if:
- `A* result: Failed` or `No path found`
- no `Navigation started`
- no `Replan #`
## File Structure
### Production code
- `MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs`
- Traverse and diagonal segment runtime; primary target for straight-line and turn-entry `0 replan`.
- `MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs`
- Single-step ascend execution; primary target for ascend landing and chained ascend stability.
- `MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs`
- Landing/final-stop descend execution.
- `MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs`
- Shared grounded completion and braking gate; likely the common source of “already good enough, but segment still fails one tick later”.
- `MinecraftClient/Pathing/Execution/Templates/TemplateHelper.cs`
- Shared geometry and settle helpers.
- `MinecraftClient/Pathing/Execution/PathSegmentManager.cs`
- Replan handling and live navigation orchestration.
- `MinecraftClient/Pathing/Core/AStarPathFinder.cs`
- Planner timeout and partial-path behavior.
- `MinecraftClient/Pathing/Moves/Impl/MoveParkour.cs`
- Run-up / jump-feasibility checks for the `3x1` rejection case.
- `MinecraftClient/Pathing/Moves/ParkourFeasibility.cs`
- Supporting parkour admissibility logic.
- `ThirdpartyReference/baritone/src/main/java/baritone/pathing/path/PathExecutor.java`
- Reference executor behavior for timeout, splice/repath, and movement handoff semantics.
- `ThirdpartyReference/baritone/src/main/java/baritone/pathing/movement/movements/MovementTraverse.java`
- Reference traverse completion semantics.
- `ThirdpartyReference/baritone/src/main/java/baritone/pathing/movement/movements/MovementAscend.java`
- Reference ascend landing and post-jump settle logic.
- `ThirdpartyReference/baritone/src/main/java/baritone/pathing/movement/movements/MovementDescend.java`
- Reference descend safe-mode and landing behavior.
- `ThirdpartyReference/baritone/src/main/java/baritone/pathing/movement/movements/MovementParkour.java`
- Reference parkour admissibility and handoff behavior.
### Tests and harness
- `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- Deterministic convergence tests for walk, descend, and future ascend/diagonal cases.
- `MinecraftClient.Tests/Pathing/Execution/PathSegmentManagerTests.cs`
- Manager-level replan and already-in-goal behavior.
- `MinecraftClient.Tests/Pathing/Execution/PathExecutorCompletionTests.cs`
- Multi-segment executor behavior.
- `MinecraftClient.Tests/Pathing/Execution/SprintJumpTemplateScenarioTests.cs`
- Parkour and landing handoff cases.
- `tools/test-transition-braking.sh`
- Short accepted-route live harness.
- `tools/test-pathing-template-regressions.sh`
- Current mixed live regression suite.
- `tools/test-pathing-long-routes.sh`
- New long-route `0 replan` live suite.
- `docs/guide/pathfinding-research.md`
- Pathing behavior contract and live test expectations.
---
### Task 1: Freeze the Zero-Replan Harness Contract
**Files:**
- Modify: `tools/test-transition-braking.sh`
- Modify: `tools/test-pathing-template-regressions.sh`
- Create: `tools/test-pathing-long-routes.sh`
- Modify: `docs/guide/pathfinding-research.md`
- [ ] **Step 1: Add live log helpers that fail on any accepted-route replan**
Add shell helpers to both existing harness scripts:
```bash
count_replans_since() {
local from_line="$1"
log_since "$from_line" | grep -Ec "\\[PathMgr\\] Replan #|\\[PathExec\\] Segment .* FAILED" || true
}
assert_no_replans_since() {
local from_line="$1"
local count
count="$(count_replans_since "$from_line")"
if [[ "$count" != "0" ]]; then
echo "Expected 0 replans, saw $count" >&2
log_since "$from_line" >&2
return 1
fi
}
assert_no_partial_since() {
local from_line="$1"
if log_since "$from_line" | grep -Fq "[Navigate] A* result: Partial"; then
echo "Expected full success path, saw partial path" >&2
log_since "$from_line" >&2
return 1
fi
}
```
- [ ] **Step 2: Normalize only the state that should differ between independent scenarios**
For accepted live scenarios, add:
```bash
mc-rcon "effect clear $USERNAME" >/dev/null 2>&1 || true
mc-rcon "tp $USERNAME 100.5 80 100.5" >/dev/null
sleep 2
send_mcc "debug state"
```
Do not add special yaw/pitch normalization. Record the pre-route state instead.
Do reset position and allow enough time that residual speed from the previous independent scenario is observable in logs.
- [ ] **Step 3: Separate accepted-route assertions from rejection-route assertions**
Update accepted-route scenarios to require:
```bash
wait_for_navigation "$start_line" 30
assert_no_partial_since "$start_line"
assert_no_replans_since "$start_line"
```
Update rejection scenarios to require:
```bash
wait_for_failure_signal "$start_line" 20
if log_since "$start_line" | grep -Eq "\\[PathMgr\\] Replan #|\\[PathExec\\] Segment .* FAILED"; then
echo "Expected direct rejection, saw execution replan" >&2
return 1
fi
```
- [ ] **Step 4: Create a new long-route live harness**
Create `tools/test-pathing-long-routes.sh` with three accepted-route buckets:
```bash
run_same_move_routes
run_mixed_move_routes
run_turn_density_routes
```
Each accepted route must assert:
```bash
wait_for_navigation "$start_line" 45
assert_no_partial_since "$start_line"
assert_no_replans_since "$start_line"
read -r x y z <<< "$(capture_debug_location)"
assert_inside_goal_block "$x" "$y" "$z" "$goal_x" "$goal_y" "$goal_z"
```
Each accepted route must also log:
```bash
capture_debug_state_before_route
capture_debug_state_after_route
```
to record start/end speed and location for diagnosis. Do not reset any state between internal actions of a single route.
- [ ] **Step 5: Document the zero-replan live contract**
Add a short section to `docs/guide/pathfinding-research.md`:
```md
### Deterministic live route contract
For the short-route and long-route 1.21.11 live harnesses, accepted routes must complete with:
- `A* result: Success`
- `0 replan`
- `0` template segment failures
- final position inside the goal support block
Rejection scenarios must fail before execution starts.
```
- [ ] **Step 6: Run harness baselines and record current failures**
Run:
```bash
source tools/mcc-env.sh
bash tools/test-transition-braking.sh 1.21.11-Vanilla
bash tools/test-pathing-template-regressions.sh 1.21.11-Vanilla
```
Expected right now: FAIL because the accepted scenarios still produce replans.
---
### Task 2: Baritone Reference Pass Before Code Changes
**Files:**
- Read: `ThirdpartyReference/baritone/src/main/java/baritone/pathing/path/PathExecutor.java`
- Read: `ThirdpartyReference/baritone/src/main/java/baritone/pathing/movement/movements/MovementTraverse.java`
- Read: `ThirdpartyReference/baritone/src/main/java/baritone/pathing/movement/movements/MovementAscend.java`
- Read: `ThirdpartyReference/baritone/src/main/java/baritone/pathing/movement/movements/MovementDescend.java`
- Read: `ThirdpartyReference/baritone/src/main/java/baritone/pathing/movement/movements/MovementParkour.java`
- Modify: `docs/guide/pathfinding-research.md`
- [ ] **Step 1: Extract only the behavior that applies to MCCs zero-replan goal**
Summarize, in MCC terms:
- when Baritone considers a movement complete
- when it keeps controlling after landing instead of immediately failing
- how it handles path executor timeout and repath
- which parkour and descend transitions depend on next-movement awareness
- [ ] **Step 2: Write down the allowed and disallowed Baritone borrow list**
Add to `docs/guide/pathfinding-research.md`:
```md
### Baritone reference notes for zero-replan work
Borrow:
- landing-aware completion
- next-movement-aware descend/ascend handoff
- conservative parkour admissibility
Do not borrow:
- GoalBlock occupancy semantics that allow success with sloppy live execution
- executor repath tolerance as a substitute for deterministic harness stability
```
- [ ] **Step 3: Do not change MCC code in this task**
This task is design grounding only. The output is a short written comparison that later tasks can cite.
---
### Task 3: Reproduce Each Replan Family With Deterministic Tests
**Files:**
- Modify: `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- Modify: `MinecraftClient.Tests/Pathing/Execution/PathExecutorCompletionTests.cs`
- Modify: `MinecraftClient.Tests/Pathing/Execution/PathSegmentManagerTests.cs`
- Modify: `MinecraftClient.Tests/Pathing/Execution/SprintJumpTemplateScenarioTests.cs`
- [ ] **Step 1: Add a walk final-stop red test for the live `(103.36, 80.00, 100.50)` case**
Add a test that seeds physics at the live near-goal position and expects completion without failure:
```csharp
[Fact]
public void WalkTemplate_FinalStop_Completes_FromLiveNearGoalState_WithoutFailure()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 95, max: 115);
var segment = new PathSegment
{
Start = new Location(102.5, 80, 100.5),
End = new Location(103.5, 80, 100.5),
MoveType = MoveType.Traverse,
ExitTransition = PathTransitionType.FinalStop
};
var template = new WalkTemplate(segment, null);
var physics = new PlayerPhysics
{
Position = new Vec3d(103.36, 80.0, 100.50),
DeltaMovement = new Vec3d(0.0346, 0.0, 0.0),
OnGround = true,
MovementSpeed = 0.1f,
Yaw = 270f
};
TemplateState state = TemplateSimulationRunner.Run(template, physics, world, 40, out _);
Assert.Equal(TemplateState.Complete, state);
}
```
- [ ] **Step 2: Add an ascend final-stop red test for the live `(191.38, 81.00, 171.38)` case**
```csharp
[Fact]
public void AscendTemplate_FinalStop_Completes_FromLiveLandingState_WithoutFailure()
{
// build the same corner-ascend world as the live harness
// seed physics at the live failure position
// expect TemplateState.Complete within a short horizon
}
```
- [ ] **Step 3: Add a descend final-stop red test for the live `(201.50, 80.00, 200.50)` case**
```csharp
[Fact]
public void DescendTemplate_FinalStop_Completes_FromLiveLandingState_WithoutFailure()
{
// build the wall-adjacent descend world
// seed physics at the live failure position
// expect TemplateState.Complete
}
```
- [ ] **Step 4: Add a manager-level red test that accepted routes finish with zero replans**
Extend `PathSegmentManagerTests.cs` with a short accepted path:
```csharp
[Fact]
public void Tick_ShortAcceptedPath_CompletesWithoutIncrementingReplanCount()
{
// start manager with a 3-node flat path
// run ticks until completion
// assert manager.ReplanCount == 0
}
```
- [ ] **Step 5: Run the focused red test set**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~GroundedTemplateConvergenceTests|FullyQualifiedName~PathExecutorCompletionTests|FullyQualifiedName~PathSegmentManagerTests|FullyQualifiedName~SprintJumpTemplateScenarioTests" -v minimal
```
Expected initially: FAIL on the new live-parity cases.
---
### Task 4: Remove Zero-Replan Gaps In Walk And FinalStop Execution
**Files:**
- Modify: `MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs`
- Modify: `MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs`
- Modify: `MinecraftClient/Pathing/Execution/Templates/TemplateHelper.cs`
- Modify: `MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs`
- [ ] **Step 1: Make `WalkTemplate` prefer completion over fail once goal support is already valid**
Before the template returns `TemplateState.Failed`, ensure it checks an explicit “good enough for this segment” predicate first:
```csharp
if (GroundedSegmentController.ShouldComplete(_segment, pos, physics))
return TemplateState.Complete;
if (_stuckTicks > 40 || _tickCount > maxTicks)
return TemplateState.Failed;
```
The final implementation must preserve that order even when the bot is slightly off center but already inside valid goal support.
- [ ] **Step 2: Tighten final-stop control so the last traverse segment stops without lateral drift**
Update planner/controller logic for `PathTransitionType.FinalStop` to penalize cross-axis drift near the end plane:
```csharp
double lateralError = TemplateHelper.CrossTrackDistance(pos, segment);
if (segment.ExitTransition == PathTransitionType.FinalStop && lateralError > 0.10)
{
// reduce forward carry and bias facing back to segment heading
}
```
- [ ] **Step 3: Ensure `ContinueStraight -> FinalStop` handoff drops sprint early enough**
Use the final live traces as the acceptance reference:
- flat final stop must not replan at segment `0`
- final stop segment must complete before the `(103.36, 80.00, 100.50)` failure state
- do not rely on yaw/pitch reset to make this pass
- residual speed within the route is allowed as long as the route still completes with `0 replan`
- [ ] **Step 4: Run focused deterministic tests**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~WalkTemplate_FinalStop|FullyQualifiedName~PathExecutorCompletionTests|FullyQualifiedName~PathSegmentManagerTests" -v minimal
```
Expected: PASS with the new walk/final-stop tests green.
---
### Task 5: Remove Zero-Replan Gaps In Ascend Execution
**Files:**
- Modify: `MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs`
- Modify: `MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs`
- Modify: `MinecraftClient/Pathing/Execution/Templates/TemplateHelper.cs`
- Modify: `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- [ ] **Step 1: Add a dedicated post-landing settle phase in `AscendTemplate`**
Current code always sets:
```csharp
input.Forward = true;
input.Sprint = true;
```
Replace that with a landing-aware phase:
```csharp
bool landedOnTargetLevel = physics.OnGround && Math.Abs(dy) < 0.2;
if (landedOnTargetLevel)
{
GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world);
if (GroundedSegmentController.ShouldComplete(_segment, pos, physics))
return TemplateState.Complete;
}
else
{
input.Forward = true;
input.Sprint = true;
if (physics.OnGround && dy > 0.1)
input.Jump = true;
}
```
The acceptance bar is not “zero residual speed after each ascend”. The acceptance bar is “the accepted route continues without a replan”.
- [ ] **Step 2: Add live-parity ascend tests**
Add deterministic tests for:
- corner ascend final stop
- chained ascend middle segment
- chained ascend final segment
- [ ] **Step 3: Run the focused ascend suite**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~Ascend|FullyQualifiedName~Corner ascend|FullyQualifiedName~PathSegmentManagerTests" -v minimal
```
Expected: PASS with no new regressions in existing ascend tests.
---
### Task 6: Remove Zero-Replan Gaps In Descend Execution
**Files:**
- Modify: `MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs`
- Modify: `MinecraftClient/Pathing/Execution/Templates/TemplateHelper.cs`
- Modify: `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- [ ] **Step 1: Make landing-state final stop complete immediately when support is already valid**
Preserve the landing-phase completion ordering:
```csharp
if (physics.OnGround && Math.Abs(dy) < 1.0)
{
GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world);
if (GroundedSegmentController.ShouldComplete(_segment, pos, physics))
return TemplateState.Complete;
}
```
The acceptance case is the exact live failure state at `(201.50, 80.00, 200.50)`.
- [ ] **Step 2: Add descend live-parity tests**
Add tests that seed the exact live landing state and assert `TemplateState.Complete` rather than `Failed`.
- [ ] **Step 3: Run the focused descend suite**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~DescendTemplate|FullyQualifiedName~GroundedTemplateConvergenceTests" -v minimal
```
Expected: PASS with the new descend test green.
---
### Task 7: Make Rejection Scenarios Reject Before Execution Starts
**Files:**
- Modify: `MinecraftClient/Pathing/Core/AStarPathFinder.cs`
- Modify: `MinecraftClient/Pathing/Moves/Impl/MoveParkour.cs`
- Modify: `MinecraftClient/Pathing/Moves/ParkourFeasibility.cs`
- Modify: `tools/test-pathing-template-regressions.sh`
- [ ] **Step 1: Add a deterministic rejection test for the `3x1 no-run-up` case**
Create or extend a pathfinder-level test that asserts:
```csharp
Assert.Equal(PathStatus.Failed, result.Status);
Assert.Empty(PathSegmentBuilder.FromPath(result.Path));
```
for the exact live `141.5 -> 144.5/81/138.5` layout.
- [ ] **Step 2: Tighten parkour admissibility before planner partial fallback is considered acceptable**
Review the current path:
- `Traverse -> Ascend` to a partial fallback at `(143,81,138)`
The fix should make this route non-admissible in the first place if the intended gap cannot be completed with valid run-up.
- Use `ThirdpartyReference/baritone/.../MovementParkour.java` and the prior admissibility spec as references, but keep MCCs execution contract stricter: direct reject in this harness.
- [ ] **Step 3: Re-run rejection-only live validation**
Run:
```bash
source tools/mcc-env.sh
bash tools/test-pathing-template-regressions.sh 1.21.11-Vanilla
```
Expected for rejection scenarios:
- `2x1 side-wall jump`: direct reject, `0 replan`
- `3x1 no-run-up gap`: direct reject, `0 replan`
---
### Task 8: Add Long-Route Zero-Replan Stability Coverage
**Files:**
- Create: `tools/test-pathing-long-routes.sh`
- Modify: `docs/guide/pathfinding-research.md`
- [ ] **Step 1: Add same-operation long routes**
Use route lengths that remain comfortably below planner timeout:
- straight traverse chain: `8-12` blocks
- diagonal zig-zag chain: `6-8` segments
- ascend staircase: `4-6` ascends
- descend staircase: `4-6` descends
- aligned parkour chain: `3-4` jumps
Each must require:
- `A* result: Success`
- `0 replan`
- final location inside goal block
- [ ] **Step 2: Add mixed-operation long routes**
Recommended mixed routes:
- `Traverse -> Turn -> Parkour -> Turn -> Traverse -> FinalStop`
- `Diagonal -> Ascend -> Traverse -> Descend -> FinalStop`
- `Traverse -> Ascend -> Traverse -> Parkour -> Descend -> FinalStop`
Keep all mixed routes inside a small pre-cleared test region so chunk loading is not the variable under test.
Do not reset speed or orientation between internal actions of a route. A route only passes if the naturally carried speed across those actions still yields `0 replan`.
- [ ] **Step 3: Add turn-density routes**
Add one route with frequent heading changes and no jumps:
- `8-10` short traverse/diagonal segments
- every segment should change heading
- must still finish with `0 replan`
- [ ] **Step 4: Add speed-carry long routes**
Add one route each for:
- repeated `Traverse -> Ascend`
- repeated `Traverse -> Descend`
- repeated `Traverse -> Parkour`
These routes exist specifically to prove that residual speed between actions does not force replans in deterministic conditions.
- [ ] **Step 5: Run the long-route harness**
Run:
```bash
source tools/mcc-env.sh
bash tools/test-pathing-long-routes.sh 1.21.11-Vanilla
```
Expected: PASS with every accepted route showing `0 replan`.
---
### Task 9: Full Verification
**Files:**
- No new files
- [ ] **Step 1: Run deterministic test coverage**
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~Pathing.Execution" -v minimal
```
Expected: PASS, `0 failed`.
- [ ] **Step 2: Run release build**
```bash
source tools/mcc-env.sh
mcc-build
```
Expected: `Build succeeded. 0 Warning(s), 0 Error(s)`.
- [ ] **Step 3: Run short live suites**
```bash
source tools/mcc-env.sh
bash tools/test-transition-braking.sh 1.21.11-Vanilla
bash tools/test-pathing-template-regressions.sh 1.21.11-Vanilla
```
Expected:
- accepted scenarios: `0 replan`
- rejection scenarios: direct reject, `0 replan`
- [ ] **Step 4: Run long-route live suite**
```bash
source tools/mcc-env.sh
bash tools/test-pathing-long-routes.sh 1.21.11-Vanilla
```
Expected: PASS, no accepted route triggers `replan`.
## Self-Review
Spec coverage:
- `0 replan` short deterministic live scenarios: covered by Tasks 1-6
- interference inventory first: covered near top of this plan, revised to remove non-applicable entity and yaw/pitch concerns
- Baritone comparison before code changes: covered by Task 2
- rejection cleanup: covered by Task 7
- long-path stability coverage, including speed-carry routes: covered by Task 8
Placeholder scan:
- No `TODO` or `TBD` placeholders remain
- Concrete files, scenarios, and commands are included
Type consistency:
- File paths and move/template names match current codebase names:
- `WalkTemplate`
- `AscendTemplate`
- `DescendTemplate`
- `GroundedSegmentController`
- `AStarPathFinder`

View file

@ -0,0 +1,398 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$REPO_ROOT/tools/mcc-env.sh"
VERSION="${1:-1.21.11-Vanilla}"
SESSION="mcc-pathing-jump-combos"
USERNAME="CursorBot"
SESSION_ROOT="$(_mcc_session_root "$SESSION")"
LOG="$(_mcc_session_log_file "$SESSION")"
PASSED_CASES=()
FAILED_CASES=()
cleanup() {
mcc-kill --session "$SESSION" >/dev/null 2>&1 || true
}
trap cleanup EXIT
send_mcc() {
mcc-cmd --session "$SESSION" "$1"
}
log_line_count() {
if [[ -f "$LOG" ]]; then
wc -l < "$LOG"
else
echo 0
fi
}
log_since() {
local from_line="$1"
if [[ ! -f "$LOG" ]]; then
return
fi
tail -n +"$((from_line + 1))" "$LOG"
}
log_since_clean() {
log_since "$1" | sed 's/\x1b\[[0-9;]*m//g'
}
wait_for_log() {
local pattern="$1"
local from_line="${2:-0}"
local timeout="${3:-30}"
for _ in $(seq 1 "$timeout"); do
if log_since_clean "$from_line" | grep -Fq "$pattern"; then
return 0
fi
sleep 1
done
return 1
}
wait_for_navigation() {
local from_line="$1"
local timeout="${2:-45}"
local saw_start=0
for _ in $(seq 1 "$timeout"); do
local recent
recent="$(log_since_clean "$from_line")"
if grep -Fq "[PathMgr] Navigation started" <<<"$recent"; then
saw_start=1
fi
if (( saw_start )) && grep -Fq "[PathMgr] Navigation complete!" <<<"$recent"; then
return 0
fi
if grep -Eq "\[PathMgr\] (Replan failed|Giving up)" <<<"$recent"; then
echo "$recent" >&2
return 1
fi
if grep -Eq "No path found|\[Navigate\] A\* result: Failed" <<<"$recent"; then
echo "$recent" >&2
return 1
fi
sleep 1
done
echo "Timed out waiting for navigation completion" >&2
log_since_clean "$from_line" >&2
return 1
}
count_replans_since() {
local from_line="$1"
log_since_clean "$from_line" | grep -Ec '\[PathMgr\] Replan #|\[PathExec\] Segment .* FAILED' || true
}
assert_no_replans_since() {
local from_line="$1"
local count
count="$(count_replans_since "$from_line")"
if [[ "$count" != "0" ]]; then
echo "Expected 0 replans, saw $count" >&2
log_since_clean "$from_line" >&2
return 1
fi
}
assert_no_partial_since() {
local from_line="$1"
if log_since_clean "$from_line" | grep -Fq "[Navigate] A* result: Partial"; then
echo "Expected full success path, saw partial path" >&2
log_since_clean "$from_line" >&2
return 1
fi
}
debug_state_snapshot() {
local label="$1"
local from_line
from_line="$(log_line_count)"
send_mcc "debug state"
wait_for_log "Location" "$from_line" 10
echo ""
echo "=== Debug state: $label ==="
log_since_clean "$from_line"
}
capture_debug_state_before_route() {
debug_state_snapshot "before route - $1"
}
capture_debug_state_after_route() {
debug_state_snapshot "after route - $1"
}
capture_debug_location() {
local start_line
start_line="$(log_line_count)"
send_mcc "debug state"
wait_for_log "Location" "$start_line" 10
extract_last_location "$start_line"
}
wait_for_location_in_block() {
local expected_x="$1"
local expected_y="$2"
local expected_z="$3"
local timeout="${4:-10}"
for _ in $(seq 1 "$timeout"); do
local actual_x actual_y actual_z
read -r actual_x actual_y actual_z <<< "$(capture_debug_location)"
if python3 - <<'PY' "$actual_x" "$actual_y" "$actual_z" "$expected_x" "$expected_y" "$expected_z"
import math
import sys
ax, ay, az, ex, ey, ez = map(float, sys.argv[1:])
if math.floor(ax) == int(ex) and math.floor(az) == int(ez) and abs(ay - ey) <= 0.05:
raise SystemExit(0)
raise SystemExit(1)
PY
then
return 0
fi
sleep 1
done
echo "Timed out waiting for player to reach start block ($expected_x, $expected_y, $expected_z)" >&2
return 1
}
prepare_independent_route() {
local label="$1"
local start_x="$2"
local start_y="$3"
local start_z="$4"
echo ""
echo "Preparing independent route: $label"
mc-rcon "effect clear $USERNAME" >/dev/null 2>&1 || true
mc-rcon "tp $USERNAME $start_x $start_y $start_z" >/dev/null
wait_for_location_in_block "$start_x" "$start_y" "$start_z" 10
}
extract_last_location() {
local from_line="${1:-0}"
python3 - "$LOG" "$from_line" <<'PY'
import pathlib
import re
import sys
log_path = pathlib.Path(sys.argv[1])
from_line = int(sys.argv[2])
text = log_path.read_text(errors="ignore")
text = "\n".join(text.splitlines()[from_line:])
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
matches = re.findall(r"Location\s+([-,0-9.]+),\s+([-,0-9.]+),\s+([-,0-9.]+)", text)
if not matches:
raise SystemExit("No Location line found in MCC log")
x, y, z = matches[-1]
print(f"{x} {y} {z}")
PY
}
assert_inside_goal_block() {
local actual_x="$1"
local actual_y="$2"
local actual_z="$3"
local expected_x="$4"
local expected_y="$5"
local expected_z="$6"
python3 - <<'PY' "$actual_x" "$actual_y" "$actual_z" "$expected_x" "$expected_y" "$expected_z"
import math
import sys
ax, ay, az, ex, ey, ez = map(float, sys.argv[1:])
if math.floor(ax) != int(ex) or math.floor(az) != int(ez) or abs(ay - ey) > 0.05:
raise SystemExit(
f"Expected location inside goal block ({int(ex)}, {ey:.2f}, {int(ez)}), got ({ax:.2f}, {ay:.2f}, {az:.2f})"
)
PY
}
print_summary() {
local header="$1"
echo ""
echo "----- $header -----"
if [[ -f "$LOG" ]]; then
tail -n 40 "$LOG" | sed 's/\x1b\[[0-9;]*m//g'
else
echo "(no log available yet)"
fi
}
fill_box() {
mc-rcon "fill $1 $2 $3 $4 $5 $6 $7" >/dev/null
}
set_stone() {
mc-rcon "setblock $1 $2 $3 stone" >/dev/null
}
run_accepted_route() {
local label="$1"
local start_x="$2"
local start_y="$3"
local start_z="$4"
local goal_x="$5"
local goal_y="$6"
local goal_z="$7"
local timeout="${8:-45}"
prepare_independent_route "$label" "$start_x" "$start_y" "$start_z"
capture_debug_state_before_route "$label"
local start_line
start_line="$(log_line_count)"
send_mcc "pathfind $goal_x $goal_y $goal_z"
wait_for_navigation "$start_line" "$timeout"
assert_no_partial_since "$start_line"
assert_no_replans_since "$start_line"
capture_debug_state_after_route "$label"
local x y z
read -r x y z <<< "$(capture_debug_location)"
echo " Final location: $x $y $z"
assert_inside_goal_block "$x" "$y" "$z" "$goal_x" "$goal_y" "$goal_z"
print_summary "$label"
}
run_case() {
local label="$1"
shift
echo ""
echo "== $label =="
set +e
(
set -e
"$@"
)
local status=$?
set -e
if [[ $status -eq 0 ]]; then
PASSED_CASES+=("$label")
echo "RESULT: PASS - $label"
else
FAILED_CASES+=("$label")
echo "RESULT: FAIL - $label"
fi
}
start_mcc() {
mkdir -p "$SESSION_ROOT"
mcc-kill --session "$SESSION" >/dev/null 2>&1 || true
mcc-build >/dev/null
mcc-debug -v "$VERSION" --session "$SESSION" --username "$USERNAME" --file-input --debug-on --no-build >/dev/null
wait_for_log "Server was successfully joined." 0 40
send_mcc "debug on"
}
scenario_repeated_cardinal_parkour() {
fill_box 578 79 578 590 79 582 air
fill_box 578 80 578 590 90 582 air
set_stone 580 79 580
set_stone 582 79 580
set_stone 584 79 580
set_stone 586 79 580
set_stone 588 79 580
run_accepted_route "Repeated jump - cardinal parkour chain" "580.5" "80" "580.5" "588" "80.00" "580"
}
scenario_repeated_diagonal_parkour() {
fill_box 598 79 598 608 79 608 air
fill_box 598 80 598 608 90 608 air
set_stone 600 79 600
set_stone 602 79 602
set_stone 604 79 604
set_stone 606 79 606
run_accepted_route "Repeated jump - diagonal parkour chain" "600.5" "80" "600.5" "606" "80.00" "606"
}
scenario_obstructed_parkour_turn_mix() {
fill_box 618 79 618 628 79 624 air
fill_box 618 80 618 628 90 624 air
set_stone 620 79 620
set_stone 622 79 620
set_stone 622 79 621
set_stone 624 79 621
set_stone 624 79 622
set_stone 626 79 622
set_stone 620 80 621
set_stone 620 81 621
set_stone 622 80 622
set_stone 622 81 622
run_accepted_route "Obstructed jump mix - repeated parkour L-turns" "620.5" "80" "620.5" "626" "80.00" "622"
}
scenario_parkour_ascend_descend_chain() {
fill_box 638 79 618 650 80 622 air
fill_box 638 81 618 650 92 622 air
set_stone 640 79 620
set_stone 642 80 620
set_stone 644 79 620
set_stone 646 80 620
set_stone 648 79 620
run_accepted_route "Vertical jump mix - parkour ascend descend chain" "640.5" "80" "620.5" "648" "80.00" "620"
}
scenario_diagonal_ascend_descend_chain() {
fill_box 678 79 618 686 80 626 air
fill_box 678 81 618 686 92 626 air
set_stone 680 79 620
set_stone 681 80 621
set_stone 682 79 622
set_stone 683 80 623
set_stone 684 79 624
run_accepted_route "Diagonal vertical mix - ascend descend chain" "680.5" "80" "620.5" "684" "80.00" "624"
}
start_mcc
mc-rcon "difficulty peaceful" >/dev/null 2>&1 || true
mc-rcon "gamerule doMobSpawning false" >/dev/null 2>&1 || true
mc-rcon "time set day" >/dev/null 2>&1 || true
run_case "Repeated jump - cardinal parkour chain" scenario_repeated_cardinal_parkour
run_case "Repeated jump - diagonal parkour chain" scenario_repeated_diagonal_parkour
run_case "Obstructed jump mix - repeated parkour L-turns" scenario_obstructed_parkour_turn_mix
run_case "Vertical jump mix - parkour ascend descend chain" scenario_parkour_ascend_descend_chain
run_case "Diagonal vertical mix - ascend descend chain" scenario_diagonal_ascend_descend_chain
echo ""
echo "Jump combo summary:"
for label in "${PASSED_CASES[@]}"; do
echo " PASS $label"
done
for label in "${FAILED_CASES[@]}"; do
echo " FAIL $label"
done
if [[ ${#FAILED_CASES[@]} -ne 0 ]]; then
exit 1
fi
echo ""
echo "Pathing jump-combo suite complete."

View file

@ -0,0 +1,454 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$REPO_ROOT/tools/mcc-env.sh"
VERSION="${1:-1.21.11-Vanilla}"
SESSION="mcc-pathing-long-routes"
USERNAME="CursorBot"
SESSION_ROOT="$(_mcc_session_root "$SESSION")"
LOG="$(_mcc_session_log_file "$SESSION")"
cleanup() {
mcc-kill --session "$SESSION" >/dev/null 2>&1 || true
}
trap cleanup EXIT
send_mcc() {
mcc-cmd --session "$SESSION" "$1"
}
log_line_count() {
if [[ -f "$LOG" ]]; then
wc -l < "$LOG"
else
echo 0
fi
}
log_since() {
local from_line="$1"
if [[ ! -f "$LOG" ]]; then
return
fi
tail -n +"$((from_line + 1))" "$LOG"
}
wait_for_log() {
local pattern="$1"
local from_line="${2:-0}"
local timeout="${3:-30}"
for _ in $(seq 1 "$timeout"); do
if log_since_clean "$from_line" | grep -Fq "$pattern"; then
return 0
fi
sleep 1
done
return 1
}
wait_for_navigation() {
local from_line="$1"
local timeout="${2:-45}"
local saw_start=0
for _ in $(seq 1 "$timeout"); do
local recent
recent="$(log_since_clean "$from_line")"
if grep -Fq "[PathMgr] Navigation started" <<<"$recent"; then
saw_start=1
fi
if (( saw_start )) && grep -Fq "[PathMgr] Navigation complete!" <<<"$recent"; then
return 0
fi
if grep -Eq "\[PathMgr\] (Replan failed|Giving up)" <<<"$recent"; then
echo "$recent" >&2
return 1
fi
if grep -Eq "No path found|\[Navigate\] A\* result: Failed" <<<"$recent"; then
echo "$recent" >&2
return 1
fi
sleep 1
done
echo "Timed out waiting for navigation completion" >&2
log_since_clean "$from_line" >&2
return 1
}
log_since_clean() {
log_since "$1" | sed 's/\x1b\[[0-9;]*m//g'
}
count_replans_since() {
local from_line="$1"
log_since_clean "$from_line" | grep -Ec '\[PathMgr\] Replan #|\[PathExec\] Segment .* FAILED' || true
}
assert_no_replans_since() {
local from_line="$1"
local count
count="$(count_replans_since "$from_line")"
if [[ "$count" != "0" ]]; then
echo "Expected 0 replans, saw $count" >&2
log_since_clean "$from_line" >&2
return 1
fi
}
assert_no_partial_since() {
local from_line="$1"
if log_since_clean "$from_line" | grep -Fq "[Navigate] A* result: Partial"; then
echo "Expected full success path, saw partial path" >&2
log_since_clean "$from_line" >&2
return 1
fi
}
debug_state_snapshot() {
local label="$1"
local from_line
from_line="$(log_line_count)"
send_mcc "debug state"
wait_for_log "Location" "$from_line" 10
echo ""
echo "=== Debug state: $label ==="
log_since_clean "$from_line"
}
capture_debug_state_before_route() {
debug_state_snapshot "before route - $1"
}
capture_debug_state_after_route() {
debug_state_snapshot "after route - $1"
}
prepare_independent_route() {
local label="$1"
local start_x="$2"
local start_y="$3"
local start_z="$4"
echo ""
echo "Preparing independent route: $label"
mc-rcon "effect clear $USERNAME" >/dev/null 2>&1 || true
mc-rcon "tp $USERNAME $start_x $start_y $start_z" >/dev/null
wait_for_location_in_block "$start_x" "$start_y" "$start_z" 10
}
capture_debug_location() {
local start_line
start_line="$(log_line_count)"
send_mcc "debug state"
wait_for_log "Location" "$start_line" 10
extract_last_location "$start_line"
}
wait_for_location_in_block() {
local expected_x="$1"
local expected_y="$2"
local expected_z="$3"
local timeout="${4:-10}"
for _ in $(seq 1 "$timeout"); do
local actual_x actual_y actual_z
read -r actual_x actual_y actual_z <<< "$(capture_debug_location)"
if python3 - <<'PY' "$actual_x" "$actual_y" "$actual_z" "$expected_x" "$expected_y" "$expected_z"
import math
import sys
ax, ay, az, ex, ey, ez = map(float, sys.argv[1:])
if math.floor(ax) == int(ex) and math.floor(az) == int(ez) and abs(ay - ey) <= 0.05:
raise SystemExit(0)
raise SystemExit(1)
PY
then
return 0
fi
sleep 1
done
echo "Timed out waiting for player to reach start block ($expected_x, $expected_y, $expected_z)" >&2
return 1
}
extract_last_location() {
local from_line="${1:-0}"
python3 - "$LOG" "$from_line" <<'PY'
import pathlib
import re
import sys
log_path = pathlib.Path(sys.argv[1])
from_line = int(sys.argv[2])
text = log_path.read_text(errors="ignore")
text = "\n".join(text.splitlines()[from_line:])
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
matches = re.findall(r"Location\s+([-,0-9.]+),\s+([-,0-9.]+),\s+([-,0-9.]+)", text)
if not matches:
raise SystemExit("No Location line found in MCC log")
x, y, z = matches[-1]
print(f"{x} {y} {z}")
PY
}
assert_inside_goal_block() {
local actual_x="$1"
local actual_y="$2"
local actual_z="$3"
local expected_x="$4"
local expected_y="$5"
local expected_z="$6"
python3 - <<'PY' "$actual_x" "$actual_y" "$actual_z" "$expected_x" "$expected_y" "$expected_z"
import math
import sys
ax, ay, az, ex, ey, ez = map(float, sys.argv[1:])
if math.floor(ax) != int(ex) or math.floor(az) != int(ez) or abs(ay - ey) > 0.05:
raise SystemExit(
f"Expected location inside goal block ({int(ex)}, {ey:.2f}, {int(ez)}), got ({ax:.2f}, {ay:.2f}, {az:.2f})"
)
PY
}
print_summary() {
local header="$1"
echo ""
echo "----- $header -----"
if [[ -f "$LOG" ]]; then
tail -n 40 "$LOG" | sed 's/\x1b\[[0-9;]*m//g'
else
echo "(no log available yet)"
fi
}
fill_box() {
mc-rcon "fill $1 $2 $3 $4 $5 $6 $7" >/dev/null
}
set_stone() {
mc-rcon "setblock $1 $2 $3 stone" >/dev/null
}
run_accepted_route() {
local label="$1"
local start_x="$2"
local start_y="$3"
local start_z="$4"
local goal_x="$5"
local goal_y="$6"
local goal_z="$7"
local timeout="${8:-45}"
prepare_independent_route "$label" "$start_x" "$start_y" "$start_z"
capture_debug_state_before_route "$label"
local start_line
start_line="$(log_line_count)"
send_mcc "pathfind $goal_x $goal_y $goal_z"
wait_for_navigation "$start_line" "$timeout"
assert_no_partial_since "$start_line"
assert_no_replans_since "$start_line"
capture_debug_state_after_route "$label"
local x y z
read -r x y z <<< "$(capture_debug_location)"
echo " Final location: $x $y $z"
assert_inside_goal_block "$x" "$y" "$z" "$goal_x" "$goal_y" "$goal_z"
print_summary "$label"
}
start_mcc() {
mkdir -p "$SESSION_ROOT"
mcc-kill --session "$SESSION" >/dev/null 2>&1 || true
mcc-build >/dev/null
mcc-debug -v "$VERSION" --session "$SESSION" --username "$USERNAME" --file-input --debug-on --no-build >/dev/null
wait_for_log "Server was successfully joined." 0 40
send_mcc "debug on"
}
run_same_move_routes() {
echo "== Same move routes =="
fill_box 298 79 298 314 79 302 air
fill_box 298 80 298 314 90 302 air
fill_box 300 79 300 312 79 300 stone
run_accepted_route "Same move - straight traverse chain" "300.5" "80" "300.5" "312" "80.00" "300"
fill_box 318 79 318 330 79 330 air
fill_box 318 80 318 330 90 330 air
set_stone 320 79 320
set_stone 321 79 321
set_stone 322 79 322
set_stone 323 79 323
set_stone 324 79 324
set_stone 325 79 325
set_stone 326 79 326
set_stone 327 79 327
run_accepted_route "Same move - diagonal chain" "320.5" "80" "320.5" "327" "80.00" "327"
fill_box 338 79 338 347 85 342 air
fill_box 338 80 338 347 90 342 air
fill_box 340 79 339 340 79 341 stone
fill_box 341 80 339 341 80 341 stone
fill_box 342 81 339 342 81 341 stone
fill_box 343 82 339 343 82 341 stone
fill_box 344 83 339 344 83 341 stone
fill_box 345 84 339 345 84 341 stone
run_accepted_route "Same move - ascend staircase" "340.5" "80" "340.5" "345" "85.00" "340"
fill_box 360 79 358 369 85 362 air
fill_box 360 80 358 369 90 362 air
fill_box 362 84 359 362 84 361 stone
fill_box 363 83 359 363 83 361 stone
fill_box 364 82 359 364 82 361 stone
fill_box 365 81 359 365 81 361 stone
fill_box 366 80 359 366 80 361 stone
fill_box 367 79 359 367 79 361 stone
run_accepted_route "Same move - descend staircase" "362.5" "85" "360.5" "367" "80.00" "360"
fill_box 378 79 378 390 79 382 air
fill_box 378 80 378 390 90 382 air
set_stone 380 79 380
set_stone 382 79 380
set_stone 384 79 380
set_stone 386 79 380
set_stone 388 79 380
run_accepted_route "Same move - aligned parkour chain" "380.5" "80" "380.5" "388" "80.00" "380"
}
run_mixed_move_routes() {
echo "== Mixed move routes =="
fill_box 398 79 398 410 79 406 air
fill_box 398 80 398 410 90 406 air
set_stone 400 79 400
set_stone 401 79 400
set_stone 402 79 400
set_stone 402 79 401
set_stone 402 79 402
set_stone 404 79 402
set_stone 405 79 402
set_stone 406 79 402
set_stone 406 79 403
set_stone 406 79 404
set_stone 407 79 404
set_stone 408 79 404
run_accepted_route "Mixed - traverse turn parkour turn traverse" "400.5" "80" "400.5" "408" "80.00" "404"
fill_box 418 79 418 430 82 424 air
fill_box 418 80 418 430 92 424 air
set_stone 420 79 420
set_stone 421 79 421
set_stone 422 79 422
set_stone 423 80 422
set_stone 424 81 422
set_stone 425 81 422
set_stone 426 81 422
set_stone 427 80 422
set_stone 428 79 422
run_accepted_route "Mixed - diagonal ascend traverse descend" "420.5" "80" "420.5" "428" "80.00" "422"
fill_box 438 79 438 450 82 442 air
fill_box 438 80 438 450 92 442 air
set_stone 440 79 440
set_stone 441 79 440
set_stone 442 80 440
set_stone 443 81 440
set_stone 444 81 440
set_stone 446 81 440
set_stone 447 80 440
set_stone 448 79 440
run_accepted_route "Mixed - traverse ascend parkour descend" "440.5" "80" "440.5" "448" "80.00" "440"
}
run_turn_density_routes() {
echo "== Turn density routes =="
fill_box 458 79 458 468 79 468 air
fill_box 458 80 458 468 90 468 air
set_stone 460 79 460
set_stone 461 79 460
set_stone 461 79 461
set_stone 462 79 462
set_stone 463 79 462
set_stone 463 79 463
set_stone 464 79 464
set_stone 465 79 464
set_stone 465 79 465
set_stone 466 79 466
run_accepted_route "Turn density - alternating traverse diagonal chain" "460.5" "80" "460.5" "466" "80.00" "466"
}
run_speed_carry_routes() {
echo "== Speed carry routes =="
fill_box 478 79 478 490 83 482 air
fill_box 478 80 478 490 94 482 air
set_stone 480 79 480
set_stone 481 79 480
set_stone 482 80 480
set_stone 483 80 480
set_stone 484 81 480
set_stone 485 81 480
set_stone 486 82 480
set_stone 487 82 480
set_stone 488 83 480
run_accepted_route "Speed carry - repeated traverse ascend" "480.5" "80" "480.5" "488" "84.00" "480"
fill_box 498 79 498 510 82 502 air
fill_box 498 80 498 510 94 502 air
set_stone 500 82 500
set_stone 501 82 500
set_stone 502 81 500
set_stone 503 81 500
set_stone 504 80 500
set_stone 505 80 500
set_stone 506 79 500
set_stone 507 79 500
run_accepted_route "Speed carry - repeated traverse descend" "500.5" "83" "500.5" "507" "80.00" "500"
fill_box 518 79 518 532 79 522 air
fill_box 518 80 518 532 90 522 air
set_stone 520 79 520
set_stone 521 79 520
set_stone 523 79 520
set_stone 524 79 520
set_stone 526 79 520
set_stone 527 79 520
set_stone 529 79 520
run_accepted_route "Speed carry - repeated traverse parkour" "520.5" "80" "520.5" "529" "80.00" "520"
}
start_mcc
mc-rcon "difficulty peaceful" >/dev/null 2>&1 || true
mc-rcon "gamerule doMobSpawning false" >/dev/null 2>&1 || true
mc-rcon "time set day" >/dev/null 2>&1 || true
run_same_move_routes
run_mixed_move_routes
run_turn_density_routes
run_speed_carry_routes
mcc-kill --session "$SESSION" >/dev/null 2>&1 || true
echo ""
echo "Pathing long-route suite complete."

View file

@ -7,17 +7,19 @@ source "$REPO_ROOT/tools/mcc-env.sh"
VERSION="${1:-1.21.11-Vanilla}"
SESSION="mcc-pathing-template"
TEST_ROOT="${TMPDIR:-/tmp}/mcc-pathing-template"
CFG="$TEST_ROOT/MinecraftClient.pathing-template.ini"
LOG="$TEST_ROOT/mcc-pathing-template.log"
INPUT_FILE="$REPO_ROOT/mcc_input.txt"
PREPARE_CFG_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh"
ENSURE_SERVER_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh"
USERNAME="CursorBot"
mkdir -p "$TEST_ROOT"
SESSION_ROOT="$(_mcc_session_root "$SESSION")"
LOG="$(_mcc_session_log_file "$SESSION")"
cleanup() {
mcc-kill --session "$SESSION" >/dev/null 2>&1 || true
}
trap cleanup EXIT
send_mcc() {
echo "$1" >> "$INPUT_FILE"
mcc-cmd --session "$SESSION" "$1"
}
log_line_count() {
@ -40,10 +42,10 @@ log_since() {
wait_for_log() {
local pattern="$1"
local from_line="${2:-0}"
local timeout="${3:-20}"
local timeout="${3:-30}"
for _ in $(seq 1 "$timeout"); do
if log_since "$from_line" | grep -Fq "$pattern"; then
if log_since_clean "$from_line" | grep -Fq "$pattern"; then
return 0
fi
sleep 1
@ -55,25 +57,35 @@ wait_for_log() {
wait_for_navigation() {
local from_line="$1"
local timeout="${2:-25}"
local saw_start=0
for _ in $(seq 1 "$timeout"); do
local recent
recent="$(log_since "$from_line")"
recent="$(log_since_clean "$from_line")"
if grep -Eq "\\[PathMgr\\] (Replan failed|Giving up)|\\[PathMgr\\] Segment failed, replanning|\\[PathExec\\] Segment .* FAILED" <<<"$recent"; then
if grep -Fq "[PathMgr] Navigation started" <<<"$recent"; then
saw_start=1
fi
if (( saw_start )) && grep -Fq "[PathMgr] Navigation complete!" <<<"$recent"; then
return 0
fi
if grep -Eq "\[PathMgr\] (Replan failed|Giving up)" <<<"$recent"; then
echo "$recent" >&2
return 1
fi
if grep -Fq "[PathMgr] Navigation complete!" <<<"$recent"; then
return 0
if grep -Eq "No path found|\[Navigate\] A\* result: Failed" <<<"$recent"; then
echo "$recent" >&2
return 1
fi
sleep 1
done
echo "Timed out waiting for navigation completion" >&2
log_since "$from_line" >&2
log_since_clean "$from_line" >&2
return 1
}
@ -83,9 +95,9 @@ wait_for_failure_signal() {
for _ in $(seq 1 "$timeout"); do
local recent
recent="$(log_since "$from_line")"
recent="$(log_since_clean "$from_line")"
if grep -Eq "\\[PathMgr\\] (Replan failed|Giving up)|No path found|\\[Navigate\\] A\\* result: Failed" <<<"$recent"; then
if grep -Eq "No path found|\[Navigate\] A\* result: Failed" <<<"$recent"; then
return 0
fi
@ -95,6 +107,82 @@ wait_for_failure_signal() {
return 1
}
log_since_clean() {
log_since "$1" | sed 's/\x1b\[[0-9;]*m//g'
}
count_replans_since() {
local from_line="$1"
log_since_clean "$from_line" | grep -Ec '\[PathMgr\] Replan #|\[PathExec\] Segment .* FAILED' || true
}
assert_no_replans_since() {
local from_line="$1"
local count
count="$(count_replans_since "$from_line")"
if [[ "$count" != "0" ]]; then
echo "Expected 0 replans, saw $count" >&2
log_since_clean "$from_line" >&2
return 1
fi
}
assert_no_partial_since() {
local from_line="$1"
if log_since_clean "$from_line" | grep -Fq "[Navigate] A* result: Partial"; then
echo "Expected full success path, saw partial path" >&2
log_since_clean "$from_line" >&2
return 1
fi
}
debug_state_snapshot() {
local label="$1"
local from_line
from_line="$(log_line_count)"
send_mcc "debug state"
wait_for_log "Location" "$from_line" 10
echo ""
echo "=== Debug state: $label ==="
log_since_clean "$from_line"
}
capture_debug_state_before_route() {
debug_state_snapshot "before route - $1"
}
capture_debug_state_after_route() {
debug_state_snapshot "after route - $1"
}
prepare_independent_route() {
local label="$1"
local start_x="$2"
local start_y="$3"
local start_z="$4"
echo ""
echo "Preparing independent route: $label"
mc-rcon "effect clear $USERNAME" >/dev/null 2>&1 || true
mc-rcon "tp $USERNAME $start_x $start_y $start_z" >/dev/null
wait_for_location_in_block "$start_x" "$start_y" "$start_z" 10
}
assert_direct_rejection_since() {
local from_line="$1"
if ! log_since_clean "$from_line" | grep -Eq "No path found|\[Navigate\] A\* result: Failed"; then
echo "Expected direct rejection before navigation execution" >&2
log_since_clean "$from_line" >&2
return 1
fi
if log_since_clean "$from_line" | grep -Fq "[PathMgr] Navigation started"; then
echo "Expected rejection before navigation started" >&2
log_since_clean "$from_line" >&2
return 1
fi
assert_no_replans_since "$from_line"
}
extract_last_location() {
local from_line="${1:-0}"
@ -108,11 +196,11 @@ from_line = int(sys.argv[2])
text = log_path.read_text(errors="ignore")
text = "\n".join(text.splitlines()[from_line:])
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
matches = re.findall(r"Location\s+([-\d.]+),\s+([-\d.]+),\s+([-\d.]+)", text)
matches = re.findall(r"Location\s+([-,0-9.]+),\s+([-,0-9.]+),\s+([-,0-9.]+)", text)
if not matches:
matches = re.findall(r"Segment \d+ complete .* at \(([-\d.]+),([-\d.]+),([-\d.]+)\)", text)
matches = re.findall(r"Segment \d+ complete .* at \(([-,0-9.]+),([-,0-9.]+),([-,0-9.]+)\)", text)
if not matches:
matches = re.findall(r"pos=\(([-\d.]+),\s*([-\d.]+),\s*([-\d.]+)\)", text)
matches = re.findall(r"pos=\(([-,0-9.]+),\s*([-,0-9.]+),\s*([-,0-9.]+)\)", text)
if not matches:
raise SystemExit("No location line found in MCC log")
x, y, z = matches[-1]
@ -120,23 +208,22 @@ print(f"{x} {y} {z}")
PY
}
assert_close() {
assert_inside_goal_block() {
local actual_x="$1"
local actual_y="$2"
local actual_z="$3"
local target_x="$4"
local target_y="$5"
local target_z="$6"
local tolerance="${7:-0.2}"
python3 - <<'PY' "$actual_x" "$actual_y" "$actual_z" "$target_x" "$target_y" "$target_z" "$tolerance"
python3 - <<'PY' "$actual_x" "$actual_y" "$actual_z" "$target_x" "$target_y" "$target_z"
import math
import sys
ax, ay, az, tx, ty, tz, tol = map(float, sys.argv[1:])
if abs(ax - tx) > tol or abs(ay - ty) > tol or abs(az - tz) > tol:
ax, ay, az, tx, ty, tz = map(float, sys.argv[1:])
if math.floor(ax) != int(tx) or math.floor(az) != int(tz) or abs(ay - ty) > 0.05:
raise SystemExit(
f"Expected ({tx:.2f}, {ty:.2f}, {tz:.2f}) within {tol:.2f}, got ({ax:.2f}, {ay:.2f}, {az:.2f})"
f"Expected location inside goal block ({int(tx)}, {ty:.2f}, {int(tz)}), got ({ax:.2f}, {ay:.2f}, {az:.2f})"
)
PY
}
@ -153,37 +240,69 @@ print_summary() {
fi
}
capture_debug_location() {
local start_line
start_line="$(log_line_count)"
send_mcc "debug state"
wait_for_log "Location" "$start_line" 10
extract_last_location "$start_line"
}
wait_for_location_in_block() {
local expected_x="$1"
local expected_y="$2"
local expected_z="$3"
local timeout="${4:-10}"
for _ in $(seq 1 "$timeout"); do
local actual_x actual_y actual_z
read -r actual_x actual_y actual_z <<< "$(capture_debug_location)"
if python3 - <<'PY' "$actual_x" "$actual_y" "$actual_z" "$expected_x" "$expected_y" "$expected_z"
import math
import sys
ax, ay, az, ex, ey, ez = map(float, sys.argv[1:])
if math.floor(ax) == int(ex) and math.floor(az) == int(ez) and abs(ay - ey) <= 0.05:
raise SystemExit(0)
raise SystemExit(1)
PY
then
return 0
fi
sleep 1
done
echo "Timed out waiting for player to reach start block ($expected_x, $expected_y, $expected_z)" >&2
return 1
}
start_mcc() {
bash "$PREPARE_CFG_SCRIPT" "$CFG" "$VERSION" CursorBot >/dev/null
: > "$INPUT_FILE"
: > "$LOG"
tmux kill-session -t "$SESSION" 2>/dev/null || true
tmux new-session -d -s "$SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- '$CFG' CursorBot - localhost:25565 > '$LOG' 2>&1; echo '=== MCC EXITED ==='; sleep 600"
wait_for_log "Server was successfully joined." 0 20
mkdir -p "$SESSION_ROOT"
mcc-kill --session "$SESSION" >/dev/null 2>&1 || true
mcc-build >/dev/null
mcc-debug -v "$VERSION" --session "$SESSION" --username "$USERNAME" --file-input --debug-on --no-build >/dev/null
wait_for_log "Server was successfully joined." 0 40
send_mcc "debug on"
sleep 1
}
run_flat_final_stop() {
echo "== Flat final stop =="
mc-rcon "fill 95 79 95 115 79 105 stone" >/dev/null
mc-rcon "fill 95 80 95 115 85 105 air" >/dev/null
mc-rcon "tp CursorBot 100.5 80 100.5" >/dev/null
sleep 2
prepare_independent_route "Flat final stop" "100.5" "80" "100.5"
capture_debug_state_before_route "Flat final stop"
local start_line
start_line="$(log_line_count)"
send_mcc "pathfind 103 80 100"
wait_for_navigation "$start_line" 30
assert_no_partial_since "$start_line"
assert_no_replans_since "$start_line"
capture_debug_state_after_route "Flat final stop"
local x y z
read -r x y z <<< "$(extract_last_location "$start_line")"
read -r x y z <<< "$(capture_debug_location)"
echo " Final location: $x $y $z"
assert_close "$x" "$y" "$z" "103.50" "80.00" "100.50"
assert_inside_goal_block "$x" "$y" "$z" "103" "80.00" "100"
print_summary "Flat final stop"
}
@ -196,18 +315,20 @@ run_parkour_into_turn() {
mc-rcon "setblock 122 79 111 stone" >/dev/null
mc-rcon "setblock 120 80 111 stone" >/dev/null
mc-rcon "setblock 120 81 111 stone" >/dev/null
mc-rcon "tp CursorBot 120.5 80 110.5" >/dev/null
sleep 2
prepare_independent_route "Parkour into L-turn" "120.5" "80" "110.5"
capture_debug_state_before_route "Parkour into L-turn"
local start_line
start_line="$(log_line_count)"
send_mcc "pathfind 122 80 111"
wait_for_navigation "$start_line" 30
assert_no_partial_since "$start_line"
assert_no_replans_since "$start_line"
capture_debug_state_after_route "Parkour into L-turn"
local x y z
read -r x y z <<< "$(extract_last_location "$start_line")"
read -r x y z <<< "$(capture_debug_location)"
echo " Final location: $x $y $z"
assert_close "$x" "$y" "$z" "122.50" "80.00" "111.50"
assert_inside_goal_block "$x" "$y" "$z" "122" "80.00" "111"
print_summary "Parkour into L-turn"
}
@ -221,14 +342,14 @@ run_side_wall_jump() {
mc-rcon "setblock 132 81 126 stone" >/dev/null
mc-rcon "setblock 133 80 126 stone" >/dev/null
mc-rcon "setblock 133 81 126 stone" >/dev/null
mc-rcon "tp CursorBot 131.5 80 127.5" >/dev/null
sleep 2
prepare_independent_route "Rejected 2x1 side-wall jump" "131.5" "80" "127.5"
capture_debug_state_before_route "Rejected 2x1 side-wall jump"
local start_line
start_line="$(log_line_count)"
send_mcc "pathfind 133 80 127"
if wait_for_failure_signal "$start_line" 20; then
assert_direct_rejection_since "$start_line"
echo " Pathfinding rejected as expected."
else
echo " Expected rejection but navigation continued." >&2
@ -244,33 +365,18 @@ run_reject_3x1_gap() {
mc-rcon "fill 140 79 135 148 79 140 stone" >/dev/null
mc-rcon "fill 140 80 135 148 85 140 air" >/dev/null
mc-rcon "setblock 143 80 138 stone" >/dev/null
mc-rcon "tp CursorBot 141.5 80 138.5" >/dev/null
sleep 2
prepare_independent_route "Rejected 3x1 no-run-up gap" "141.5" "80" "138.5"
capture_debug_state_before_route "Rejected 3x1 gap"
local start_line
start_line="$(log_line_count)"
send_mcc "pathfind 144 81 138"
if wait_for_log "Replan failed" "$start_line" 20; then
if wait_for_failure_signal "$start_line" 20; then
assert_direct_rejection_since "$start_line"
echo " Pathfinding rejected as expected."
elif wait_for_navigation "$start_line" 30; then
local x y z
read -r x y z <<< "$(extract_last_location "$start_line")"
if python3 - <<'PY' "$x" "$y" "$z"
import sys
x, y, z = map(float, sys.argv[1:])
tx, ty, tz = 144.5, 81.0, 138.5
tol = 0.2
sys.exit(0 if abs(x - tx) > tol or abs(y - ty) > tol or abs(z - tz) > tol else 1)
PY
then
echo " Pathfinder only reached a partial fallback, rejection accepted."
else
echo " Expected rejection but goal was reached." >&2
return 1
fi
else
echo " Expected rejection but navigation continued." >&2
log_since "$start_line" >&2
return 1
fi
@ -284,18 +390,20 @@ run_corner_ascend_around_wall() {
mc-rcon "setblock 191 80 171 stone" >/dev/null
mc-rcon "setblock 191 80 170 stone" >/dev/null
mc-rcon "setblock 191 81 170 stone" >/dev/null
mc-rcon "tp CursorBot 190.5 80 170.5" >/dev/null
sleep 2
prepare_independent_route "Corner ascend around wall" "190.5" "80" "170.5"
capture_debug_state_before_route "Corner ascend around wall"
local start_line
start_line="$(log_line_count)"
send_mcc "pathfind 191 81 171"
wait_for_navigation "$start_line" 25
assert_no_partial_since "$start_line"
assert_no_replans_since "$start_line"
capture_debug_state_after_route "Corner ascend around wall"
local x y z
read -r x y z <<< "$(extract_last_location "$start_line")"
read -r x y z <<< "$(capture_debug_location)"
echo " Final location: $x $y $z"
assert_close "$x" "$y" "$z" "191.50" "81.00" "171.50" "0.25"
assert_inside_goal_block "$x" "$y" "$z" "191" "81.00" "171"
print_summary "Corner ascend around wall"
}
@ -309,18 +417,20 @@ run_wall_adjacent_descend_smoke() {
mc-rcon "setblock 202 80 199 stone" >/dev/null
mc-rcon "setblock 201 81 199 stone" >/dev/null
mc-rcon "setblock 202 81 199 stone" >/dev/null
mc-rcon "tp CursorBot 200.5 81 200.5" >/dev/null
sleep 2
prepare_independent_route "Wall-adjacent descend" "200.5" "81" "200.5"
capture_debug_state_before_route "Wall-adjacent descend"
local start_line
start_line="$(log_line_count)"
send_mcc "pathfind 201 80 200"
wait_for_navigation "$start_line" 25
assert_no_partial_since "$start_line"
assert_no_replans_since "$start_line"
capture_debug_state_after_route "Wall-adjacent descend"
local x y z
read -r x y z <<< "$(extract_last_location "$start_line")"
read -r x y z <<< "$(capture_debug_location)"
echo " Final location: $x $y $z"
assert_close "$x" "$y" "$z" "201.50" "80.00" "200.50" "0.25"
assert_inside_goal_block "$x" "$y" "$z" "201" "80.00" "200"
print_summary "Wall-adjacent descend"
}
@ -331,30 +441,24 @@ run_ascend_chain_smoke() {
mc-rcon "setblock 175 80 162 stone" >/dev/null
mc-rcon "setblock 176 81 162 stone" >/dev/null
mc-rcon "setblock 177 82 162 stone" >/dev/null
mc-rcon "fill 178 78 160 182 78 164 stone" >/dev/null
mc-rcon "fill 178 83 160 182 83 164 air" >/dev/null
mc-rcon "setblock 181 80 162 minecraft:ladder[facing=east]" >/dev/null
mc-rcon "setblock 181 81 162 minecraft:ladder[facing=east]" >/dev/null
mc-rcon "setblock 181 82 162 minecraft:ladder[facing=east]" >/dev/null
mc-rcon "setblock 181 83 162 minecraft:ladder[facing=east]" >/dev/null
mc-rcon "tp CursorBot 171.5 80 160.5" >/dev/null
sleep 2
prepare_independent_route "Ascend chain smoke" "171.5" "80" "160.5"
capture_debug_state_before_route "Ascend chain smoke"
local start_line
start_line="$(log_line_count)"
send_mcc "pathfind 182 83 162"
send_mcc "pathfind 177 83 162"
wait_for_navigation "$start_line" 35
assert_no_partial_since "$start_line"
assert_no_replans_since "$start_line"
capture_debug_state_after_route "Ascend chain smoke"
local x y z
read -r x y z <<< "$(capture_debug_location)"
echo " Final location: $x $y $z"
assert_inside_goal_block "$x" "$y" "$z" "177" "83.00" "162"
echo " Ascend chain completed."
print_summary "Ascend chain smoke"
}
mcc-preflight "$VERSION" >/dev/null
mc-reset-test-env "$VERSION" >/dev/null
bash "$ENSURE_SERVER_SCRIPT" "$VERSION" >/dev/null
mc-start "$VERSION" >/dev/null
mc-wait-ready "$VERSION" 60 >/dev/null
mcc-kill >/dev/null 2>&1 || true
start_mcc
mc-rcon "difficulty peaceful" >/dev/null 2>&1 || true

View file

@ -7,17 +7,19 @@ source "$REPO_ROOT/tools/mcc-env.sh"
VERSION="${1:-1.21.11-Vanilla}"
SESSION="mcc-brake-test"
TEST_ROOT="${TMPDIR:-/tmp}/mcc-debug"
CFG="$TEST_ROOT/MinecraftClient.transition-braking.ini"
LOG="$TEST_ROOT/mcc-transition-braking.log"
INPUT_FILE="$REPO_ROOT/mcc_input.txt"
PREPARE_CFG_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh"
ENSURE_SERVER_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh"
USERNAME="CursorBot"
mkdir -p "$TEST_ROOT"
SESSION_ROOT="$(_mcc_session_root "$SESSION")"
LOG="$(_mcc_session_log_file "$SESSION")"
cleanup() {
mcc-kill --session "$SESSION" >/dev/null 2>&1 || true
}
trap cleanup EXIT
send_mcc() {
echo "$1" >> "$INPUT_FILE"
mcc-cmd --session "$SESSION" "$1"
}
log_line_count() {
@ -40,10 +42,10 @@ log_since() {
wait_for_log() {
local pattern="$1"
local from_line="${2:-0}"
local timeout="${3:-20}"
local timeout="${3:-30}"
for _ in $(seq 1 "$timeout"); do
if log_since "$from_line" | grep -Fq "$pattern"; then
if log_since_clean "$from_line" | grep -Fq "$pattern"; then
return 0
fi
sleep 1
@ -55,16 +57,26 @@ wait_for_log() {
wait_for_navigation() {
local from_line="$1"
local timeout="${2:-20}"
local saw_start=0
for _ in $(seq 1 "$timeout"); do
local recent
recent="$(log_since "$from_line")"
recent="$(log_since_clean "$from_line")"
if grep -Fq "[PathMgr] Navigation complete!" <<<"$recent"; then
if grep -Fq "[PathMgr] Navigation started" <<<"$recent"; then
saw_start=1
fi
if (( saw_start )) && grep -Fq "[PathMgr] Navigation complete!" <<<"$recent"; then
return 0
fi
if grep -Eq "\\[PathMgr\\] (Replan failed|Giving up)|\\[PathExec\\] Segment .* FAILED" <<<"$recent"; then
if grep -Eq "\[PathMgr\] (Replan failed|Giving up)" <<<"$recent"; then
echo "$recent" >&2
return 1
fi
if grep -Eq "No path found|\[Navigate\] A\* result: Failed" <<<"$recent"; then
echo "$recent" >&2
return 1
fi
@ -73,10 +85,71 @@ wait_for_navigation() {
done
echo "Timed out waiting for navigation completion" >&2
log_since "$from_line" >&2
log_since_clean "$from_line" >&2
return 1
}
log_since_clean() {
log_since "$1" | sed 's/\x1b\[[0-9;]*m//g'
}
count_replans_since() {
local from_line="$1"
log_since_clean "$from_line" | grep -Ec '\[PathMgr\] Replan #|\[PathExec\] Segment .* FAILED' || true
}
assert_no_replans_since() {
local from_line="$1"
local count
count="$(count_replans_since "$from_line")"
if [[ "$count" != "0" ]]; then
echo "Expected 0 replans, saw $count" >&2
log_since_clean "$from_line" >&2
return 1
fi
}
assert_no_partial_since() {
local from_line="$1"
if log_since_clean "$from_line" | grep -Fq "[Navigate] A* result: Partial"; then
echo "Expected full success path, saw partial path" >&2
log_since_clean "$from_line" >&2
return 1
fi
}
debug_state_snapshot() {
local label="$1"
local from_line
from_line="$(log_line_count)"
send_mcc "debug state"
wait_for_log "Location" "$from_line" 10
echo ""
echo "=== Debug state: $label ==="
log_since_clean "$from_line"
}
capture_debug_state_before_route() {
debug_state_snapshot "before route - $1"
}
capture_debug_state_after_route() {
debug_state_snapshot "after route - $1"
}
prepare_independent_route() {
local label="$1"
local start_x="$2"
local start_y="$3"
local start_z="$4"
echo ""
echo "Preparing independent route: $label"
mc-rcon "effect clear $USERNAME" >/dev/null 2>&1 || true
mc-rcon "tp $USERNAME $start_x $start_y $start_z" >/dev/null
wait_for_location_in_block "$start_x" "$start_y" "$start_z" 10
}
extract_last_location() {
local from_line="${1:-0}"
@ -90,7 +163,7 @@ from_line = int(sys.argv[2])
text = log_path.read_text(errors="ignore")
text = "\n".join(text.splitlines()[from_line:])
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
matches = re.findall(r"Location\s+([-\d.]+),\s+([-\d.]+),\s+([-\d.]+)", text)
matches = re.findall(r"Location\s+([-,0-9.]+),\s+([-,0-9.]+),\s+([-,0-9.]+)", text)
if not matches:
raise SystemExit("No Location line found in MCC log")
x, y, z = matches[-1]
@ -98,23 +171,22 @@ print(f"{x} {y} {z}")
PY
}
assert_close() {
assert_inside_goal_block() {
local actual_x="$1"
local actual_y="$2"
local actual_z="$3"
local expected_x="$4"
local expected_y="$5"
local expected_z="$6"
local tolerance="${7:-0.05}"
python3 - <<'PY' "$actual_x" "$actual_y" "$actual_z" "$expected_x" "$expected_y" "$expected_z" "$tolerance"
python3 - <<'PY' "$actual_x" "$actual_y" "$actual_z" "$expected_x" "$expected_y" "$expected_z"
import math
import sys
ax, ay, az, ex, ey, ez, tol = map(float, sys.argv[1:])
if abs(ax - ex) > tol or abs(ay - ey) > tol or abs(az - ez) > tol:
ax, ay, az, ex, ey, ez = map(float, sys.argv[1:])
if math.floor(ax) != int(ex) or math.floor(az) != int(ez) or abs(ay - ey) > 0.05:
raise SystemExit(
f"Expected ({ex:.2f}, {ey:.2f}, {ez:.2f}) within {tol:.2f}, got ({ax:.2f}, {ay:.2f}, {az:.2f})"
f"Expected location inside goal block ({int(ex)}, {ey:.2f}, {int(ez)}), got ({ax:.2f}, {ay:.2f}, {az:.2f})"
)
PY
}
@ -123,71 +195,90 @@ capture_debug_location() {
local start_line
start_line="$(log_line_count)"
send_mcc "debug state"
wait_for_log "Location" "$start_line" 5
wait_for_log "Location" "$start_line" 10
extract_last_location "$start_line"
}
wait_for_location_in_block() {
local expected_x="$1"
local expected_y="$2"
local expected_z="$3"
local timeout="${4:-10}"
for _ in $(seq 1 "$timeout"); do
local actual_x actual_y actual_z
read -r actual_x actual_y actual_z <<< "$(capture_debug_location)"
if python3 - <<'PY' "$actual_x" "$actual_y" "$actual_z" "$expected_x" "$expected_y" "$expected_z"
import math
import sys
ax, ay, az, ex, ey, ez = map(float, sys.argv[1:])
if math.floor(ax) == int(ex) and math.floor(az) == int(ez) and abs(ay - ey) <= 0.05:
raise SystemExit(0)
raise SystemExit(1)
PY
then
return 0
fi
sleep 1
done
echo "Timed out waiting for player to reach start block ($expected_x, $expected_y, $expected_z)" >&2
return 1
}
start_mcc() {
bash "$PREPARE_CFG_SCRIPT" "$CFG" "$VERSION" CursorBot >/dev/null
: > "$INPUT_FILE"
: > "$LOG"
tmux kill-session -t "$SESSION" 2>/dev/null || true
tmux new-session -d -s "$SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- '$CFG' CursorBot - localhost:25565 > '$LOG' 2>&1; echo '=== MCC EXITED ==='; sleep 600"
wait_for_log "Server was successfully joined." 0 20
mcc-kill --session "$SESSION" >/dev/null 2>&1 || true
mkdir -p "$SESSION_ROOT"
mcc-build >/dev/null
mcc-debug -v "$VERSION" --session "$SESSION" --username "$USERNAME" --file-input --no-build --debug-on >/dev/null
wait_for_log "Server was successfully joined." 0 30
send_mcc "debug on"
sleep 1
}
run_flat_final_stop() {
echo "== Flat final stop =="
mc-rcon "fill 95 79 95 115 79 105 stone" >/dev/null
mc-rcon "fill 95 80 95 115 85 105 air" >/dev/null
mc-rcon "tp CursorBot 100.5 80 100.5" >/dev/null
sleep 2
prepare_independent_route "Flat final stop" "100.5" "80" "100.5"
capture_debug_state_before_route "Flat final stop"
local start_line
start_line="$(log_line_count)"
send_mcc "goto 103 80 100"
wait_for_navigation "$start_line" 20
sleep 1
assert_no_partial_since "$start_line"
assert_no_replans_since "$start_line"
capture_debug_state_after_route "Flat final stop"
local x y z
read -r x y z <<< "$(capture_debug_location)"
echo "Final location: $x $y $z"
assert_close "$x" "$y" "$z" "103.50" "80.00" "100.50"
assert_inside_goal_block "$x" "$y" "$z" "103" "80.00" "100"
}
run_parkour_into_turn() {
echo "== Parkour into turn =="
mc-rcon "fill 118 79 108 126 79 112 air" >/dev/null
mc-rcon "fill 118 80 108 126 85 112 air" >/dev/null
mc-rcon "setblock 120 79 110 stone" >/dev/null
mc-rcon "setblock 123 79 110 stone" >/dev/null
mc-rcon "setblock 123 79 111 stone" >/dev/null
mc-rcon "tp CursorBot 120.5 80 110.5" >/dev/null
sleep 2
prepare_independent_route "Parkour into turn" "120.5" "80" "110.5"
capture_debug_state_before_route "Parkour into turn"
local start_line
start_line="$(log_line_count)"
send_mcc "goto 123 80 111"
wait_for_navigation "$start_line" 20
sleep 1
assert_no_partial_since "$start_line"
assert_no_replans_since "$start_line"
capture_debug_state_after_route "Parkour into turn"
local x y z
read -r x y z <<< "$(capture_debug_location)"
echo "Final location: $x $y $z"
assert_close "$x" "$y" "$z" "123.50" "80.00" "111.50"
assert_inside_goal_block "$x" "$y" "$z" "123" "80.00" "111"
}
mcc-preflight "$VERSION" >/dev/null
mc-reset-test-env "$VERSION" >/dev/null
bash "$ENSURE_SERVER_SCRIPT" "$VERSION" >/dev/null
mc-start "$VERSION" >/dev/null
mc-wait-ready "$VERSION" 60 >/dev/null
mcc-kill >/dev/null 2>&1 || true
start_mcc
mc-rcon "difficulty peaceful" >/dev/null 2>&1 || true