diff --git a/tools/android/exercise-final-progression.ps1 b/tools/android/exercise-final-progression.ps1 index 446871d..25e07ff 100644 --- a/tools/android/exercise-final-progression.ps1 +++ b/tools/android/exercise-final-progression.ps1 @@ -3,7 +3,27 @@ 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. 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 Orbit([string]$Name) { Step $Name @('shell','input','swipe','2200','410','2320','410','350') } +function Orbit([string]$Name,$CurrentBoard) { + 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)}) + 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){ + Step $Name @('shell','input','swipe',"$x","$y","$($x+120)","$y",'350');return + } + }} + throw 'No safe blank orbit start in the actual board view.' +} +function ZoomOut([int]$Stage,$CurrentBoard) { + $helper=Join-Path $PSScriptRoot 'send-composition-touch.ps1' + foreach($contacts in @(@(30,400,999,1024),@(30,400,999,0),@(900,350,1,1024,1500,350,2,1024),@(1050,350,1,1024,1350,350,2,1024),@(1050,350,1,0,1350,350,2,0))){ + & pwsh -NoProfile -Command "& '$helper' -Contacts $($contacts-join',')" + $code=$LASTEXITCODE;$commands.Add(@{command='native sendTouch pinch-out';contacts=$contacts;exitCode=$code;utc=[DateTime]::UtcNow.ToString('o')});if($code-ne0){throw 'Native pinch failed'};Start-Sleep -Milliseconds 120 + } + Step "stage-$Stage-zoom-out" @() 300 + $after=Board (Save "stage-$Stage-zoom-out-state") $Stage + Check "stage-$Stage-pinch-preserves-board" ((Same $CurrentBoard.balls $after.balls)-and(Same $CurrentBoard.groups $after.groups)-and(Same $CurrentBoard.countedBallIds $after.countedBallIds)-and$after.camera.distance-gt$CurrentBoard.camera.distance) 'Actual two-finger zoom-out; no ball, membership or count edits' + return $after +} function MergeReal([int]$Stage,[string]$Name) { for($attempt=0;$attempt-lt16;$attempt++) { $before=Save "$Name-before-$attempt"; $board=Board $before $Stage @@ -24,7 +44,7 @@ function MergeReal([int]$Stage,[string]$Name) { $after=Save "$Name-after-$attempt"; $newBoard=Board $after $Stage if($null-eq$newBoard-or$newBoard.groups.Count-lt$oldCount){return $after} } - Orbit "$Name-orbit-$attempt" + Orbit "$Name-orbit-$attempt" $board } throw "Native merge bound reached for Stage $Stage; all actual attempts retained." } @@ -42,7 +62,7 @@ function SplitReal([int]$Stage,[int]$Selected,[string]$Name) { if($null-eq$actual-or$actual.groups.Count-gt$board.groups.Count){return $after} # Cancel failed selection before a new view; do not silently turn an invalid selection into a solve. Step "$Name-cancel-$attempt" @('shell','input','tap','312','832') - Orbit "$Name-orbit-$attempt" + Orbit "$Name-orbit-$attempt" $board } throw "Native split bound reached for Stage $Stage." } @@ -63,6 +83,7 @@ function Collect([int]$Stage) { } $state=Save "stage-$Stage-collected"; $board=Board $state $Stage Check "stage-$Stage-direct-collected-not-counted" ($board.groups.Count-eq1-and$board.collectedBallIds.Count-eq$board.balls.Count-and$board.countedBallIds.Count-eq0) 'Every ball directly dragged or merged; no injected or automatic count' + if($board.balls.Count-ge6){$board=ZoomOut $Stage $board} return $board } function CountRemaining([int]$Stage) { @@ -71,10 +92,13 @@ function CountRemaining([int]$Stage) { if($null-eq$board){return $state} $uncounted=@($board.balls|Where-Object {$board.countedBallIds-notcontains$_.id}) $beforeCount=$board.countedBallIds.Count - TapBall "stage-$Stage-count-$attempt" $board $uncounted[0].id + $id=$null + foreach($ball in $uncounted){if(WorldSafe (BallPoint $board $ball.id)){$id=$ball.id;break}} + if(!$id){Orbit "stage-$Stage-count-safe-view-$attempt" $board;continue} + TapBall "stage-$Stage-count-$attempt" $board $id $after=Save "stage-$Stage-count-$attempt-after"; $newBoard=Board $after $Stage if($null-eq$newBoard){return $after} - if($newBoard.countedBallIds.Count-eq$beforeCount){Orbit "stage-$Stage-count-reveal-$attempt"} + if($newBoard.countedBallIds.Count-eq$beforeCount){Orbit "stage-$Stage-count-reveal-$attempt" $newBoard} } throw "Native counting bound reached Stage $Stage." } @@ -114,13 +138,13 @@ try { $collected=Collect $stage if($stage-eq2) { TapBall 'partial2-first-count' $collected $collected.balls[0].id - Orbit 'partial2-orbit';$partial=Save 'partial2-before';$partialBoard=Board $partial 2 + Orbit 'partial2-orbit' $collected;$partial=Save 'partial2-before';$partialBoard=Board $partial 2 Check 'positive-partial2' ($partialBoard.countedBallIds.Count-eq1-and$partialBoard.collectedBallIds.Count-eq2-and!(Same $partialBoard.camera $collected.camera)) 'Actual1/2 plus changed camera' Step 'partial2-home' @('shell','input','tap','312','108');SelectStage 'replay1-unfinished' 1 $replayBefore=Save 'replay1-before';$replayBoard=Board $replayBefore 1;$point=BallPoint $replayBoard $replayBoard.balls[0].id Step 'replay1-unfinished-drag' @('shell','input','swipe',"$($point[0])","$($point[1])","$($point[0]+60)","$($point[1])",'500') Step 'replay1-home' @('shell','input','tap','312','108');Step 'free1-continue' @('shell','input','tap','1200','810') - Orbit 'free1-orbit';$three=Save 'two-unfinished-and-free-before' + Orbit 'free1-orbit' (Save 'free1-before-orbit').latestFreePlay;$three=Save 'two-unfinished-and-free-before' Check 'two-unfinished-plus-free' ($three.puzzleStates.Count-eq2-and$three.latestFreePlay.sourceStageId-eq1-and(Same $partialBoard (Board $three 2))-and$three.completedStageIds.Count-eq1) 'Replay1, actual partially counted2, and independent Free1 coexist without fixture' Restart 'three-board-restart';Step 'free1-restored' @('shell','input','tap','1200','810') Check 'two-puzzles-free-whole-restored' (Same $three (Save 'two-unfinished-and-free-after')) 'Pause force-stop startup Home then Free restore exact full save' @@ -131,8 +155,15 @@ try { } if($stage-eq20) { # Cache projected coordinates, then only native taps during speech; duplicates must not queue another word. - $actual=Board (Save 'twenty-before-rapid') 20 - $points=@($actual.balls|ForEach-Object{,(BallPoint $actual $_.id)}) + $visible=$false + for($view=0;$view-lt24;$view++){ + $actual=Board (Save "twenty-before-rapid-view-$view") 20 + $points=@($actual.balls|ForEach-Object{,(BallPoint $actual $_.id)}) + $visible=@($points|Where-Object{!(WorldSafe $_)}).Count-eq0 + for($a=0;$a-lt$points.Count;$a++){for($b=$a+1;$b-lt$points.Count;$b++){if([Math]::Pow($points[$a][0]-$points[$b][0],2)+[Math]::Pow($points[$a][1]-$points[$b][1],2)-lt10000){$visible=$false}}} + if($visible){break};Orbit "twenty-rapid-safe-view-$view" $actual + } + Check 'twenty-all-ten-safe-separated-projections' $visible ($points|ConvertTo-Json -Compress) StartAudio 'audio-ten-rapid' 24 $result.rapidStartedUtc=[DateTime]::UtcNow.ToString('o') for($pindex=0;$pindex-lt$points.Count;$pindex++){$p=$points[$pindex];Adb @('shell','input','tap',"$($p[0])","$($p[1])")|Out-Null;if($pindex-eq0){Adb @('shell','input','tap',"$($p[0])","$($p[1])")|Out-Null}} diff --git a/tools/android/final-common.ps1 b/tools/android/final-common.ps1 index 43ec486..f7d3654 100644 --- a/tools/android/final-common.ps1 +++ b/tools/android/final-common.ps1 @@ -34,7 +34,12 @@ function BallPoint($Board,[string]$Id) { $scale=($height/2)/([Math]::Tan(31*[Math]::PI/180)*$local.Z) return @([int][Math]::Round(($width/2)+$local.X*$scale),[int][Math]::Round(($height/2)-$local.Y*$scale)) } -function TapBall([string]$Name,$Board,[string]$Id) { $point=BallPoint $Board $Id; Step $Name @('shell','input','tap',"$($point[0])","$($point[1])") } +function WorldSafe($Point) { return $Point[0]-ge530-and$Point[0]-le1900-and$Point[1]-ge280-and$Point[1]-le750 } +function TapBall([string]$Name,$Board,[string]$Id) { + $point=BallPoint $Board $Id + if(!(WorldSafe $point)){throw "Refused unsafe native ball tap $Id at $($point-join',')"} + Step $Name @('shell','input','tap',"$($point[0])","$($point[1])") +} function StagePoint([int]$Id) { $index=$Id-1; return @([int][Math]::Round((.12+($index%6)*.152)*2400),[int][Math]::Round((1-(.76-[Math]::Floor($index/6)*.123))*1080)) } function SelectStage([string]$Name,[int]$Id) { Step "$Name-selection" @('shell','input','tap','1200','616') diff --git a/tools/android/test-final-safe-tap.ps1 b/tools/android/test-final-safe-tap.ps1 new file mode 100644 index 0000000..a9d1001 --- /dev/null +++ b/tools/android/test-final-safe-tap.ps1 @@ -0,0 +1,19 @@ +param([Parameter(Mandatory)][string]$EvidenceDir,[string]$Source=(Join-Path $PSScriptRoot 'final-common.ps1')) +$ErrorActionPreference='Stop' +New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null +$ast=[Management.Automation.Language.Parser]::ParseFile($Source,[ref]$null,[ref]$null) +foreach($name in @('WorldSafe','TapBall')){ + $function=$ast.Find({param($n)$n-is[Management.Automation.Language.FunctionDefinitionAst]-and$n.Name-eq$name},$true) + if($function){. ([scriptblock]::Create($function.Extent.Text))} +} +$script:taps=0 +function BallPoint($Board,[string]$Id){return $script:point} +function Step { $script:taps++ } +$checks=@(foreach($case in @(@{point=@(1200,500);safe=$true},@{point=@(312,108);safe=$false},@{point=@(-30,500);safe=$false},@{point=@(2500,500);safe=$false},@{point=@(1200,900);safe=$false})){ + $script:point=$case.point;$script:taps=0;$rejected=$false + try{TapBall 'probe' $null 'known-id'}catch{$rejected=$true} + @{point=$case.point;expectedSafe=$case.safe;rejected=$rejected;taps=$script:taps;passed=if($case.safe){!$rejected-and$script:taps-eq1}else{$rejected-and$script:taps-eq0}} +}) +$passed=@($checks|Where-Object passed -ne $true).Count-eq0 +@{source=$Source;passed=$passed;checks=$checks}|ConvertTo-Json -Depth 6|Set-Content (Join-Path $EvidenceDir 'safe-tap-result.json') +if(!$passed){exit 1};exit 0