272 lines
27 KiB
PowerShell
272 lines
27 KiB
PowerShell
param([Parameter(Mandatory)][string]$ApkPath,[Parameter(Mandatory)][string]$EvidenceDir,[string]$Serial='emulator-5554')
|
|
. (Join-Path $PSScriptRoot 'final-common.ps1') -ApkPath $ApkPath -EvidenceDir $EvidenceDir -Serial $Serial
|
|
$result.milestone='PlaygroundInteraction';$result.startedUtc=[DateTime]::UtcNow.ToString('o')
|
|
$result.scope='Native boundary fixtures after unmodified Fresh30; Editor oracle is compared with actual player saves, never accepted as native pass'
|
|
$helper=Join-Path $PSScriptRoot 'send-composition-touch.ps1'
|
|
$fixturePath=Join-Path $projectRoot 'artifacts/verification/PlaygroundInteraction/NativeFixtures/native-fixtures.json'
|
|
$observationPath=Join-Path $projectRoot 'artifacts/verification/PlaygroundInteraction/native-observation.json'
|
|
$pending=[Collections.Generic.List[string]]::new()
|
|
$original=$null
|
|
function NativeTouch([int[]]$Contacts) {
|
|
& pwsh -NoProfile -CommandWithArgs '$ErrorActionPreference="Stop"; $touchHelper=$args[0]; $touchContacts=@($args[1..($args.Count-1)]|ForEach-Object {[int]$_}); & $touchHelper -Contacts $touchContacts; exit $LASTEXITCODE' $helper @Contacts
|
|
$actualExit=$LASTEXITCODE
|
|
$commands.Add(@{command='EmulatorController.sendTouch';contacts=$Contacts;exitCode=$actualExit;utc=[DateTime]::UtcNow.ToString('o')})
|
|
if($actualExit-ne0){throw "Actual native gesture failed: $actualExit"}
|
|
}
|
|
function Clone($Value){return $Value|ConvertTo-Json -Depth 20|ConvertFrom-Json}
|
|
function Float32Record($Value,[string]$Parent=''){
|
|
if($Value-is[Collections.IDictionary]){
|
|
$keys=@($Value.Keys)
|
|
foreach($key in $keys){
|
|
$isFloat=($Parent-cin@('position','target')-and$key-cin@('x','y','z'))-or($Parent-ceq'rotation'-and$key-cin@('x','y','z','w'))-or($key-ceq'distance'-and$keys-ccontains'target'-and$keys-ccontains'rotation')
|
|
if($isFloat){
|
|
$number=$Value[$key]
|
|
if(($number-isnot[long]-and$number-isnot[double])-or![single]::IsFinite([single]$number)){throw 'Preserved DTO float must be a finite JSON number'}
|
|
$Value[$key]='float32:'+([BitConverter]::SingleToInt32Bits([single]$number)).ToString('X8')
|
|
}else{Float32Record $Value[$key] $key}
|
|
}
|
|
}elseif($Value-is[Collections.IList]){foreach($item in $Value){Float32Record $item $Parent}}
|
|
}
|
|
function Same($First,$Second){
|
|
# Only DTO float fields use IEEE754 identity; all keys, array order and other values stay exact.
|
|
try{
|
|
$records=@(foreach($value in @($First,$Second)){
|
|
$copy=ConvertFrom-Json -InputObject (ConvertTo-Json -InputObject $value -Depth 20 -Compress) -AsHashtable -NoEnumerate
|
|
Float32Record $copy
|
|
ConvertTo-Json -InputObject $copy -Depth 20 -Compress
|
|
})
|
|
return $records[0]-ceq$records[1]
|
|
}catch{return $false}
|
|
}
|
|
function SameProgress($First,$Second){
|
|
# Save slots are keyed by unique stageId; preserve every other array's strict order.
|
|
try{
|
|
$firstCopy=Clone $First;$secondCopy=Clone $Second
|
|
foreach($copy in @($firstCopy,$secondCopy)){
|
|
if($copy-isnot[pscustomobject]-or$copy.PSObject.Properties.Name-cnotcontains'puzzleStates'-or$copy.puzzleStates-isnot[array]-or$copy.puzzleStates.Count-gt30){return $false}
|
|
$ids=[Collections.Generic.HashSet[long]]::new()
|
|
foreach($slot in $copy.puzzleStates){
|
|
if($slot-isnot[pscustomobject]-or$slot.PSObject.Properties.Name-cnotcontains'stageId'-or($slot.stageId-isnot[long]-and$slot.stageId-isnot[int])-or$slot.stageId-lt1-or$slot.stageId-gt30-or!$ids.Add([long]$slot.stageId)){return $false}
|
|
}
|
|
$copy.puzzleStates=@($copy.puzzleStates|Sort-Object stageId)
|
|
}
|
|
return Same $firstCopy $secondCopy
|
|
}catch{return $false}
|
|
}
|
|
function Slot($State,[string]$Type,[int]$Stage){if($Type-eq'free'){return $State.latestFreePlay};return @($State.puzzleStates|Where-Object stageId -eq $Stage)[0]}
|
|
function SameBalls($First,$Second,[double]$Tolerance=.004){
|
|
if((($First.balls.id|Sort-Object)-join',')-ne(($Second.balls.id|Sort-Object)-join',')){return $false}
|
|
foreach($ball in $First.balls){$match=@($Second.balls|Where-Object id -eq $ball.id);if($match.Count-ne1){return $false};$point=$match[0].position
|
|
if([Math]::Abs($ball.position.x-$point.x)-gt$Tolerance-or[Math]::Abs($ball.position.y-$point.y)-gt$Tolerance-or[Math]::Abs($ball.position.z-$point.z)-gt$Tolerance){return $false}}
|
|
return $true
|
|
}
|
|
function SameMembership($First,$Second){
|
|
foreach($board in @($First,$Second)){
|
|
$ids=@($board.balls.id);$members=@($board.groups|ForEach-Object {$_.ballIds})
|
|
if(@($ids|Sort-Object -Unique).Count-ne$ids.Count-or@($members|Sort-Object -Unique).Count-ne$members.Count-or(($ids|Sort-Object)-join',')-ne(($members|Sort-Object)-join',')){return $false}
|
|
}
|
|
return Same @($First.groups|Sort-Object id|ForEach-Object {[ordered]@{id=$_.id;ids=@($_.ballIds|Sort-Object)}}) @($Second.groups|Sort-Object id|ForEach-Object {[ordered]@{id=$_.id;ids=@($_.ballIds|Sort-Object)}})
|
|
}
|
|
function InBounds($Board,[int]$Stage){
|
|
$bounds=@($fixtures.stages|Where-Object stage -eq $Stage)[0]
|
|
foreach($ball in $Board.balls){foreach($axis in @('x','y','z')){if($ball.position.$axis-lt$bounds.min.$axis+.499-or$ball.position.$axis-gt$bounds.max.$axis-.499){return $false}}}
|
|
return $true
|
|
}
|
|
function FixtureState($Board,[string]$Type,[int]$Stage){
|
|
$state=Clone $original
|
|
# These untouched independent slots are labelled fixtures, not sequential completion evidence.
|
|
$state.puzzleStates=@(3,6|ForEach-Object {Clone (@($fixtures.stages|Where-Object stage -eq $_)[0].initial)})
|
|
if($Type-eq'free'){$state.latestFreePlay=Clone $Board}else{$state.puzzleStates=@($state.puzzleStates|Where-Object stageId -ne $Stage)+@(Clone $Board)}
|
|
return $state
|
|
}
|
|
function BoardScreen([string]$Path){
|
|
Add-Type -AssemblyName System.Drawing;$image=[Drawing.Bitmap]::new($Path)
|
|
$pixels=[Collections.Generic.List[object]]::new();$gold=0;$world=0
|
|
try{
|
|
if($image.Width-eq2400-and$image.Height-eq1080){
|
|
foreach($point in @(@(80,70),@(380,70),@(80,140),@(380,140))){
|
|
$pixel=$image.GetPixel($point[0],$point[1]);$pixels.Add(@{x=$point[0];y=$point[1];r=$pixel.R;g=$pixel.G;b=$pixel.B})
|
|
if($pixel.R-ge230-and$pixel.G-ge185-and$pixel.G-le210-and$pixel.B-ge60-and$pixel.B-le90){$gold++}
|
|
}
|
|
foreach($x in @(600,1200,1800)){foreach($y in @(300,500,700)){
|
|
$pixel=$image.GetPixel($x,$y);$pixels.Add(@{x=$x;y=$y;r=$pixel.R;g=$pixel.G;b=$pixel.B})
|
|
if($pixel.R-gt20-or$pixel.G-gt20-or$pixel.B-gt20){$world++}
|
|
}}
|
|
}
|
|
return @{ready=$gold-eq4-and$world-ge3;width=$image.Width;height=$image.Height;goldCorners=$gold;nonblackWorldSamples=$world;pixels=$pixels.ToArray()}
|
|
}finally{$image.Dispose()}
|
|
}
|
|
function WaitBoard([string]$Name){
|
|
$observations=[Collections.Generic.List[object]]::new();$ready=$false;$state=$null
|
|
try{
|
|
for($attempt=0;$attempt-lt30;$attempt++){
|
|
$stepName="$Name-board-ready-$attempt";Step $stepName @() 500
|
|
$png=Join-Path $EvidenceDir "$stepName.png";$screen=BoardScreen $png
|
|
$state=Save "$stepName-save";$savePath=Join-Path $EvidenceDir "$stepName-save.json"
|
|
$observations.Add(@{attempt=$attempt;utc=[DateTime]::UtcNow.ToString('o');png=$png;pngSha256=(Get-FileHash -LiteralPath $png).Hash;screen=$screen;save=$savePath;saveSha256=(Get-FileHash -LiteralPath $savePath).Hash})
|
|
if($screen.ready){$ready=$true;break}
|
|
}
|
|
Check "$Name-board-ui-ready" $ready 'Actual 2400x1080 board: four golden Home-button corners and nonblack world samples; unchanged pulled save alone is never entry proof; at most 30 observations at 500ms intervals'
|
|
Copy-Item -LiteralPath $observations[-1].save -Destination (Join-Path $EvidenceDir "$Name-entered.json")
|
|
return $state
|
|
}finally{@{ready=$ready;finishedUtc=[DateTime]::UtcNow.ToString('o');observations=$observations.ToArray()}|ConvertTo-Json -Depth 8|Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-board-readiness.json") -Encoding utf8}
|
|
}
|
|
function LoadFixture([string]$Name,$State,[string]$Type,[int]$Stage){
|
|
Adb @('shell','am','force-stop',$package)|Out-Null
|
|
$path=Join-Path $EvidenceDir "$Name-injected.json";$State|ConvertTo-Json -Depth 20|Set-Content -LiteralPath $path -Encoding utf8
|
|
@{name=$Name;origin='Actual Fresh30/thirty-completed.json; explicitly labelled injected boundary case';originalFile=$originalPath;originalSha256=(Get-FileHash -LiteralPath $originalPath).Hash;fixtureSha256=(Get-FileHash -LiteralPath $path).Hash;changedFields=@('puzzleStates','latestFreePlay');preservedFields=@('schemaVersion','completedStageIds','seenTutorialIds');utc=[DateTime]::UtcNow.ToString('o')}|ConvertTo-Json -Depth 5|Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-origin.json")
|
|
Adb @('push',$path,$remote)|Out-Null;Adb @('push',$path,($remote+'.bak'))|Out-Null
|
|
StartHome "$Name-home"
|
|
if($Type-eq'free'){Step "$Name-enter-free" @('shell','input','tap','1200','810') 0}else{SelectStage "$Name-enter-puzzle" $Stage}
|
|
return WaitBoard $Name
|
|
}
|
|
function PreservesOtherSlots([string]$Name,$Before,$After,[string]$Type,[int]$Stage){
|
|
Check "$Name-progress-preserved" ((Same $Before.completedStageIds $After.completedStageIds)-and(Same $Before.seenTutorialIds $After.seenTutorialIds)-and$After.schemaVersion-eq1) 'Completion/unlock/tutorial/schema unchanged'
|
|
$otherBefore=if($Type-eq'free'){@($Before.puzzleStates)}else{@($Before.puzzleStates|Where-Object stageId -ne $Stage)}
|
|
$otherAfter=if($Type-eq'free'){@($After.puzzleStates)}else{@($After.puzzleStates|Where-Object stageId -ne $Stage)}
|
|
Check "$Name-other-puzzles-preserved" (Same $otherBefore $otherAfter) 'Two unrelated puzzle slots remain exact'
|
|
if($Type-eq'puzzle'){Check "$Name-unrelated-free-preserved" (Same $Before.latestFreePlay $After.latestFreePlay) 'Unrelated Free record remains exact'}
|
|
}
|
|
function HoldDrag([string]$Name,$Board,[string]$Id,[int]$EndX,[int]$EndY,[int]$Hold,[switch]$Cancel,[int]$ReversePixels=0){
|
|
$point=BallPoint $Board $Id
|
|
Check "$Name-safe-start-end" ((WorldSafe $point)-and(WorldSafe @($EndX,$EndY))) 'Documented phone UI-safe coordinates; actual whole ID projected exactly once'
|
|
NativeTouch @($point[0],$point[1],71,1024);Start-Sleep -Milliseconds 100
|
|
NativeTouch @($EndX,$EndY,71,1024);Start-Sleep -Milliseconds $Hold
|
|
Step "$Name-during-live-camera" @() 0
|
|
$during=Save "$Name-during-save"
|
|
Check "$Name-auto-preview-not-saved" (Same $Board $during.latestFreePlay) 'Live auto camera and preview never overwrite confirmed board/manual camera'
|
|
if($ReversePixels-gt0){$EndX-=$ReversePixels;NativeTouch @($EndX,$EndY,71,1024);Step "$Name-immediate-reverse" @() 100}
|
|
if($Cancel){Adb @('shell','input','keyevent','3')|Out-Null;Start-Sleep -Milliseconds 250}
|
|
NativeTouch @($EndX,$EndY,71,0)
|
|
if($Cancel){Adb @('shell','am','start','-W','-n',$activity)|Out-Null}
|
|
Step "$Name-after-return" @() 2500
|
|
return Save "$Name-dropped"
|
|
}
|
|
function BlueFriends([string]$Path){
|
|
# Targeted live observation for two isolated blue friends. It is never used as an ID/count verdict.
|
|
Add-Type -AssemblyName System.Drawing;$bitmap=[Drawing.Bitmap]::new($Path)
|
|
try{
|
|
if($bitmap.Width-ne2400-or$bitmap.Height-ne1080){throw 'Live friend vision requires actual phone viewport'}
|
|
$width=331;$height=119;$mask=[bool[]]::new($width*$height)
|
|
for($y=0;$y-lt$height;$y++){for($x=0;$x-lt$width;$x++){$pixel=$bitmap.GetPixel(530+$x*4,280+$y*4);$mask[$y*$width+$x]=$pixel.R-lt135-and$pixel.B-ge100-and($pixel.B-$pixel.R)-gt45-and($pixel.G-$pixel.R)-gt20}}
|
|
$friends=[Collections.Generic.List[object]]::new();$queue=[Collections.Generic.Queue[int]]::new()
|
|
for($index=0;$index-lt$mask.Length;$index++){
|
|
if(!$mask[$index]){continue};$mask[$index]=$false;$queue.Enqueue($index);$count=0;$sumX=0;$sumY=0;$touchesBorder=$false
|
|
while($queue.Count){$cell=$queue.Dequeue();$x=$cell%$width;$y=[int][Math]::Floor($cell/$width);$count++;$sumX+=$x;$sumY+=$y
|
|
if($x-eq0-or$x-eq($width-1)-or$y-eq0-or$y-eq($height-1)){$touchesBorder=$true}
|
|
foreach($neighbor in @(($cell-1),($cell+1),($cell-$width),($cell+$width))){if($neighbor-lt0-or$neighbor-ge$mask.Length){continue};if([Math]::Abs(($neighbor%$width)-$x)+[Math]::Abs([int][Math]::Floor($neighbor/$width)-$y)-ne1){continue};if($mask[$neighbor]){$mask[$neighbor]=$false;$queue.Enqueue($neighbor)}}}
|
|
if($count-ge30-and!$touchesBorder){$friends.Add(@{x=530+4*$sumX/$count;y=280+4*$sumY/$count;sampledBluePixels=$count})}
|
|
}
|
|
return @($friends|Sort-Object sampledBluePixels -Descending|Select-Object -First 2)
|
|
}finally{$bitmap.Dispose()}
|
|
}
|
|
function ApproachDepth([string]$Name,$Source){
|
|
$state=FixtureState $Source 'free' 1;$before=LoadFixture $Name $state 'free' 1
|
|
$initial=Clone $before.latestFreePlay
|
|
Check "$Name-real-initial-depth" ([Math]::Abs($initial.balls[0].position.z-$initial.balls[1].position.z)-gt3) 'Manual identity camera: original same-ID friends differ by more than three actual world depth units'
|
|
for($attempt=0;$attempt-lt5;$attempt++){
|
|
$current=if($attempt-eq0){$before}else{Save "$Name-before-attempt-$attempt"};$board=$current.latestFreePlay
|
|
$point=BallPoint $board 'native-depth-front';Check "$Name-first-hit-safe-$attempt" (WorldSafe $point) ($point-join',')
|
|
$pointer=@(($point[0]+20),$point[1]);NativeTouch @($point[0],$point[1],73,1024);NativeTouch @($pointer[0],$pointer[1],73,1024);Start-Sleep -Milliseconds 800
|
|
for($step=0;$step-lt45;$step++){
|
|
$label="$Name-live-$attempt-$step";Step $label @() 0;$friends=@(BlueFriends (Join-Path $EvidenceDir "$label.png"))
|
|
$commands.Add(@{command='Observe actual live blue friends for input steering';png="$label.png";friends=$friends;pointer=$pointer;utc=[DateTime]::UtcNow.ToString('o')})
|
|
if($friends.Count-lt2){break}
|
|
$sourcePoint=@($friends|Sort-Object {[Math]::Pow($_.x-$pointer[0],2)+[Math]::Pow($_.y-$pointer[1],2)})[0]
|
|
$targetPoint=@($friends|Where-Object {$_-ne$sourcePoint})[0]
|
|
$dx=$targetPoint.x-$sourcePoint.x;$dy=$targetPoint.y-$sourcePoint.y;$length=[Math]::Sqrt($dx*$dx+$dy*$dy)
|
|
if($length-lt2){break};$scale=[Math]::Min(1,12/$length)
|
|
$next=@([int][Math]::Round($pointer[0]+$dx*$scale),[int][Math]::Round($pointer[1]+$dy*$scale))
|
|
if(!(WorldSafe $next)){throw 'Live depth approach leaves UI-safe input region'}
|
|
$pointer=$next;NativeTouch @($pointer[0],$pointer[1],73,1024)
|
|
}
|
|
NativeTouch @($pointer[0],$pointer[1],73,0);Step "$Name-drop-$attempt" @() 2500;$after=Save "$Name-after-$attempt"
|
|
if($after.latestFreePlay.groups.Count-eq1){
|
|
$actual=$after.latestFreePlay;$first=$actual.balls[0].position;$second=$actual.balls[1].position;$distance=[Math]::Sqrt([Math]::Pow($first.x-$second.x,2)+[Math]::Pow($first.y-$second.y,2)+[Math]::Pow($first.z-$second.z,2))
|
|
Check "$Name-actual-id-contact-merge" ((($actual.balls.id|Sort-Object)-join',')-eq(($initial.balls.id|Sort-Object)-join',')-and(($actual.groups[0].ballIds|Sort-Object)-join',')-eq(($initial.balls.id|Sort-Object)-join',')-and$distance-ge.998-and$distance-le1.002-and(InBounds $actual 1)-and(Same $actual.camera $initial.camera)) "Actual exact two IDs, real3D contact distance=$distance, saved manual camera unchanged"
|
|
PreservesOtherSlots $Name $state $after 'free' 1;return
|
|
}
|
|
}
|
|
throw 'Actual deep friend native merge was not achieved within bounded live-screen steering; all PNG/input/3D drop attempts retained'
|
|
}
|
|
try {
|
|
Check 'clean-source' (!$result.sourceDirty) $result.sourceCommit
|
|
Check 'designated-avd-only' ($Serial-eq'emulator-5554'-and(Adb @('emu','avd','name'))-match'^Medium_Phone_API_37\.0') 'Sole authorized AVD'
|
|
$size=Adb @('shell','wm','size');Check 'phone-touch-physical-resolution' ($size-match'Physical size: 1080x2400'-and$size-notmatch'Override size:') $size
|
|
$build=Get-Content -LiteralPath (Join-Path $EvidenceDir 'androidbuild-evidence.json') -Raw|ConvertFrom-Json
|
|
Check 'exact-final-build' ($build.status-eq'passed'-and$build.exitCode-eq0-and$build.sourceCommit-eq$result.sourceCommit-and$build.sourceCommitEnd-eq$result.sourceCommit-and!$build.sourceDirty-and!$build.sourceDirtyEnd-and$build.apkSha256-eq$result.apkSha256) $result.apkSha256
|
|
$installed=(Adb @('shell','pm','path',$package)).Replace('package:','').Split("`n")[0].Trim();Adb @('pull',$installed,(Join-Path $EvidenceDir 'installed-base.apk'))|Out-Null
|
|
Check 'installed-exact-final-apk' ((Get-FileHash -LiteralPath (Join-Path $EvidenceDir 'installed-base.apk')).Hash-eq$result.apkSha256) $result.apkSha256
|
|
$fixtures=Get-Content -LiteralPath $fixturePath -Raw|ConvertFrom-Json
|
|
Check 'same-source-actual-editor-fixtures' ($fixtures.sourceCommit-eq$result.sourceCommit-and$fixtures.sourceDirty-eq'False'-and$fixtures.stages.Count-eq30-and$fixtures.restores.Count-eq9-and$fixtures.drags.Count-eq15) 'Actual Main oracle from this final PlayMode source, native evaluation follows separately'
|
|
Copy-Item -LiteralPath $fixturePath -Destination (Join-Path $EvidenceDir 'native-fixtures.json')
|
|
$originalPath=Join-Path (Split-Path $EvidenceDir -Parent) 'Fresh30/thirty-completed.json'
|
|
$original=Get-Content -LiteralPath $originalPath -Raw|ConvertFrom-Json
|
|
Check 'actual-fresh30-origin' ($original.completedStageIds.Count-eq30-and(($original.completedStageIds|Sort-Object)-join',')-eq(1..30-join',')-and$original.latestFreePlay.sourceStageId-eq30) 'No completion injection in normal progression; boundary fixtures use its retained exact records'
|
|
foreach($fixture in $fixtures.restores){
|
|
$source=if($fixture.slot-eq'free'){$fixture.freeSource}else{$fixture.puzzleSource};$expected=if($fixture.slot-eq'free'){$fixture.freeExpected}else{$fixture.puzzleExpected}
|
|
$state=FixtureState $source $fixture.slot $fixture.stage
|
|
$watch=[Diagnostics.Stopwatch]::StartNew();$after=LoadFixture $fixture.name $state $fixture.slot $fixture.stage;$watch.Stop()
|
|
$actual=Slot $after $fixture.slot $fixture.stage
|
|
Check "$($fixture.name)-source-stage-retained" ($(if($fixture.slot-eq'free'){$actual.sourceStageId}else{$actual.stageId})-eq$fixture.stage) 'Affected slot retains exact puzzle/source stage connection'
|
|
Check "$($fixture.name)-actual-player-result" ((SameBalls $expected $actual)-and(SameMembership $expected $actual)-and(InBounds $actual $fixture.stage)) 'Actual saved player result equals actual Unity oracle within .004 units; no ID or membership weakening'
|
|
if($fixture.slot-eq'puzzle'){Check "$($fixture.name)-count-collect-preserved-or-exact-reset" ((Same $expected.collectedBallIds $actual.collectedBallIds)-and(Same $expected.countedBallIds $actual.countedBallIds)) 'Expected position-only repair retains exact lists; impossible reset uses exact source initial lists'}
|
|
PreservesOtherSlots $fixture.name $state $after $fixture.slot $fixture.stage
|
|
if($fixture.name-eq'multiple-repairs'){
|
|
Adb @('pull',($remote+'.bak'),(Join-Path $EvidenceDir 'multiple-repairs-backup.json'))|Out-Null
|
|
$backup=Get-Content -LiteralPath (Join-Path $EvidenceDir 'multiple-repairs-backup.json') -Raw|ConvertFrom-Json
|
|
Check 'multiple-repairs-one-disk-write' ((Same $state $backup)-and!(Same $state $after)) 'Two repaired groups: backup is injected original and primary is complete repaired record; second write would replace backup'
|
|
}
|
|
@{name=$fixture.name;entryObservationMilliseconds=$watch.Elapsed.TotalMilliseconds;editorEntryMilliseconds=$fixture.editorEntryMilliseconds;method='Native wall time includes process launch/home/readiness/UI/ADB/save pull; does not isolate GameApp entry CPU';utc=[DateTime]::UtcNow.ToString('o')}|ConvertTo-Json|Set-Content -LiteralPath (Join-Path $EvidenceDir "$($fixture.name)-timing.json")
|
|
Step "$($fixture.name)-home-again" @('shell','input','tap','312','108')
|
|
if($fixture.slot-eq'free'){Step "$($fixture.name)-reenter" @('shell','input','tap','1200','810')}else{SelectStage "$($fixture.name)-reenter" $fixture.stage}
|
|
Check "$($fixture.name)-reentry-exact" (SameProgress $after (Save "$($fixture.name)-reentered")) 'Repair/reset is stable; only unique stage-keyed puzzle slot order may differ, all nested values and arrays remain exact'
|
|
}
|
|
foreach($fixture in $fixtures.drags){
|
|
$state=FixtureState $fixture.source 'free' 1;$before=LoadFixture $fixture.name $state 'free' 1;$board=$before.latestFreePlay
|
|
Check "$($fixture.name)-whole-group-input" (Same $fixture.source $board) 'No entry correction of native drag source'
|
|
$after=HoldDrag $fixture.name $board $fixture.ballId ([int][Math]::Round($fixture.end.x)) ([int][Math]::Round($fixture.end.y)) 500
|
|
Check "$($fixture.name)-actual-stop-all-ids" ((SameBalls $fixture.expected $after.latestFreePlay)-and(SameMembership $fixture.expected $after.latestFreePlay)-and(($board.balls.id|Sort-Object)-join',')-eq(($after.latestFreePlay.balls.id|Sort-Object)-join',')-and(InBounds $after.latestFreePlay 1)-and(Same $board.camera $after.latestFreePlay.camera)) 'Actual whole-ID player positions/membership equal expected boundary/foliage/blocked contact/safe2+3 result; saved manual camera unchanged'
|
|
PreservesOtherSlots $fixture.name $state $after 'free' 1
|
|
}
|
|
$thin=@($fixtures.drags|Where-Object kind -eq 'thin-obstacle')[0]
|
|
$before=LoadFixture 'thin-fast-reverse' (FixtureState $thin.source 'free' 1) 'free' 1
|
|
$after=HoldDrag 'thin-fast-reverse' $before.latestFreePlay $thin.ballId ([int][Math]::Round($thin.end.x)) ([int][Math]::Round($thin.end.y)) 300 -ReversePixels 80
|
|
$stopped=@($thin.expected.balls|Where-Object id -eq $thin.ballId)[0].position;$reversed=@($after.latestFreePlay.balls|Where-Object id -eq $thin.ballId)[0].position
|
|
Check 'thin-fast-reverse-opposite-immediate-safe-drop' ($reversed.x-lt$stopped.x-.05-and(SameMembership $thin.source $after.latestFreePlay)-and(InBounds $after.latestFreePlay 1)-and(Same $thin.source.camera $after.latestFreePlay.camera)) 'Actual opposite move from blocked last-safe point reacts immediately and drops with both exact groups preserved'
|
|
$depth=Clone $fixtures.restores[0].freeSource;$depth.sourceStageId=1;$depth.camera=[pscustomobject][ordered]@{target=[pscustomobject][ordered]@{x=0;y=2.35;z=0};rotation=[pscustomobject][ordered]@{x=0;y=0;z=0;w=1};distance=14}
|
|
$depth.balls=@([pscustomobject][ordered]@{id='native-depth-back';position=[pscustomobject][ordered]@{x=1;y=3;z=4}},[pscustomobject][ordered]@{id='native-depth-front';position=[pscustomobject][ordered]@{x=-1;y=3;z=0}})
|
|
$depth.groups=@([pscustomobject][ordered]@{id='native-depth-back-group';ballIds=@('native-depth-back')},[pscustomobject][ordered]@{id='native-depth-front-group';ballIds=@('native-depth-front')})
|
|
$short=$null
|
|
foreach($hold in @(150,2500)){
|
|
$state=FixtureState $depth 'free' 1;$before=LoadFixture "stationary-$hold" $state 'free' 1;$point=BallPoint $before.latestFreePlay 'native-depth-front'
|
|
$after=HoldDrag "stationary-$hold" $before.latestFreePlay 'native-depth-front' ($point[0]+80) $point[1] $hold
|
|
Check "stationary-$hold-real-drag" (!(SameBalls $before.latestFreePlay $after.latestFreePlay)-and(SameMembership $depth $after.latestFreePlay)-and(InBounds $after.latestFreePlay 1)-and(Same $depth.camera $after.latestFreePlay.camera)) 'Actual initial pointer movement changes friend, hold/camera does not save automatic pose'
|
|
if($hold-eq150){$short=$after}else{Check 'stationary-short-long-identical-3d-drop' (SameBalls $short.latestFreePlay $after.latestFreePlay .002) 'Same initial input followed by short/long stationary hold: actual whole-ID 3D drop equal'}
|
|
}
|
|
$before=LoadFixture 'pause-cancel' (FixtureState $depth 'free' 1) 'free' 1;$point=BallPoint $before.latestFreePlay 'native-depth-front'
|
|
$after=HoldDrag 'pause-cancel' $before.latestFreePlay 'native-depth-front' ($point[0]+80) $point[1] 1200 -Cancel
|
|
Check 'pause-cancel-restores-board-manual-camera' (Same $before $after) 'Native HOME interrupts actual assisted drag, board and manual camera restored'
|
|
(@($depth.balls|Where-Object {$_.id-ceq'native-depth-front'})[0]).position=[pscustomobject][ordered]@{x=-1.5;y=2;z=-1};(@($depth.balls|Where-Object {$_.id-ceq'native-depth-back'})[0]).position=[pscustomobject][ordered]@{x=.5;y=2;z=2.2}
|
|
ApproachDepth 'actual-deep-contact' $depth
|
|
$required=@('six-scenes-ground-prop-feet-arch-hole','front-prop-occlusion-all-original-ids-search','stable-candidate-display-live-assist-and-visible-manual-return','six-scenes-variants-all-five-boundaries-free-source','phone-tablet-readable-ui-numbers-gaps','matched-baseline-native-presentation-memory-observation')
|
|
if(Test-Path -LiteralPath $observationPath){
|
|
$observed=Get-Content -LiteralPath $observationPath -Raw|ConvertFrom-Json
|
|
Check 'actual-observation-provenance' ($observed.sourceCommit-eq$result.sourceCommit-and$observed.apkSha256-eq$result.apkSha256-and$observed.serial-eq$Serial-and$observed.status-eq'passed'-and$observed.startedUtc-and$observed.finishedUtc) 'Directly observed live camera/scene/UI and matched baseline measurements from this APK'
|
|
foreach($id in $required){$entry=@($observed.checks|Where-Object id -eq $id);if($entry.Count-ne1-or!$entry[0].passed-or!$entry[0].actual-or!$entry[0].command-or$entry[0].exitCode-ne0-or$entry[0].artifacts.Count-eq0){$pending.Add($id);continue}
|
|
foreach($artifact in $entry[0].artifacts){$path=Join-Path $projectRoot $artifact.path;Check "observed-$id-evidence-sha" ((Get-FileHash -LiteralPath $path).Hash-eq$artifact.sha256) $artifact.path}
|
|
Check "observed-$id" $true $entry[0].actual
|
|
}
|
|
}else{foreach($id in $required){$pending.Add($id)}}
|
|
Check 'r08-all-required-native-observations-evaluated' ($pending.Count-eq0) ($pending-join', ')
|
|
$result.sourceCommitEnd=(& git rev-parse HEAD);$result.sourceDirtyEnd=[bool](& git status --porcelain)
|
|
Check 'unchanged-source-end' (!$result.sourceDirtyEnd-and$result.sourceCommitEnd-eq$result.sourceCommit) $result.sourceCommitEnd
|
|
$result.status='passed'
|
|
}catch{$result.error=$_.Exception.Message;Write-Host $result.error -ForegroundColor Red}
|
|
finally {
|
|
if($original){try{Adb @('shell','am','force-stop',$package)|Out-Null;Adb @('push',$originalPath,$remote)|Out-Null;Adb @('push',$originalPath,($remote+'.bak'))|Out-Null;Check 'actual-fresh30-original-restored' (Same $original (Save 'original-progress-restored')) 'All original exact records restored after separate fixtures'}catch{$result.status='failed';$result.restoreError=$_.Exception.Message}}
|
|
$result.pending=$pending.ToArray();$result.skipped=@();$result.checks=$checks.ToArray();$result.commands=$commands.ToArray();$result.finishedUtc=[DateTime]::UtcNow.ToString('o')
|
|
$result|ConvertTo-Json -Depth 20|Set-Content -LiteralPath (Join-Path $EvidenceDir 'android-playground-evidence.json') -Encoding utf8
|
|
}
|
|
if($result.status-ne'passed'){exit 1};exit 0
|