Guard native world taps and zoom larger counting boards through real touch

This commit is contained in:
NINI
2026-10-06 23:54:11 +09:00
parent 931b5f42e7
commit 35e063b17b
3 changed files with 65 additions and 10 deletions
+40 -9
View File
@@ -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}}
+6 -1
View File
@@ -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')
+19
View File
@@ -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