From 7c2f11ef06e73c6d7aa99fba65fa6b69dd0edde7 Mon Sep 17 00:00:00 2001 From: NINI Date: Wed, 7 Oct 2026 00:56:46 +0900 Subject: [PATCH] Confirm native split UI before selecting and drag a visible selected ball --- tools/android/exercise-final-progression.ps1 | 26 +++++++++++++------- tools/android/final-common.ps1 | 16 ++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/tools/android/exercise-final-progression.ps1 b/tools/android/exercise-final-progression.ps1 index b9bb652..430e472 100644 --- a/tools/android/exercise-final-progression.ps1 +++ b/tools/android/exercise-final-progression.ps1 @@ -3,12 +3,14 @@ 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,$CurrentBoard) { +function Orbit([string]$Name,$CurrentBoard,[switch]$Pitch) { 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 + $endX=if($Pitch){$x}else{$x+120};$endY=if($Pitch){$y+120}else{$y} + if($endY-gt750){continue} + Step $Name @('shell','input','swipe',"$x","$y","$endX","$endY",'350');return } }} throw 'No safe blank orbit start in the actual board view.' @@ -62,12 +64,18 @@ function SplitReal([int]$Stage,[int]$Selected,[string]$Name) { $before=Save "$Name-before-$attempt"; $board=Board $before $Stage $group=@($board.groups|Sort-Object {$_.ballIds.Count} -Descending)[0] $ids=@(if($Stage-eq13-and$Selected-eq2){$group.ballIds[0];$group.ballIds[-1]}else{$group.ballIds|Select-Object -First $Selected}) - $board=RevealBall $Stage $ids[0] "$Name-choose-reveal-$attempt" - TapBall "$Name-choose-$attempt" $board $ids[0] - Step "$Name-mode-$attempt" @('shell','input','tap','312','832') + $mode=$false + foreach($chooseId in @($ids[-1])+@($group.ballIds)){ + $board=RevealBall $Stage $chooseId "$Name-choose-reveal-$attempt-$chooseId" + TapBall "$Name-choose-$attempt-$chooseId" $board $chooseId + $modeName="$Name-mode-$attempt-$chooseId" + Step $modeName @('shell','input','tap','312','832') + if(SplitModeVisible (Join-Path $EvidenceDir "$modeName.png")){$mode=$true;break} + } + if(!$mode){Orbit "$Name-reveal-group-$attempt" $board -Pitch;continue} foreach($id in $ids){$board=RevealBall $Stage $id "$Name-select-reveal-$attempt-$id";TapBall "$Name-select-$attempt-$id" $board $id} - $board=RevealBall $Stage $ids[0] "$Name-drag-reveal-$attempt" - $point=BallPoint $board $ids[0] + $board=RevealBall $Stage $ids[-1] "$Name-drag-reveal-$attempt" + $point=BallPoint $board $ids[-1] Step "$Name-drag-$attempt" @('shell','input','swipe',"$($point[0])","$($point[1])","$($point[0])","$($point[1]-160)",'500') $after=Save "$Name-after-$attempt"; $actual=Board $after $Stage if($null-eq$actual){$actual=$after.latestFreePlay} @@ -75,7 +83,7 @@ function SplitReal([int]$Stage,[int]$Selected,[string]$Name) { if(@($actual.groups|Where-Object{(($_.ballIds|Sort-Object)-join',')-eq(($ids|Sort-Object)-join',')}).Count-ne1){ $commands.Add(@{command='Actual split selected IDs differ: preserve attempt and retry through native merge/orbit';expected=$ids;actualGroups=$actual.groups;utc=[DateTime]::UtcNow.ToString('o')}) MergeReal $Stage "$Name-remerge-$attempt"|Out-Null - Orbit "$Name-retry-view-$attempt" (Board (Save "$Name-retry-state-$attempt") $Stage) + Orbit "$Name-retry-view-$attempt" (Board (Save "$Name-retry-state-$attempt") $Stage) -Pitch:($attempt%2-eq1) continue } Check "$Name-exact-selected-ids-$attempt" (@($actual.groups|Where-Object{(($_.ballIds|Sort-Object)-join',')-eq(($ids|Sort-Object)-join',')}).Count-eq1) 'Native split group is exactly the tapped selection, not an occluded substitute' @@ -83,7 +91,7 @@ function SplitReal([int]$Stage,[int]$Selected,[string]$Name) { } # 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" $board + Orbit "$Name-orbit-$attempt" $actual -Pitch:($attempt%2-eq1) } throw "Native split bound reached for Stage $Stage." } diff --git a/tools/android/final-common.ps1 b/tools/android/final-common.ps1 index a150059..e36ca1d 100644 --- a/tools/android/final-common.ps1 +++ b/tools/android/final-common.ps1 @@ -57,6 +57,22 @@ function BallClear($Board,[string]$Id) { } return $true } +function SplitModeVisible([string]$Path) { + Add-Type -AssemblyName System.Drawing + $image=[Drawing.Bitmap]::new($Path) + $min=450;$max=0;$dark=0 + try { + if($image.Width-ne2400-or$image.Height-ne1080){throw 'Split UI probe requires the documented 2400x1080 AVD'} + for($y=802;$y-lt862;$y++){for($x=180;$x-lt450;$x++){ + $pixel=$image.GetPixel($x,$y) + if($pixel.R-lt100-and$pixel.G-lt110-and$pixel.B-lt150){$dark++;$min=[Math]::Min($min,$x);$max=[Math]::Max($max,$x)} + }} + } finally {$image.Dispose()} + # Korean fixed-size label: 취소 spans79px; 나누기 spans117px in the actual APK captures. + $visible=$dark-ge300-and($max-$min)-ge60-and($max-$min)-le95 + $commands.Add(@{command='Read actual native split button label bounds';png=$Path;darkPixels=$dark;minX=$min;maxX=$max;splitCancelLabel=$visible;utc=[DateTime]::UtcNow.ToString('o')}) + return $visible +} function TapBall([string]$Name,$Board,[string]$Id) { $point=BallPoint $Board $Id if(!(WorldSafe $point)){throw "Refused unsafe native ball tap $Id at $($point-join',')"}