Search counting targets at safe alternate camera elevations

This commit is contained in:
NINI
2026-10-10 15:30:07 +09:00
parent 3bfcac8a58
commit d4128d713d
2 changed files with 95 additions and 4 deletions
+16 -4
View File
@@ -3,13 +3,13 @@ param([Parameter(Mandatory)][string]$ApkPath,[Parameter(Mandatory)][string]$Evid
# This suite never pushes a save or injects completed stages. Every checkpoint is read from the APK. # This suite never pushes a save or injects completed stages. Every checkpoint is read from the APK.
function Board($State,[int]$Stage) { return @($State.puzzleStates | Where-Object stageId -eq $Stage)[0] } function Board($State,[int]$Stage) { return @($State.puzzleStates | Where-Object stageId -eq $Stage)[0] }
function Counts($Board) { return (@($Board.groups | ForEach-Object {$_.ballIds.Count} | Sort-Object) -join ',') } function Counts($Board) { return (@($Board.groups | ForEach-Object {$_.ballIds.Count} | Sort-Object) -join ',') }
function Orbit([string]$Name,$CurrentBoard,[switch]$Pitch,[int]$YawPixels=120) { function Orbit([string]$Name,$CurrentBoard,[switch]$Pitch,[int]$YawPixels=120,[int]$PitchPixels=120) {
if(!$CurrentBoard){$saved=Save "$Name-view";$CurrentBoard=if($saved.puzzleStates.Count){$saved.puzzleStates[-1]}else{$saved.latestFreePlay}} if(!$CurrentBoard){$saved=Save "$Name-view";$CurrentBoard=if($saved.puzzleStates.Count){$saved.puzzleStates[-1]}else{$saved.latestFreePlay}}
$points=@($CurrentBoard.balls|ForEach-Object{,(BallPoint $CurrentBoard $_.id)}) $points=@($CurrentBoard.balls|ForEach-Object{,(BallPoint $CurrentBoard $_.id)})
foreach($y in @(300,700)){foreach($x in @(650,900,1150,1400,1650)){ foreach($y in @(300,700)){foreach($x in @(650,900,1150,1400,1650)){
if(@($points|Where-Object{[Math]::Pow($_[0]-$x,2)+[Math]::Pow($_[1]-$y,2)-lt22500}).Count-eq0){ if(@($points|Where-Object{[Math]::Pow($_[0]-$x,2)+[Math]::Pow($_[1]-$y,2)-lt22500}).Count-eq0){
$endX=if($Pitch){$x}else{$x+$YawPixels};$endY=if($Pitch){$y+120}else{$y} $endX=if($Pitch){$x}else{$x+$YawPixels};$endY=if($Pitch){$y+$PitchPixels}else{$y}
if($endY-gt750){continue} if($endY-lt280-or$endY-gt750){continue}
if($YawPixels-ne120-and!$Pitch){ if($YawPixels-ne120-and!$Pitch){
# Latch the real >14px orbit drag, then reverse to the small net yaw. # Latch the real >14px orbit drag, then reverse to the small net yaw.
try{ try{
@@ -257,7 +257,19 @@ function CountRemaining([int]$Stage) {
$beforeCount=$board.countedBallIds.Count $beforeCount=$board.countedBallIds.Count
$id=$null $id=$null
foreach($ball in $uncounted){if(BallClear $board $ball.id){$id=$ball.id;break}} foreach($ball in $uncounted){if(BallClear $board $ball.id){$id=$ball.id;break}}
if(!$id){Orbit "stage-$Stage-count-safe-view-$attempt" $board;continue} if(!$id){
$rotation=[Numerics.Quaternion]::new($board.camera.rotation.x,$board.camera.rotation.y,$board.camera.rotation.z,$board.camera.rotation.w)
# Local pitch reverses its elevation effect after crossing the vertical pole.
$pitchPixels=if([Numerics.Vector3]::Transform([Numerics.Vector3]::UnitY,$rotation).Y-ge0){-120}else{120}
if([Numerics.Vector3]::Transform(-[Numerics.Vector3]::UnitZ,$rotation).Y-lt.5){$pitchPixels=-$pitchPixels}
Orbit "stage-$Stage-count-safe-view-$attempt" $board -Pitch:($attempt%4-eq3) -PitchPixels $pitchPixels
$view=Save "stage-$Stage-count-view-after-$attempt"
$unchanged=$view|ConvertTo-Json -Depth 20|ConvertFrom-Json
$viewBoard=Board $unchanged $Stage
if($viewBoard){$viewBoard.camera=$board.camera}
Check "stage-$Stage-count-view-preserves-whole-save-$attempt" (Same $state $unchanged) 'Only the actual manual view may change while searching for an uncounted first hit'
continue
}
TapBall "stage-$Stage-count-$attempt" $board $id TapBall "stage-$Stage-count-$attempt" $board $id
$after=Save "stage-$Stage-count-$attempt-after"; $newBoard=Board $after $Stage $after=Save "stage-$Stage-count-$attempt-after"; $newBoard=Board $after $Stage
if($null-eq$newBoard){return $after} if($null-eq$newBoard){return $after}
+79
View File
@@ -0,0 +1,79 @@
param([Parameter(Mandatory)][string]$EvidenceDir,[Parameter(Mandatory)][string]$Fixture,[string]$ProgressionSource=(Join-Path $PSScriptRoot 'exercise-final-progression.ps1'))
$ErrorActionPreference='Stop'
New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null
$checks=[Collections.Generic.List[object]]::new();$commands=[Collections.Generic.List[object]]::new()
function Record($Id,$Passed,$Actual){$checks.Add(@{id=$Id;passed=[bool]$Passed;actual=$Actual})}
function Check($Id,$Passed,$Actual){if(!$Passed){throw "$Id : $Actual"}}
function ReadFunction($Path,$Name){
$ast=[Management.Automation.Language.Parser]::ParseFile($Path,[ref]$null,[ref]$null)
$f=$ast.Find({param($n)$n-is[Management.Automation.Language.FunctionDefinitionAst]-and$n.Name-eq$Name},$true)
$expected=$f.Extent.Text.Replace("function $Name","function global:$Name")
. ([scriptblock]::Create($expected))
$actual=(Get-Command $Name).ScriptBlock.Ast.Extent.Text
Record "$Name-production-body-identity" ($actual-ceq$expected) @{source=$Path;sha256=[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($actual)))}
}
function Adb($Arguments){return 'Physical size: 1080x2400'}
$common=Join-Path $PSScriptRoot 'final-common.ps1'
foreach($name in @('Same','BallPoint','BallClear','WorldSafe','TapBall')){ReadFunction $common $name}
foreach($name in @('Board','Orbit','CountRemaining')){ReadFunction $ProgressionSource $name}
$captured=Get-Content -LiteralPath $Fixture -Raw|ConvertFrom-Json
$stage=@($captured.puzzleStates|Where-Object {$_.balls.id-contains's16-b05'})[0].stageId
if(!$stage-or(Board $captured $stage).countedBallIds.Count-ne5){throw 'Requires the exact captured Stage16 five-of-six blocked count state'}
function Save($Name){if($Name-match'-count-\d+-state$'){$script:stateReads++};return $script:state|ConvertTo-Json -Depth 20|ConvertFrom-Json}
function Step($Name,$Arguments=@(),$Delay=500){
$script:steps.Add(@{name=$Name;arguments=$Arguments})
if($Arguments[2]-eq'swipe'){
$b=Board $script:state $stage;$q=[Numerics.Quaternion]::new($b.camera.rotation.x,$b.camera.rotation.y,$b.camera.rotation.z,$b.camera.rotation.w)
$yaw=([int]$Arguments[5]-[int]$Arguments[3])*.25*[Math]::PI/180
$pitch=([int]$Arguments[6]-[int]$Arguments[4])*.25*[Math]::PI/180
$q=[Numerics.Quaternion]::CreateFromAxisAngle([Numerics.Vector3]::UnitY,$yaw)*$q*[Numerics.Quaternion]::CreateFromAxisAngle([Numerics.Vector3]::UnitX,$pitch)
if($script:mutation-ne'low-view-occluded'-or$pitch-ne0){$b.camera.rotation=[pscustomobject]@{x=$q.X;y=$q.Y;z=$q.Z;w=$q.W}}
switch($script:mutation){
'ball'{$b.balls[0].position.x+=.1}
'membership'{$b.groups[0].ballIds=@($b.groups[0].ballIds|Select-Object -Skip 1)}
'collection'{$b.collectedBallIds=@($b.collectedBallIds|Select-Object -Skip 1)}
'count'{$b.countedBallIds+= 's16-b05'}
'completed'{$script:state.completedStageIds+= $stage}
'other-puzzle'{$script:state.puzzleStates[0].balls[0].position.x+=.1}
'free-play'{$script:state.latestFreePlay.camera.distance+=.1}
'tutorial'{$script:state.seenTutorialIds+= 'unobserved'}
}
}
if($Arguments[2]-eq'tap'){
$b=Board $script:state $stage;$point=BallPoint $b 's16-b05'
$script:invalidTap=$script:invalidTap-or!(BallClear $b 's16-b05')-or[int]$Arguments[3]-ne$point[0]-or[int]$Arguments[4]-ne$point[1]
}
# A tap is observed, never translated into synthetic counting or completion.
}
try{
foreach($kind in @('captured-past-pole','near-upper-limit','low-view-occluded','ball','membership','collection','count','completed','other-puzzle','free-play','tutorial')){
$script:state=$captured|ConvertTo-Json -Depth 20|ConvertFrom-Json;$script:steps=[Collections.Generic.List[object]]::new();$script:mutation=$kind;$script:stateReads=0;$script:invalidTap=$false
if($kind-eq'near-upper-limit'){
$q=[Numerics.Quaternion]::CreateFromAxisAngle([Numerics.Vector3]::UnitX,(89*[Math]::PI/180))
(Board $script:state $stage).camera.rotation=[pscustomobject]@{x=$q.X;y=$q.Y;z=$q.Z;w=$q.W}
}
if($kind-eq'low-view-occluded'){
# Counterfactual low camera and front sphere; yaw leaf remains stationary.
# This tests signed pitch selection, never an actual reachable scene claim.
$b=Board $script:state $stage;$b.camera.rotation=[pscustomobject]@{x=0;y=0;z=0;w=1}
$p=@($b.balls|Where-Object id -eq 's16-b05')[0].position
@($b.balls|Where-Object id -eq 's16-b04')[0].position=[pscustomobject]@{x=$p.x;y=$p.y;z=($p.z-1)}
}
$before=$script:state|ConvertTo-Json -Depth 20|ConvertFrom-Json
$blocked=!(BallClear (Board $before $stage) 's16-b05')
$errorText=$null;try{CountRemaining $stage|Out-Null}catch{$errorText=$_.Exception.Message}
$taps=@($script:steps|Where-Object {$_.arguments[2]-eq'tap'})
$pitch=@($script:steps|Where-Object {$_.arguments[2]-eq'swipe'-and$_.arguments[3]-eq$_.arguments[5]})
if($kind-in@('captured-past-pole','near-upper-limit','low-view-occluded')){
$direction=if($kind-eq'near-upper-limit'){-120}else{120}
$correctPitch=$pitch.Count-gt0-and$pitch[0].name-match'count-safe-view'-and([int]$pitch[0].arguments[6]-[int]$pitch[0].arguments[4])-eq$direction
Record 'blocked-center-uses-lower-view-before-real-ID-tap-request' ($blocked-and$correctPitch-and$taps.Count-gt0-and!$script:invalidTap) @{kind=$kind;pitchCommands=$pitch;tapRequests=$taps.Count;scope='Offline projection and gesture requests only; scenery first hit and native count not evaluated'}
$unchanged=$script:state|ConvertTo-Json -Depth 20|ConvertFrom-Json;(Board $unchanged $stage).camera=(Board $before $stage).camera
Record 'forty-bound-no-fake-count-or-completion' ($errorText-match'Native counting bound reached'-and$script:stateReads-eq40-and(Same $before $unchanged)) @{kind=$kind;stateReads=$script:stateReads;gestureRequests=$script:steps.Count;error=$errorText}
Record 'lower-pitch-gesture-keeps-ui-safe-endpoints' ($correctPitch-and(WorldSafe @([int]$pitch[0].arguments[3],[int]$pitch[0].arguments[4]))-and(WorldSafe @([int]$pitch[0].arguments[5],[int]$pitch[0].arguments[6]))) $kind
}else{Record 'manual-view-search-rejects-save-mutation-before-tap' ($errorText-match'count-view-preserves-whole-save'-and$taps.Count-eq0) @{kind=$kind;error=$errorText}}
}
}catch{Record 'production-evaluation' $false $_.Exception.Message}
$passed=$checks.Count-gt0-and@($checks|Where-Object passed -ne $true).Count-eq0
@{passed=$passed;scope='Captured state replay with production function body identity and leaf gesture mocks. No ADB, native first hit or completed-stage claim.';fixture=$Fixture;fixtureSha256=(Get-FileHash $Fixture).Hash;progressionSource=$ProgressionSource;progressionSha256=(Get-FileHash $ProgressionSource).Hash;commonSha256=(Get-FileHash $common).Hash;checks=$checks.ToArray()}|ConvertTo-Json -Depth 14|Set-Content (Join-Path $EvidenceDir 'count-view-result.json')
if(!$passed){exit 1};exit 0