From 8bd6c426a8d05d2c20b0352bfc0a3d8ce3c16b2e Mon Sep 17 00:00:00 2001 From: NINI Date: Sat, 10 Oct 2026 12:05:59 +0900 Subject: [PATCH] Distinguish obstructed native picks from grabbed detours --- tools/android/exercise-final-progression.ps1 | 13 ++++ tools/android/final-common.ps1 | 18 ++++++ tools/android/test-final-live-drag.ps1 | 65 +++++++++++++++++++- 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/tools/android/exercise-final-progression.ps1 b/tools/android/exercise-final-progression.ps1 index 10bb86d..a8dddb6 100644 --- a/tools/android/exercise-final-progression.ps1 +++ b/tools/android/exercise-final-progression.ps1 @@ -62,7 +62,13 @@ function LiftMergeRoute([int]$Stage,[string]$Name,$Board,[int]$SourceCount=0,[in Check "$Name-unsafe-endpoint-board-unchanged" ($null-ne$actual-and(Same $board $actual)) 'No native touch: the complete revealed board, counts and manual pose remain exact' return [pscustomobject]@{State=$unchanged;Before=$board;SourceId=$id} } + $confirmed=Save "$Name-first-hit-before" + Check "$Name-current-source-projection" (Same $board (Board $confirmed $Stage)) 'The intended ID uses the exact currently confirmed board and manual projection' $after=NativeMoveAndHold $Name $board $id $end + if(IsNativeOrbitMiss $confirmed $after $Stage){ + $commands.Add(@{command='Actual first-hit miss: background orbit only, never a grabbed detour or merge';intendedSourceId=$id;utc=[DateTime]::UtcNow.ToString('o')}) + return [pscustomobject]@{State=$after;Before=$board;BeforeState=$confirmed;SourceId=$id;Outcome='MissedGrabOrbit'} + } $actual=Board $after $Stage;if(!$actual){$actual=$after.latestFreePlay} Check "$Name-detour-manual-camera-preserved" (Same $board.camera $actual.camera) 'Every detour outcome retains the exact confirmed manual pose' if($actual.groups.Count-lt$board.groups.Count){AssertNativeMerge $Name $board $actual $id $TargetCount;return [pscustomobject]@{State=$after;Before=$board;SourceId=$id}} @@ -97,6 +103,13 @@ function MergeReal([int]$Stage,[string]$Name,[int]$SourceCount=0,[int]$TargetCou $routed=LiftMergeRoute $Stage "$Name-upward-route" $board $SourceCount $TargetCount if(!$routed){break} $routedBoard=Board $routed.State $Stage + if($routed.Outcome-eq'MissedGrabOrbit'){ + Check "$Name-actual-first-hit-miss-only" (IsNativeOrbitMiss $routed.BeforeState $routed.State $Stage) 'Actual board and all independent records unchanged; only the expected clamped UP120 manual orbit occurred' + $unchanged=$routed.State|ConvertTo-Json -Depth 20|ConvertFrom-Json + (Board $unchanged $Stage).camera=(Board $before $Stage).camera + Check "$Name-miss-global-records-unmodified" (Same $before $unchanged) 'Reveal and missed pointer input preserve the whole original save except this stage manual view' + $board=$routedBoard;continue + } if(!$routedBoard-or$routedBoard.groups.Count-lt$routed.Before.groups.Count){ $actual=if($routedBoard){$routedBoard}else{$routed.State.latestFreePlay} if(!$routedBoard){Check "$Name-route-completed-stage" ($routed.State.completedStageIds-contains$Stage-and$actual.sourceStageId-eq$Stage) 'A missing puzzle requires actual completion and its own free-play board'} diff --git a/tools/android/final-common.ps1 b/tools/android/final-common.ps1 index 531d222..f8d4ce6 100644 --- a/tools/android/final-common.ps1 +++ b/tools/android/final-common.ps1 @@ -22,6 +22,24 @@ function Step([string]$Name,[string[]]$Arguments=@(),[int]$Delay=500) { } function Save([string]$Name) { Adb @('pull',$remote,(Join-Path $EvidenceDir "$Name.json")) | Out-Null; return Get-Content -LiteralPath (Join-Path $EvidenceDir "$Name.json") -Raw | ConvertFrom-Json } function Same($First,$Second) { return ($First | ConvertTo-Json -Depth 12 -Compress) -eq ($Second | ConvertTo-Json -Depth 12 -Compress) } +function IsNativeOrbitMiss($Before,$After,[int]$Stage) { + $beforeBoards=@($Before.puzzleStates|Where-Object stageId -eq $Stage) + $afterBoards=@($After.puzzleStates|Where-Object stageId -eq $Stage) + if($beforeBoards.Count-ne1-or$afterBoards.Count-ne1){return $false} + $camera=$beforeBoards[0].camera;$observed=$afterBoards[0].camera + if(!(Same $camera.target $observed.target)-or!(Same $camera.distance $observed.distance)){return $false} + $unchanged=$After|ConvertTo-Json -Depth 20|ConvertFrom-Json + @($unchanged.puzzleStates|Where-Object stageId -eq $Stage)[0].camera=$camera + if(!(Same $Before $unchanged)){return $false} + $from=[Numerics.Quaternion]::new($camera.rotation.x,$camera.rotation.y,$camera.rotation.z,$camera.rotation.w) + $to=[Numerics.Quaternion]::new($observed.rotation.x,$observed.rotation.y,$observed.rotation.z,$observed.rotation.w) + if([Math]::Abs($from.LengthSquared()-1)-gt.00001-or[Math]::Abs($to.LengthSquared()-1)-gt.00001){return $false} + $relative=[Numerics.Quaternion]::Normalize([Numerics.Quaternion]::Inverse($from)*$to) + if($relative.W-lt0){$relative=[Numerics.Quaternion]::Negate($relative)} + $angle=2*[Math]::Acos([Math]::Clamp([double]$relative.W,-1.0,1.0))*180/[Math]::PI + # One actual UP120 gives local-right -30 degrees; scenery/ground may clamp it. + return $relative.X-lt-.000001-and[Math]::Abs($relative.Y)-lt.00001-and[Math]::Abs($relative.Z)-lt.00001-and$angle-le30.002 +} function NativePointer([int[]]$Contacts) { $touchHelper=Join-Path $PSScriptRoot 'send-composition-touch.ps1' & pwsh -NoProfile -CommandWithArgs '$ErrorActionPreference="Stop"; $touchHelper=$args[0]; $touchContacts=@($args[1..($args.Count-1)]|ForEach-Object {[int]$_}); & $touchHelper -Contacts $touchContacts; exit $LASTEXITCODE' $touchHelper @Contacts diff --git a/tools/android/test-final-live-drag.ps1 b/tools/android/test-final-live-drag.ps1 index 28e2aea..f71aece 100644 --- a/tools/android/test-final-live-drag.ps1 +++ b/tools/android/test-final-live-drag.ps1 @@ -1,4 +1,4 @@ -param([Parameter(Mandatory)][string]$EvidenceDir,[Parameter(Mandatory)][string]$Fixture,[string]$CommonSource=(Join-Path $PSScriptRoot 'final-common.ps1'),[string]$ProgressionSource=(Join-Path $PSScriptRoot 'exercise-final-progression.ps1'),[string]$BlockedFixture,[string]$LiftBefore,[string]$LiftAfter,[string]$FineFixture) +param([Parameter(Mandatory)][string]$EvidenceDir,[Parameter(Mandatory)][string]$Fixture,[string]$CommonSource=(Join-Path $PSScriptRoot 'final-common.ps1'),[string]$ProgressionSource=(Join-Path $PSScriptRoot 'exercise-final-progression.ps1'),[string]$BlockedFixture,[string]$LiftBefore,[string]$LiftAfter,[string]$FineFixture,[string]$OrbitMissBefore,[string]$OrbitMissAfter) $ErrorActionPreference='Stop' New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null $checks=[Collections.Generic.List[object]]::new() @@ -25,6 +25,8 @@ function global:NativeMoveAndHold([string]$Name,$Board,[string]$Id,[int[]]$End){ function global:LiftMergeRoute([int]$Stage,[string]$Name,$Board,[int]$SourceCount=0,[int]$TargetCount=0){return [pscustomobject]@{State=$raw;Before=$Board;SourceId=$Board.balls[0].id}} try{ foreach($name in @('BallPoint','WorldSafe','BallClear','Same')){ReadFunction $CommonSource $name} + $ast=[Management.Automation.Language.Parser]::ParseFile($CommonSource,[ref]$null,[ref]$null) + if($ast.Find({param($n)$n-is[Management.Automation.Language.FunctionDefinitionAst]-and$n.Name-eq'IsNativeOrbitMiss'},$true)){ReadFunction $CommonSource 'IsNativeOrbitMiss'} foreach($name in @('Board','Orbit','MergeReal')){ReadFunction $ProgressionSource $name} $bound=$false;$replayError=$null;try{MergeReal 5 'replay-actual-stage5'|Out-Null}catch{$replayError=$_.Exception.Message;$bound=$replayError-match'Native merge bound reached'} $pitch=@($commands|Where-Object {$_.arguments.Count-ge7-and$_.arguments[2]-eq'swipe'-and$_.arguments[3]-eq$_.arguments[5]-and$_.arguments[4]-ne$_.arguments[6]}) @@ -203,7 +205,66 @@ try{ Record 'unsafe-detour-requires-exact-present-board-without-touch' ($passed-and$script:liftRequests.Count-eq0) $kind } } + if($OrbitMissBefore){ + if(!$OrbitMissAfter){throw 'Requires both original actual first-hit miss saves'} + ReadFunction $CommonSource 'IsNativeOrbitMiss' + ReadFunction $ProgressionSource 'LiftMergeRoute' + $raw=Get-Content -LiteralPath $OrbitMissBefore -Raw|ConvertFrom-Json + $missAfter=Get-Content -LiteralPath $OrbitMissAfter -Raw|ConvertFrom-Json + $stage=@($raw.puzzleStates|Where-Object {$_.balls.id-contains's04-b04'})[0].stageId + $board=Board $raw $stage + function Save([string]$Name){return $raw|ConvertTo-Json -Depth 20|ConvertFrom-Json} + function RevealBall([int]$Stage,[string]$Id,[string]$Name){return Board $raw $Stage} + function global:NativeMoveAndHold {return $missAfter} + $commands.Clear();$actual=LiftMergeRoute $stage 'actual-background-orbit-miss' $board + Record 'actual-first-hit-miss-keeps-exact-board-and-global-records' ($actual.Outcome-eq'MissedGrabOrbit'-and(Same $actual.State $missAfter)-and$commands.Count-eq1-and$commands[0].command-match'first-hit miss') 'Actual frozen Stage4 saves; no grabbed detour, manual-preservation or merge claim' + foreach($kind in @('source-and-camera','foreign','membership','collection','count','target','distance','yaw','roll','opposite-pitch','zero-rotation','over30','wrong-stage','missing-board','completed','other-puzzle','free-play','tutorial','non-unit-quaternion')){ + $bad=$missAfter|ConvertTo-Json -Depth 20|ConvertFrom-Json;$b=Board $bad $stage + switch($kind){ + 'source-and-camera'{@($b.balls|Where-Object id -eq 's04-b04')[0].position.y+=.1} + 'foreign'{$b.balls[0].position.x+=.1} + 'membership'{$b.groups[0].ballIds+= 's04-b04'} + 'collection'{$b.collectedBallIds+= 's04-b04'} + 'count'{$b.countedBallIds+= 's04-b04'} + 'target'{$b.camera.target.x+=.1} + 'distance'{$b.camera.distance+=.1} + 'yaw'{$b.camera.rotation=[pscustomobject]@{x=0;y=.08715574;z=0;w=.9961947}} + 'roll'{$b.camera.rotation=[pscustomobject]@{x=0;y=0;z=.08715574;w=.9961947}} + 'opposite-pitch'{$b.camera.rotation.x=-$b.camera.rotation.x} + 'zero-rotation'{$b.camera.rotation=$board.camera.rotation} + 'over30'{$b.camera.rotation=[pscustomobject]@{x=-.34202014;y=0;z=0;w=.93969262}} + 'wrong-stage'{$b.stageId=$stage+1} + 'missing-board'{$bad.puzzleStates=@($bad.puzzleStates|Where-Object stageId -ne $stage)} + 'completed'{$bad.completedStageIds+= $stage} + 'other-puzzle'{$bad.puzzleStates[0].balls[0].position.x+=.1} + 'free-play'{$bad.latestFreePlay.camera.distance+=.1} + 'tutorial'{$bad.seenTutorialIds+= 'unobserved'} + 'non-unit-quaternion'{$b.camera.rotation.x*=2;$b.camera.rotation.w*=2} + } + Record 'invalid-first-hit-miss-classification-is-rejected' (!(IsNativeOrbitMiss $raw $bad $stage)) $kind + if($kind-eq'source-and-camera'){ + $missAfter=$bad;$rejected=$false;$errorText=$null + try{LiftMergeRoute $stage 'genuine-movement-camera-violation' $board|Out-Null}catch{$rejected=$true;$errorText=$_.Exception.Message} + Record 'genuine-source-motion-retains-strict-manual-camera-guard' ($rejected-and$errorText-match'detour-manual-camera-preserved') $errorText + $missAfter=Get-Content -LiteralPath $OrbitMissAfter -Raw|ConvertFrom-Json + } + } + $commands.Clear();$script:projectedMissCamera=$false + function global:LiftMergeRoute {return $actual} + function Step([string]$Name,[string[]]$Arguments=@(),[int]$Delay=500){ + if(Same $CurrentBoard.camera (Board $missAfter $stage).camera){$script:projectedMissCamera=$true} + $commands.Add(@{name=$Name;arguments=$Arguments}) + } + $bound=$false;try{MergeReal $stage 'reproject-actual-miss'|Out-Null}catch{$bound=$_.Exception.Message-match'Native merge bound reached'} + Record 'miss-reprojects-actual-new-pose-with-original-bound' ($bound-and$script:projectedMissCamera-and$commands.Count-eq16) @{observedSavedPoseUsed=$script:projectedMissCamera;orbitAttempts=$commands.Count} + $actual.BeforeState.completedStageIds+= $stage + $errorText=$null;try{MergeReal $stage 'corrupted-miss-caller'|Out-Null}catch{$errorText=$_.Exception.Message} + Record 'caller-rechecks-all-records-before-using-miss-pose' ($errorText-match'actual-first-hit-miss-only') $errorText + $actual.State.completedStageIds+= $stage + $errorText=$null;try{MergeReal $stage 'corrupted-reveal-caller'|Out-Null}catch{$errorText=$_.Exception.Message} + Record 'caller-detects-record-corruption-before-pointer-snapshot' ($errorText-match'miss-global-records-unmodified') $errorText + } }catch{Record 'production-function-evaluation' $false $_.Exception.Message} $passed=$checks.Count-gt0-and@($checks|Where-Object passed -ne $true).Count-eq0 -@{passed=$passed;scope='Offline actual failed save replay and native-input/assertion negative controls; no ADB, no Unity, no native success claim';fixture=$Fixture;fixtureSha256=(Get-FileHash -LiteralPath $Fixture).Hash;routeFixtures=@(@($BlockedFixture,$LiftBefore,$LiftAfter,$FineFixture)|Where-Object {$_}|ForEach-Object {@{path=$_;sha256=(Get-FileHash -LiteralPath $_).Hash}});commonSource=$CommonSource;commonSha256=(Get-FileHash -LiteralPath $CommonSource).Hash;progressionSource=$ProgressionSource;progressionSha256=(Get-FileHash -LiteralPath $ProgressionSource).Hash;checks=$checks.ToArray()}|ConvertTo-Json -Depth 12|Set-Content -LiteralPath (Join-Path $EvidenceDir 'live-drag-result.json') +@{passed=$passed;scope='Offline actual failed save replay and native-input/assertion negative controls; no ADB, no Unity, no native success claim';fixture=$Fixture;fixtureSha256=(Get-FileHash -LiteralPath $Fixture).Hash;routeFixtures=@(@($BlockedFixture,$LiftBefore,$LiftAfter,$FineFixture,$OrbitMissBefore,$OrbitMissAfter)|Where-Object {$_}|ForEach-Object {@{path=$_;sha256=(Get-FileHash -LiteralPath $_).Hash}});commonSource=$CommonSource;commonSha256=(Get-FileHash -LiteralPath $CommonSource).Hash;progressionSource=$ProgressionSource;progressionSha256=(Get-FileHash -LiteralPath $ProgressionSource).Hash;checks=$checks.ToArray()}|ConvertTo-Json -Depth 12|Set-Content -LiteralPath (Join-Path $EvidenceDir 'live-drag-result.json') if(!$passed){exit 1};exit 0