Search counting targets at safe alternate camera elevations
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user