2026-06-24 13:48:22 +09:00
#!/usr/bin/env python3
""" Lightweight regression guard for the buildless Tarinai app.
Checks static - file consistency , generated - load order , syntax , and the ActionSpec
2026-07-10 16:40:06 +09:00
registry contract . This covers static contracts and focused runtime smoke tests . The current codec intentionally supports only the current binary schema .
2026-06-24 13:48:22 +09:00
"""
from __future__ import annotations
from pathlib import Path
import json
import re
import subprocess
import sys
import tempfile
ROOT = Path ( __file__ ) . resolve ( ) . parents [ 1 ]
MANIFEST = ROOT / " app_manifest.json "
def fail ( message : str ) - > None :
print ( f " [FAIL] { message } " , file = sys . stderr )
raise SystemExit ( 1 )
def ok ( message : str ) - > None :
print ( f " [OK] { message } " )
def read_text ( path : Path ) - > str :
return path . read_text ( encoding = " utf-8 " )
def manifest ( ) - > dict :
try :
data = json . loads ( read_text ( MANIFEST ) )
except Exception as exc : # pragma: no cover - diagnostic path
fail ( f " cannot read app_manifest.json: { exc } " )
for key in ( " version " , " css " , " js " ) :
if key not in data :
fail ( f " manifest missing { key } " )
return data
def check_files_exist ( data : dict ) - > None :
missing = [ p for p in [ * data [ " css " ] , * data [ " js " ] ] if not ( ROOT / p ) . exists ( ) ]
if missing :
fail ( " manifest references missing files: " + " , " . join ( missing ) )
ok ( " manifest file references exist " )
def check_versions ( data : dict ) - > None :
version = str ( data [ " version " ] )
version_js = read_text ( ROOT / " js/version.js " )
service_worker = read_text ( ROOT / " service-worker.js " )
if f ' APP_VERSION = " { version } " ' not in version_js :
fail ( " js/version.js APP_VERSION does not match manifest " )
if f ' APP_VERSION = " { version } " ' not in service_worker :
fail ( " service-worker.js APP_VERSION does not match manifest " )
ok ( f " version synchronized: { version } " )
def check_index_order ( data : dict ) - > None :
index = read_text ( ROOT / " index.html " )
css = re . findall ( r ' <link rel= " stylesheet " href= " ([^ " ]+) " \ s*/> ' , index )
scripts = re . findall ( r ' <script src= " ([^ " ]+) " defer></script> ' , index )
css_clean = [ entry . split ( " ? " , 1 ) [ 0 ] for entry in css if entry . startswith ( " css/ " ) ]
script_clean = [ entry . split ( " ? " , 1 ) [ 0 ] for entry in scripts if entry . startswith ( " js/ " ) ]
if css_clean [ : len ( data [ " css " ] ) ] != data [ " css " ] :
fail ( " index.html CSS order differs from manifest " )
if script_clean [ : len ( data [ " js " ] ) ] != data [ " js " ] :
fail ( " index.html JS order differs from manifest " )
ok ( " index.html load order matches manifest " )
def check_service_worker_order ( data : dict ) - > None :
sw = read_text ( ROOT / " service-worker.js " )
match = re . search ( r " const coreScriptNames = \ [ \ s*(.*?) \ s* \ ]; " , sw , re . S )
if not match :
fail ( " service-worker.js coreScriptNames not found " )
names = re . findall ( r ' " ([^ " ]+) " ' , match . group ( 1 ) )
expected = [ Path ( p ) . stem for p in data [ " js " ] ]
if names != expected :
fail ( " service-worker.js coreScriptNames differs from manifest JS order " )
ok ( " service-worker.js core script order matches manifest " )
def node_check ( paths : list [ Path ] ) - > None :
node = " node "
for path in paths :
result = subprocess . run ( [ node , " --check " , str ( path ) ] , cwd = ROOT , text = True , capture_output = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( f " node --check failed: { path . relative_to ( ROOT ) } " )
ok ( f " node --check passed for { len ( paths ) } files " )
def action_spec_smoke ( ) - > None :
source = read_text ( ROOT / " js/tarinai_action_spec.js " )
js = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
vm . createContext ( context ) ;
vm . runInContext ( { source ! r } , context , { { filename : ' tarinai_action_spec.js ' } } ) ;
2026-06-29 16:21:58 +09:00
const action = context . registerTarinaiActionSpecs ( [ { {
2026-06-24 13:48:22 +09:00
id : ' smoke_action ' ,
need : ' fulfill ' ,
label : ' smoke ' ,
selectTarget ( ) { { return { { id : ' target-1 ' } } ; } } ,
canStart ( t , world , ctx ) { { const target = ctx . target | | this . selectTarget ( t , world , ctx ) ; return target & & target . id == = ' target-1 ' ; } } ,
start ( t , world , ctx ) { { t . startedWith = ctx . target . id ; return true ; } } ,
tick ( ) { { return ' finished ' ; } } ,
finish ( t ) { { t . finished = true ; return true ; } } ,
2026-06-29 16:21:58 +09:00
} } ] ) [ 0 ] ;
2026-06-24 13:48:22 +09:00
const subject = { { } } ;
if ( ! context . canStartTarinaiAction ( ' smoke_action ' , subject , { { } } ) ) throw new Error ( ' canStart failed ' ) ;
if ( ! context . startTarinaiAction ( action , subject , { { } } ) ) throw new Error ( ' start failed ' ) ;
if ( subject . startedWith != = ' target-1 ' ) throw new Error ( ' selectTarget/start context failed ' ) ;
if ( context . tickTarinaiAction ( ' smoke_action ' , subject , { { } } , 0.1 , { { } } ) != = ' finished ' ) throw new Error ( ' tick failed ' ) ;
if ( ! context . finishTarinaiAction ( ' smoke_action ' , subject , { { } } , { { phase : ' finished ' } } ) ) throw new Error ( ' finish failed ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " ActionSpec smoke test failed " )
ok ( " ActionSpec lifecycle smoke test passed " )
def behavior_state_smoke ( ) - > None :
source = read_text ( ROOT / " js/tarinai_behavior_state.js " )
js = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . Tarinai = class Tarinai { { } } ;
vm . createContext ( context ) ;
vm . runInContext ( { source ! r } , context , { { filename : ' tarinai_behavior_state.js ' } } ) ;
const t = new context . Tarinai ( ) ;
t . world = { { time : 12 } } ;
t . state = ' seek_food ' ;
2026-06-25 14:51:58 +09:00
context . setTarinaiBehavior ( t , { { actionId : ' eat_food ' , need : ' food ' , label : ' eat ' , reason : ' hungry ' , text : ' hungry ' , tiedNeeds : [ ' food ' , ' sleep ' ] , source : ' forced ' , forcedId : ' f1 ' , forcedRequest : { { uid : ' f1 ' , id : ' eat_food ' , source : ' test ' , priority : 99 , reasonText : ' forced hungry ' , status : ' active ' } } } } ) ;
2026-06-24 13:48:22 +09:00
if ( context . getTarinaiBehaviorId ( t ) != = ' eat_food ' ) throw new Error ( ' behavior id mismatch ' ) ;
if ( context . getTarinaiBehaviorNeed ( t ) != = ' food ' ) throw new Error ( ' behavior need mismatch ' ) ;
2026-06-25 14:51:58 +09:00
if ( ! context . isTarinaiBehaviorForced ( t ) ) throw new Error ( ' forced state not visible ' ) ;
const behavior = context . currentTarinaiBehavior ( t ) ;
if ( ! behavior | | behavior . actionId != = ' eat_food ' | | behavior . reason != = ' hungry ' | | behavior . source != = ' forced ' ) throw new Error ( ' canonical behavior fields not normalized ' ) ;
if ( ' intent ' in t | | ' currentAction ' in t | | ' activeBehavior ' in t | | ' activeForcedBehavior ' in t ) throw new Error ( ' legacy behavior accessors still present ' ) ;
2026-06-24 13:48:22 +09:00
const forced = context . getTarinaiForcedBehavior ( t ) ;
if ( ! forced | | forced . uid != = ' f1 ' | | forced . priority != = 99 ) throw new Error ( ' forced request not normalized ' ) ;
context . setTarinaiForcedBehavior ( t , null ) ;
2026-06-25 14:51:58 +09:00
if ( context . currentTarinaiBehavior ( t ) . source == = ' forced ' | | context . getTarinaiForcedBehavior ( t ) ) throw new Error ( ' forced source not cleared ' ) ;
2026-06-24 13:48:22 +09:00
context . clearTarinaiBehavior ( t ) ;
if ( context . currentTarinaiBehavior ( t ) != = null ) throw new Error ( ' behavior not cleared ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " Behavior state smoke test failed " )
ok ( " behavior state smoke test passed " )
2026-07-09 16:08:11 +09:00
def need_action_runtime_bridge_smoke ( ) - > None :
items_source = read_text ( ROOT / " js/tarinai_needs_items.js " )
core_source = read_text ( ROOT / " js/tarinai_needs_core.js " )
forced_source = read_text ( ROOT / " js/tarinai_forced_behavior.js " )
js = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . Tarinai = class Tarinai { { } } ;
vm . createContext ( context ) ;
vm . runInContext ( { items_source ! r } , context , { { filename : ' tarinai_needs_items.js ' } } ) ;
if ( ! context . TarinaiNeedsRuntime ) throw new Error ( ' TarinaiNeedsRuntime missing ' ) ;
if ( typeof context . TarinaiNeedsRuntime . startNeedAction != = ' function ' ) throw new Error ( ' startNeedAction bridge missing ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " need-action runtime bridge smoke test failed " )
qualified_call = " globalThis.TarinaiNeedsRuntime?.startNeedAction?.( "
if core_source . count ( qualified_call ) != 2 :
fail ( " tarinai_needs_core.js must route both emergency starts through TarinaiNeedsRuntime " )
if forced_source . count ( qualified_call ) != 1 :
fail ( " tarinai_forced_behavior.js must route forced starts through TarinaiNeedsRuntime " )
if re . search ( r " (?<![. \ w])startNeedAction \ s* \ ( " , core_source + " \n " + forced_source ) :
fail ( " cross-file bare startNeedAction call remains " )
ok ( " need-action runtime bridge is exported and all cross-file calls are qualified " )
2026-06-24 13:48:22 +09:00
def check_legacy_behavior_refs ( ) - > None :
tokens = re . compile ( r " \ b(activeBehavior|currentAction|activeForcedBehavior|intent) \ b " )
offenders = [ ]
for path in ( ROOT / " js " ) . glob ( " *.js " ) :
for i , line in enumerate ( read_text ( path ) . splitlines ( ) , start = 1 ) :
2026-06-25 14:51:58 +09:00
stripped = line . strip ( )
if stripped . startswith ( " // " ) :
continue
if tokens . search ( line ) :
offenders . append ( f " { path . relative_to ( ROOT ) . as_posix ( ) } : { i } " )
2026-06-24 13:48:22 +09:00
if offenders :
2026-06-25 14:51:58 +09:00
fail ( " legacy behavior fields still present: " + " , " . join ( offenders [ : 20 ] ) )
ok ( " legacy behavior runtime fields removed " )
2026-06-24 13:48:22 +09:00
2026-06-27 19:22:38 +09:00
def check_legacy_physics_props_removed ( ) - > None :
terms = [
" rotatorThickness " , " rotatorSegments " , " rotatorPowered " , " rotatorSpeed " , " rotatorAngularVelocity " ,
" poisonCollisionEnabled " , " poisonDamage " , " poisonMass " , " poisonInertia " ,
" reciprocatorPowered " , " reciprocatorSpeed " , " reciprocatorTravel " , " reciprocatorPhase " ,
" reciprocatorDirection " , " reciprocatorAxisAngle " , " reciprocatorVelocity " , " reciprocatorAnchorX " , " reciprocatorAnchorY " ,
" linkA " , " linkB " , " linkLength " , " linkMidX " , " linkMidY " , " linkMidVX " , " linkMidVY " ,
" ropeParticles " , " _physicsAwakeUntil " , " _linkAwakeUntil " ,
]
offenders = [ ]
for path in ( ROOT / " js " ) . glob ( " *.js " ) :
text = read_text ( path )
for term in terms :
if term in text :
offenders . append ( f " { path . relative_to ( ROOT ) . as_posix ( ) } : { term } " )
if offenders :
fail ( " legacy physics property names still present: " + " , " . join ( offenders [ : 20 ] ) )
ok ( " legacy physics property names removed " )
2026-07-10 16:40:06 +09:00
def check_redundant_correction_paths_removed ( ) - > None :
terms = [
" markBodyChanged " , " purgeLegacyPhysicsStorage " , " nextTarinaiCollisionCheckAt " ,
" _physicsWorldFrame " , " bodyReads " , " bodyWrites " , " constraintReads " , " constraintWrites " ,
" compactEffects " ,
]
offenders = [ ]
for path in ( ROOT / " js " ) . glob ( " *.js " ) :
text = read_text ( path )
for term in terms :
if term in text :
offenders . append ( f " { path . relative_to ( ROOT ) . as_posix ( ) } : { term } " )
if offenders :
fail ( " redundant correction path returned: " + " , " . join ( offenders [ : 20 ] ) )
ball_source = read_text ( ROOT / " js/item_dynamic_ball_system.js " )
if ball_source . count ( " tryBallPickup( " ) != 1 :
fail ( " ball update must perform exactly one pickup check " )
ok ( " redundant correction paths remain removed " )
2026-07-11 21:13:39 +09:00
def direct_feeding_smoke ( ) - > None :
source = read_text ( ROOT / " js/tarinai_direct_feeding_system.js " )
placement = read_text ( ROOT / " js/world_placement_log.js " )
ui = read_text ( ROOT / " js/ui_input_shared.js " )
render = read_text ( ROOT / " js/render.js " )
world_source = read_text ( ROOT / " js/world.js " )
js = f """
const vm = require ( ' vm ' ) ;
class TestEffect { {
constructor ( type , x , y , options = { { } } ) { { Object . assign ( this , { { type , x , y } } , options ) ; } }
} }
const context = { {
console , globalThis : null , window : null ,
rand ( a , b ) { { return ( Number ( a ) + Number ( b ) ) * 0.5 ; } } ,
distXY ( ax , ay , bx , by ) { { return Math . hypot ( ax - bx , ay - by ) ; } } ,
isServingFoodType ( type ) { { return ! [ ' grass ' , ' water ' ] . includes ( type ) ; } } ,
toolLabel ( type ) { { return ( { { sweet : ' \u305a \u3093 \u3060 \u9905 ' , protein : ' \u30d7 \u30ed \u30c6 \u30a4 \u30f3 ' } } ) [ type ] | | type ; } } ,
TarinaiEffect : TestEffect ,
TarinaiItemRegistry : { { food : { { nutrition ( type ) { { return type == = ' sweet ' ? 16.5 : 15 ; } } } } } } ,
uiCache : { { selectedSnapshot : ' stale ' } } ,
} } ;
context . globalThis = context ; context . window = context ;
context . consumeBehaviorTarget = ( t , world , item , role ) = > { {
item . consumedRole = role ;
item . foodServingsRemaining = Math . max ( 0 , Number ( item . foodServingsRemaining | | 1 ) - 1 ) ;
return true ;
} } ;
context . TarinaiStructureLifecycle = { {
deleteItem ( world , item ) { { item . dead = true ; world . items = world . items . filter ( entry = > entry != = item ) ; return true ; } }
} } ;
vm . createContext ( context ) ;
vm . runInContext ( { source ! r } , context , { { filename : ' tarinai_direct_feeding_system.js ' } } ) ;
const api = context . TarinaiDirectFeedingSystem ;
if ( ! api | | ! api . isDirectFeedType ( ' sweet ' ) | | ! api . isDirectFeedType ( ' protein ' ) | | api . isDirectFeedType ( ' ball ' ) ) throw new Error ( ' direct-feed eligibility mismatch ' ) ;
const world = { {
tarinai : [ ] , items : [ ] , effects : [ ] , logs : [ ] , time : 12 , hoverGiveTarget : null ,
isTarinaiHiddenInNestBox ( ) { { return false ; } } ,
log ( text , kind , meta ) { { this . logs . push ( { { text , kind , meta } } ) ; } } ,
recordFamily ( t ) { { this . recorded = t ; } } ,
} } ;
function makeTarinai ( name , x , y ) { {
return { {
name , x , y , radius : 22 , world , affection : 0 , currentPersonality : { { openness : 0 , sociability : 0 } } ,
contains ( px , py ) { { return Math . hypot ( px - this . x , py - this . y ) < = 24 ; } } ,
adjustPersonality ( key , delta , reason ) { {
this . currentPersonality [ key ] + = delta ;
this . personalityReasons = this . personalityReasons | | [ ] ;
this . personalityReasons . push ( reason ) ;
return delta ;
} } ,
bubble ( text ) { { this . lastBubble = text ; } } ,
} } ;
} }
const first = makeTarinai ( ' first ' , 20 , 20 ) ;
const top = makeTarinai ( ' top ' , 20 , 20 ) ;
world . tarinai = [ first , top ] ;
if ( api . findTargetAt ( world , 20 , 20 ) != = top ) throw new Error ( ' top-most direct-feed target was not selected ' ) ;
const sweet = { { type : ' sweet ' , amount : 5 , foodServingsRemaining : 5 , x : 20 , y : 20 } } ;
world . items . push ( sweet ) ;
const result = api . give ( world , top , sweet , { { source : ' pinch ' } } ) ;
if ( ! result ? . foodGift | | ! result . removed | | ! sweet . dead | | world . items . includes ( sweet ) ) throw new Error ( ' pinched food was not consumed and removed ' ) ;
const hearts = world . effects . filter ( effect = > effect . type == = ' heart ' ) ;
if ( hearts . length < 10 | | hearts . length > 14 ) throw new Error ( ` food gift heart count out of range : $ { { hearts . length } } ` ) ;
if ( ! ( top . currentPersonality . openness > 0.018 ) | | ! ( top . currentPersonality . sociability > = 0.010 ) ) throw new Error ( ' food gift personality effect is too weak ' ) ;
if ( top . currentPersonality . openness < = 0.006 ) throw new Error ( ' food gift does not exceed one second of cursor openness effect ' ) ;
const expectedPersonalityReason = ' \u30ab \u30fc \u30bd \u30eb \u304b \u3089 \u76f4 \u63a5 \u98df \u3079 \u7269 \u3092 \u98df \u3079 \u305f ' ;
if ( ! top . personalityReasons ? . length | | top . personalityReasons . some ( reason = > reason != = expectedPersonalityReason ) ) throw new Error ( ' direct-feed personality reason mismatch ' ) ;
if ( top . lastBubble != = ' \u2661 ' | | context . uiCache . selectedSnapshot != = ' ' ) throw new Error ( ' food gift feedback/cache refresh missing ' ) ;
if ( ! world . logs . some ( entry = > entry . meta ? . eventType == = ' direct_feed ' ) ) throw new Error ( ' direct-feed observation log missing ' ) ;
const priorEffects = world . effects . length ;
const medicine = { { type : ' protein ' , amount : 5 , foodServingsRemaining : 5 , x : 20 , y : 20 } } ;
world . items . push ( medicine ) ;
const medicineResult = api . give ( world , top , medicine , { { source : ' pinch ' } } ) ;
if ( ! medicineResult | | medicineResult . foodGift | | medicine . consumedRole != = ' medicine ' ) throw new Error ( ' medicine direct-feed path mismatch ' ) ;
if ( world . effects . length != = priorEffects ) throw new Error ( ' medicine incorrectly emitted food hearts ' ) ;
if ( ! ( top . focusPulseTimer > = 0.72 ) ) throw new Error ( ' medicine feedback pulse missing ' ) ;
const placementItem = { { type : ' sweet ' , amount : 5 , foodServingsRemaining : 5 , x : 20 , y : 20 } } ;
const placementResult = api . give ( world , top , placementItem , { { source : ' placement ' } } ) ;
if ( ! placementResult | | placementResult . removed | | placementItem . dead ) throw new Error ( ' ephemeral placement item should be consumed without lifecycle deletion ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " direct feeding runtime smoke test failed " )
placement_start = placement . find ( " placeItem(item, dropped = true) " )
direct_call = placement . find ( " directFeeding.give?. " , placement_start )
object_limit = placement . find ( " this.canAddObjects?.(1) " , placement_start )
blocked = placement . find ( " this.placementBlocked(item) " , placement_start )
if min ( placement_start , direct_call , object_limit , blocked ) < 0 or not ( direct_call < object_limit < blocked ) :
fail ( " direct placement feeding must run before object-limit and overlap rejection " )
click_section = placement . find ( " const itemType = toolItemType(tool) " )
click_direct = placement . find ( " const directTarget = " , click_section )
duplicator_set = placement . find ( " this.directSetDuplicatorAt?. " , click_section )
2026-07-18 13:06:02 +09:00
if min ( click_section , click_direct , duplicator_set ) < 0 or duplicator_set > click_direct :
fail ( " Duplicator loading must take precedence over Tarinai direct feeding at the same point " )
2026-07-11 21:13:39 +09:00
required_ui = [ " directFeedTargetAt " , ' source: " pinch " ' , " setGiveHover " , " hoverGiveTarget " ]
if any ( token not in ui for token in required_ui ) :
fail ( " pinch direct-feeding input bridge is incomplete " )
if ' label: " \\ u4e0e \\ u3048 \\ u308b " ' not in render or " world.hoverGiveTarget " not in render :
fail ( " direct-feeding hover overlay is missing " )
if world_source . count ( " this.hoverGiveTarget = null; " ) < 2 :
fail ( " direct-feeding hover state is not initialized and reset " )
ok ( " direct food/medicine feeding, placement interception, personality, hearts, and overlay verified " )
def check_collision_reason_and_projection_dirty_removed ( ) - > None :
collision_paths = [
ROOT / " js/world_environment.js " ,
ROOT / " js/tarinai_update_step_movement.js " ,
ROOT / " js/tarinai_social_move_life.js " ,
]
collision_source = " \n " . join ( read_text ( path ) for path in collision_paths )
if re . search ( r " resolveSolidObstacleCollision \ s* \ ([^)]* \ { [^}]* \ breason \ s*: " , collision_source , re . S ) :
fail ( " unused resolveSolidObstacleCollision reason option returned " )
environment = read_text ( ROOT / " js/world_environment.js " )
if " opts.reason " in environment :
fail ( " resolveSolidObstacleCollision still reads unused reason option " )
constraint = read_text ( ROOT / " js/constraint_system.js " )
match = re . search ( r " function finishLinkSpatialUpdate \ ([^)]* \ ) \ s* \ { (.*?) \ n \ } " , constraint , re . S )
if not match :
fail ( " finishLinkSpatialUpdate helper not found " )
body = match . group ( 1 )
if " Math.hypot " in body or " before " in body or body . count ( " markConstraintSpatialDirty " ) != 1 :
fail ( " constraint Projection dirty determination is duplicated " )
ok ( " unused collision reason and duplicate constraint Projection dirty determination removed " )
2026-06-25 14:51:58 +09:00
def check_basic_action_spec_runtime ( ) - > None :
source = " \n " . join ( read_text ( ROOT / rel ) for rel in [ " js/tarinai_action_definitions.js " , " js/tarinai_needs_items.js " ] )
2026-06-24 13:48:22 +09:00
required = {
" eat_food " : " createConsumableActionSpec " ,
" drink_water " : " createConsumableActionSpec " ,
" use_medicine " : " createConsumableActionSpec " ,
" sleep_in_bed " : " createSleepInBedActionSpec " ,
" sleep_build_grass_bed " : " createSleepBuildGrassBedActionSpec " ,
" sleep_anywhere " : " createSleepAnywhereActionSpec " ,
" rest_to_recover " : " createRestActionSpec " ,
" sunbath " : " createSunbathActionSpec " ,
" wander_lightly " : " createWanderActionSpec " ,
}
for action_id , factory in required . items ( ) :
if factory not in source :
fail ( f " basic ActionSpec factory missing for { action_id } : { factory } " )
forbidden_late_ticks = [
' set( " eat_food " , ' ,
' set( " drink_water " , ' ,
' set( " use_medicine " , ' ,
' set( " sunbath " , ' ,
]
offenders = [ token for token in forbidden_late_ticks if token in source ]
if offenders :
fail ( " basic actions still patched in configureTarinaiNeedActions: " + " , " . join ( offenders ) )
2026-06-25 14:51:58 +09:00
ok ( " basic need actions owned by ActionSpec factories " )
2026-06-24 13:48:22 +09:00
2026-06-25 14:51:58 +09:00
def check_social_action_spec_runtime ( ) - > None :
source = " \n " . join ( read_text ( ROOT / rel ) for rel in [ " js/tarinai_action_definitions.js " , " js/tarinai_needs_items.js " ] )
2026-06-24 13:48:22 +09:00
required_factories = [
" createPanicEscapeActionSpec " ,
" createApproachFriendActionSpec " ,
" createApproachFamilyActionSpec " ,
" createApproachMateActionSpec " ,
" createFightRivalActionSpec " ,
" createIntimidateEnemyActionSpec " ,
" createBirthRitualActionSpec " ,
]
for factory in required_factories :
if factory not in source :
fail ( f " social/combat ActionSpec factory missing: { factory } " )
definitions_match = re . search ( r " const TARINAI_ACTION_DEFINITIONS = \ [(.*?) \ ]; " , source , re . S )
if not definitions_match :
fail ( " TARINAI_ACTION_DEFINITIONS block missing " )
definitions = definitions_match . group ( 1 )
for factory in required_factories :
if f " { factory } () " not in definitions :
fail ( f " { factory } is not registered in TARINAI_ACTION_DEFINITIONS " )
if " function configureTarinaiNeedActions " in source :
2026-06-25 14:51:58 +09:00
fail ( " configureTarinaiNeedActions should not be needed after ActionSpec runtime consolidation " )
2026-06-24 13:48:22 +09:00
forbidden_late_ticks = [
' set( " panic_escape " , ' ,
' set( " approach_friend " , ' ,
' set( " approach_parent_or_child " , ' ,
' set( " approach_mate " , ' ,
' set( " fight_rival " , ' ,
' set( " intimidate_enemy " , ' ,
' set( " sunbath " , ' ,
" birthRitualAction " ,
" TARINAI_ACTIONS.push " ,
]
offenders = [ token for token in forbidden_late_ticks if token in source ]
if offenders :
fail ( " actions still patched late: " + " , " . join ( offenders ) )
2026-06-25 14:51:58 +09:00
ok ( " social/fear/combat/mate/birth/sunbath actions owned by ActionSpec factories " )
def check_action_spec_no_legacy_hooks ( ) - > None :
source = read_text ( ROOT / " js/tarinai_action_spec.js " )
forbidden = [ " condition " , " originalRun " , " originalUpdate " , " Legacy hooks " ]
offenders = [ token for token in forbidden if token in source ]
if offenders :
fail ( " ActionSpec legacy hook support still present: " + " , " . join ( offenders ) )
needs = " \n " . join ( read_text ( ROOT / rel ) for rel in [ " js/tarinai_action_definitions.js " , " js/tarinai_consumable_behavior.js " , " js/tarinai_social_action_runtime.js " , " js/tarinai_building_behavior.js " , " js/tarinai_needs_items.js " ] )
if " .condition " in needs or " action.condition " in needs :
fail ( " need action runtime still falls back to action.condition " )
ok ( " ActionSpec legacy hooks removed " )
2026-06-24 13:48:22 +09:00
def check_runtime_diagnostics_hooks ( ) - > None :
spatial_budget = read_text ( ROOT / " js/world_spatial_budget.js " )
debug_tools = read_text ( ROOT / " js/debug_tools.js " )
world_update = read_text ( ROOT / " js/world_update.js " )
2026-06-26 12:46:32 +09:00
simulation_systems = read_text ( ROOT / " js/simulation_systems.js " )
2026-06-24 13:48:22 +09:00
if " runtimeDiagnostics() " not in spatial_budget :
fail ( " World.runtimeDiagnostics hook missing " )
2026-06-29 16:21:58 +09:00
if " lastRuntimeDiagnostics " not in ( world_update + simulation_systems ) :
fail ( " world update systems do not publish lastRuntimeDiagnostics " )
2026-06-24 13:48:22 +09:00
if " spatial rebuild " not in debug_tools or " behavior forced " not in debug_tools :
fail ( " debug overlay is missing runtime diagnostic lines " )
2026-06-25 14:51:58 +09:00
spatial_budget = read_text ( ROOT / " js/world_spatial_budget.js " )
debug_tools = read_text ( ROOT / " js/debug_tools.js " )
if " performance: " in spatial_budget or " diag.performance " in debug_tools or " load-check removed " in debug_tools :
fail ( " removed performance/load-check diagnostics residue remains " )
2026-06-24 13:48:22 +09:00
ok ( " runtime diagnostics hooks present " )
def check_social_balance_guards ( ) - > None :
2026-06-25 14:51:58 +09:00
needs = " \n " . join ( read_text ( ROOT / rel ) for rel in [
" js/tarinai_needs_core.js " ,
" js/tarinai_behavior_text.js " ,
" js/tarinai_forced_behavior.js " ,
" js/tarinai_item_targeting.js " ,
" js/tarinai_action_definitions.js " ,
" js/tarinai_consumable_behavior.js " ,
" js/tarinai_social_action_runtime.js " ,
" js/tarinai_building_behavior.js " ,
" js/tarinai_needs_items.js " ,
] )
2026-06-24 13:48:22 +09:00
world_social = read_text ( ROOT / " js/world_family_social.js " )
social_move = read_text ( ROOT / " js/tarinai_social_move_life.js " )
required_need_tokens = [
2026-06-30 10:57:06 +09:00
" fight_rival: 72 " ,
" intimidate_enemy: 0 " ,
" approach_mate: 72 " ,
2026-06-24 13:48:22 +09:00
" approach_friend: 66 " ,
2026-06-30 13:40:36 +09:00
" return ( { conflict: 18, mate: 24, family: 12, bond: 14 " ,
" const relationDrive = Math.max(Number(parts.mate || 0) " ,
" drugBoosted || relationDrive >= Math.min " ,
" social-fight-vs-mate-weighted " ,
2026-06-24 13:48:22 +09:00
]
for token in required_need_tokens :
if token not in needs :
fail ( f " social balance guard token missing: { token } " )
if " canStart: basicCanStartWithTarget, \n start(t, world, ctx = {} ) { \n const rival " in needs :
fail ( " intimidate_enemy reverted to ungated basicCanStart " )
required_world_tokens = [
" fightPairCooldowns " ,
" fightPairKey(a, b) " ,
" fightPairCooldownRemaining(a, b) " ,
" markFightPairCooldown(a, b " ,
" isAlreadyFightingPair(a, b) " ,
" if (this.isAlreadyFightingPair?.(a, b)) return true " ,
]
for token in required_world_tokens :
if token not in world_social :
fail ( f " fight re-entry guard missing: { token } " )
2026-06-30 13:40:36 +09:00
if " social-weighted-fight-mate-start " not in social_move or " pickSocialFightMate " not in social_move :
fail ( " ordinary proximity weighted fight/mate guard missing " )
2026-06-24 13:48:22 +09:00
ok ( " social balance and fight re-entry guards present " )
def check_item_bucket_polish ( ) - > None :
spatial_budget = read_text ( ROOT / " js/world_spatial_budget.js " )
required_tokens = [
" markItemBucketsDirty(reason " ,
" ensureItemBuckets(reason " ,
" addItem(item " ,
" itemById(id) " ,
" itemsOfType(type) " ,
" this.itemIdMap = new Map() " ,
" bucketRebuildsTotal " ,
]
for token in required_tokens :
if token not in spatial_budget :
fail ( f " item bucket polish helper missing: { token } " )
if " return item && !item.dead ? item : null " not in spatial_budget :
fail ( " itemById must not return dead items " )
debug_tools = read_text ( ROOT / " js/debug_tools.js " )
if " bucket " not in debug_tools . lower ( ) and " id-map " not in debug_tools . lower ( ) :
fail ( " debug overlay is missing item bucket/id-map diagnostics " )
routed_files = {
" js/world_placement_log.js " : " addItem?.(item " ,
2026-06-26 12:46:32 +09:00
" js/simulation_ambient_system.js " : " addItem?.(drop " ,
2026-06-24 13:48:22 +09:00
" js/world_combat_effects.js " : " addItem?.(it " ,
" js/ants.js " : " addItem?.(corpse " ,
" js/tarinai_social_move_life.js " : " addItem?.(new Item( \" trace \" " ,
2026-06-25 14:51:58 +09:00
" js/tarinai_building_behavior.js " : " addItem?.(structure " ,
2026-06-24 13:48:22 +09:00
}
for rel , token in routed_files . items ( ) :
if token not in read_text ( ROOT / rel ) :
fail ( f " runtime item creation path is not routed through addItem: { rel } " )
ok ( " runtime item bucket/id-map polish present " )
2026-06-25 14:51:58 +09:00
def check_current_save_format ( ) - > None :
2026-06-24 13:48:22 +09:00
snapshot = read_text ( ROOT / " js/snapshot_system.js " )
save = read_text ( ROOT / " js/save_system.js " )
2026-06-25 14:51:58 +09:00
save_codec = read_text ( ROOT / " js/save_codec.js " )
save_storage = read_text ( ROOT / " js/save_storage.js " )
restore_coordinator = read_text ( ROOT / " js/restore_coordinator.js " )
2026-06-24 13:48:22 +09:00
required_snapshot = [
2026-06-26 19:04:32 +09:00
" const SNAPSHOT_VERSION = SaveSchema.SNAPSHOT_VERSION " ,
2026-06-25 17:43:24 +09:00
" a: \" tj1 \" " ,
2026-06-24 13:48:22 +09:00
" function compactWorld " ,
" function compactTarinai " ,
" function compactItem " ,
2026-06-25 17:43:24 +09:00
" worldRef.logs = [] " ,
2026-06-24 13:48:22 +09:00
" worldRef.family = {} " ,
" recordFamily " ,
]
for token in required_snapshot :
if token not in snapshot :
2026-06-25 17:43:24 +09:00
fail ( f " binary save snapshot token missing: { token } " )
2026-06-24 13:48:22 +09:00
forbidden_snapshot = [
2026-06-25 17:43:24 +09:00
" compactAnt " ,
2026-06-24 13:48:22 +09:00
" effects: (worldRef.effects " ,
2026-06-25 17:43:24 +09:00
" cameraX " ,
" cameraY " ,
" behavior: " ,
" deathReason " ,
2026-06-24 13:48:22 +09:00
" summary: " ,
" app: \" tarinai_colony_game \" " ,
]
offenders = [ token for token in forbidden_snapshot if token in snapshot ]
if offenders :
2026-06-25 17:43:24 +09:00
fail ( " broad snapshot fields still present after light-save conversion: " + " , " . join ( offenders ) )
2026-06-25 14:51:58 +09:00
required_codec = [
2026-06-30 22:30:37 +09:00
' EXPORT_PREFIX = " \\ u305f " ' ,
2026-07-10 16:40:06 +09:00
' SAVE_TEXT_BITS = 15 ' ,
2026-06-30 22:30:37 +09:00
" writeStringTable " ,
2026-07-10 16:40:06 +09:00
" saveTextCharFromValue " ,
" saveTextValueFromCode " ,
" base32768Encode " ,
" base32768Decode " ,
" makeSaveFrame " ,
" openSaveFrame " ,
" writeTarinaiBitPlane " ,
" readTarinaiBitPlane " ,
" TARINAI_COLUMN_LAYOUT " ,
" chooseCommonExtraPlan " ,
" standardItemAmount " ,
" chooseAmountPlan " ,
" worldSeedWords " ,
" saveTextCharFromValue " ,
" saveTextValueFromCode " ,
" itemFlagPlaneBytes " ,
" writeNormalItemExtra " ,
" readNormalItemExtra " ,
" writeBirthDescriptor " ,
" readBirthDescriptor " ,
2026-06-24 13:48:22 +09:00
" async function encodeSnapshot " ,
" async function decodeSnapshot " ,
]
2026-06-25 14:51:58 +09:00
for token in required_codec :
if token not in save_codec :
2026-06-25 17:43:24 +09:00
fail ( f " binary save codec token missing: { token } " )
2026-07-10 16:40:06 +09:00
if " SAVE_TEXT_ALPHABET " in save_codec or " SAVE_TEXT_REVERSE " in save_codec :
fail ( " 15-bit save text still allocates the legacy alphabet or reverse map " )
if " writer.u(Snapshot.version) " in save_codec :
fail ( " duplicate snapshot version remains in the binary header " )
if " writeSaveString(writer, w[8] " in save_codec :
fail ( " worldSeed is still stored as a string " )
for token in ( " formatHeader & 0x7f " , " formatHeader & 0x80 " , " payload.length * 2 + (useCompressed ? 1 : 0) " ) :
if token not in save_codec :
fail ( f " compact save framing token missing: { token } " )
if " tailMask " in save_codec :
fail ( " redundant Tarinai tailMask remains in current schema " )
if " costBits < best.costBits " not in save_codec or " itemFlagPlaneBytes " not in save_codec :
fail ( " cost-based common extra or continuous item flag plane is missing " )
2026-06-25 14:51:58 +09:00
required_storage = [
2026-06-25 17:43:24 +09:00
' STORAGE_PREFIX = " tarinai_japanese_hash_slot_v1_ " ' ,
2026-06-30 22:30:37 +09:00
' hash.startsWith( " \\ u305f " ) ' ,
2026-06-25 14:51:58 +09:00
" function writeSlot " ,
" function readSlot " ,
" function requireSlot " ,
" function deleteSlot " ,
]
for token in required_storage :
if token not in save_storage :
2026-06-25 17:43:24 +09:00
fail ( f " binary save storage token missing: { token } " )
2026-06-25 14:51:58 +09:00
required_facade = [
" TarinaiSaveSystem " ,
" TarinaiSaveCodec " ,
" TarinaiSaveStorage " ,
" TarinaiRestoreCoordinator " ,
]
combined = save + save_codec + save_storage + restore_coordinator
for token in required_facade :
if token not in combined :
fail ( f " save/restore split facade token missing: { token } " )
2026-06-25 17:43:24 +09:00
for token in ( " TN2! " , " TN3! " , " tarinai_save_slot_v2_ " , " tarinai_save_slot_v3_ " , " legacyForcedColumn " ) :
2026-06-25 14:51:58 +09:00
if token in combined or token in snapshot :
fail ( f " save backward compatibility residue still present: { token } " )
if " renderStats " in snapshot or " TarinaiFreezeSystem " in snapshot or " renderArchive " in snapshot :
fail ( " snapshot restore must not call UI or freeze subsystems directly " )
effects = read_text ( ROOT / " js/world_event_effects.js " )
2026-06-26 19:04:32 +09:00
if " tool:placed " not in effects or " function playLogSoundForEntry " not in effects :
fail ( " event effects do not own tool/log side effects " )
for rel in ( " js/freeze_system.js " , " js/freeze_store.js " , " js/freeze_snapshot_adapter.js " , " js/freeze_panel.js " ) :
if ( ROOT / rel ) . exists ( ) :
fail ( f " freezer subsystem file still exists after freezer removal: { rel } " )
2026-06-25 14:51:58 +09:00
placement = read_text ( ROOT / " js/world_placement_log.js " )
if " playLogSoundForEntry " in placement :
fail ( " log sound mapping still lives in world_placement_log.js " )
2026-06-28 23:07:40 +09:00
if " player_item_placement " in placement :
fail ( " player item placement should not create placement log entries " )
2026-06-25 17:43:24 +09:00
ok ( " binary hash save snapshot, codec, storage, and restore coordinator present " )
2026-06-25 14:51:58 +09:00
def check_unified_item_registry ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
2026-06-28 23:07:40 +09:00
required_files = [
" js/item_tool_definitions.js " ,
" js/item_visual_definitions.js " ,
" js/item_food_definitions.js " ,
" js/item_effect_definitions.js " ,
" js/item_registry.js " ,
]
missing = [ rel for rel in required_files if rel not in js_files or not ( ROOT / rel ) . exists ( ) ]
if missing :
fail ( " split item registry files missing: " + " , " . join ( missing ) )
order = [ js_files . index ( rel ) for rel in required_files ]
if order != sorted ( order ) :
fail ( " split item registry files are not loaded before the facade in manifest order " )
stale_files = [ rel for rel in [ " js/food_registry.js " , " js/effect_registry.js " , " js/item_visual_registry.js " ] if ( ROOT / rel ) . exists ( ) or rel in js_files ]
2026-06-25 14:51:58 +09:00
if stale_files :
2026-06-28 23:07:40 +09:00
fail ( " obsolete item registry split files remain: " + " , " . join ( stale_files ) )
tool_source = read_text ( ROOT / " js/item_tool_definitions.js " )
visual_source = read_text ( ROOT / " js/item_visual_definitions.js " )
food_source = read_text ( ROOT / " js/item_food_definitions.js " )
effect_source = read_text ( ROOT / " js/item_effect_definitions.js " )
facade_source = read_text ( ROOT / " js/item_registry.js " )
required_pairs = [
( tool_source , " const TOOL_DEFINITIONS " ) ,
( visual_source , " function itemVisualDefinition " ) ,
( food_source , " class FoodRegistry " ) ,
( food_source , " global.TarinaiFoodRegistry " ) ,
( effect_source , " class EffectRegistry " ) ,
( effect_source , " global.TarinaiEffectRegistry " ) ,
( facade_source , " global.TarinaiItemRegistry " ) ,
2026-06-29 16:21:58 +09:00
( facade_source , " food: global.TarinaiFoodRegistry " ) ,
( facade_source , " effect: global.TarinaiEffectRegistry " ) ,
2026-06-25 14:51:58 +09:00
]
2026-06-28 23:07:40 +09:00
for source , token in required_pairs :
2026-06-25 14:51:58 +09:00
if token not in source :
2026-06-28 23:07:40 +09:00
fail ( f " split item registry token missing: { token } " )
2026-06-25 14:51:58 +09:00
data_source = read_text ( ROOT / " js/data.js " )
forbidden_data = [ " const TOOL_DEFINITIONS " , " const ITEM_TRAITS " , " class FoodRegistry " , " class EffectRegistry " , " TARINAI_ITEM_VISUALS " ]
offenders = [ token for token in forbidden_data if token in data_source ]
if offenders :
fail ( " item definitions still mixed into data.js: " + " , " . join ( offenders ) )
2026-06-28 23:07:40 +09:00
ok ( " item registry responsibilities split behind facade " )
2026-06-25 14:51:58 +09:00
def check_docs_removed ( ) - > None :
docs = ROOT / " docs "
if docs . exists ( ) and any ( docs . iterdir ( ) ) :
fail ( " non-runtime docs directory still contains files " )
ok ( " non-runtime docs removed " )
def check_phase_and_input_split ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [
2026-06-26 12:46:32 +09:00
" js/simulation_runtime_helpers.js " ,
" js/simulation_environment_system.js " ,
" js/simulation_item_ant_system.js " ,
" js/simulation_effects_system.js " ,
" js/simulation_creature_system.js " ,
" js/simulation_maintenance_system.js " ,
" js/simulation_ambient_system.js " ,
" js/simulation_systems.js " ,
2026-06-25 14:51:58 +09:00
" js/ui_input_shared.js " ,
" js/ui_input_touch.js " ,
" js/ui_input_mouse.js " ,
]
for rel in required :
if rel not in js_files :
fail ( f " split runtime module missing from manifest: { rel } " )
world_update = read_text ( ROOT / " js/world_update.js " )
ui_bind = read_text ( ROOT / " js/ui_bind.js " )
touch = read_text ( ROOT / " js/ui_input_touch.js " )
mouse = read_text ( ROOT / " js/ui_input_mouse.js " )
shared = read_text ( ROOT / " js/ui_input_shared.js " )
2026-06-26 12:46:32 +09:00
systems = read_text ( ROOT / " js/simulation_systems.js " )
2026-06-29 16:21:58 +09:00
if " TarinaiSimulation " not in world_update :
fail ( " World.update is not delegated to TarinaiSimulation " )
2026-06-25 14:51:58 +09:00
for token in [ " updateClock " , " updateEnvironment " , " updateItemsAndAnts " , " updateCreatures " , " runMaintenance " , " spawnRainWater " ] :
2026-06-29 16:21:58 +09:00
if token not in systems :
2026-06-25 14:51:58 +09:00
fail ( f " world update phase missing: { token } " )
if " touchstart " in ui_bind or " mousedown " in ui_bind or " touchmove " in ui_bind :
fail ( " ui_bind.js still owns field pointer input handlers " )
for token in [ " TarinaiUIInputShared " , " createInputContext " ] :
if token not in shared :
fail ( f " shared input helper missing: { token } " )
if " touchstart " not in touch or " TarinaiTouchInput " not in touch :
fail ( " touch input module missing touch handlers " )
if " mousedown " not in mouse or " TarinaiMouseInput " not in mouse :
fail ( " mouse input module missing mouse handlers " )
if ui_bind . count ( " syncToolSizeBadges(); " ) != 1 :
fail ( " ui_bind.js should bind/sync tool size badges only once " )
2026-06-29 16:21:58 +09:00
phase_birth_direct = False
2026-06-25 14:51:58 +09:00
placement_birth_direct = " audio.birth " in read_text ( ROOT / " js/world_placement_log.js " )
family_birth_direct = " audio.birth " in read_text ( ROOT / " js/world_family_social.js " )
if phase_birth_direct or placement_birth_direct or family_birth_direct :
fail ( " birth log sound can double-fire; direct audio.birth remains in log-emitting birth flows " )
ok ( " world phase split and UI input split present " )
def check_processing_split ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [
" js/item_type_initializers.js " ,
" js/item_render_runtime.js " ,
" js/tarinai_item_targeting.js " ,
" js/tarinai_action_definitions.js " ,
" js/tarinai_consumable_behavior.js " ,
" js/tarinai_social_action_runtime.js " ,
" js/tarinai_building_behavior.js " ,
]
for rel in required :
if rel not in js_files :
fail ( f " processing split module missing from manifest: { rel } " )
2026-06-29 16:21:58 +09:00
if not ( js_files . index ( " js/items.js " ) < js_files . index ( " js/item_type_initializers.js " ) < js_files . index ( " js/item_render_runtime.js " ) < js_files . index ( " js/ants.js " ) ) :
fail ( " item init/render split load order is invalid " )
2026-06-25 14:51:58 +09:00
if not ( js_files . index ( " js/tarinai_forced_behavior.js " ) < js_files . index ( " js/tarinai_item_targeting.js " ) < js_files . index ( " js/tarinai_consumable_behavior.js " ) < js_files . index ( " js/tarinai_social_action_runtime.js " ) < js_files . index ( " js/tarinai_building_behavior.js " ) < js_files . index ( " js/tarinai_action_definitions.js " ) < js_files . index ( " js/tarinai_needs_items.js " ) ) :
fail ( " tarinai behavior split load order is invalid " )
items = read_text ( ROOT / " js/items.js " )
2026-07-11 21:13:39 +09:00
temperature_system = read_text ( ROOT / " js/world_temperature_system.js " )
2026-06-25 14:51:58 +09:00
init = read_text ( ROOT / " js/item_type_initializers.js " )
item_render = read_text ( ROOT / " js/item_render_runtime.js " )
2026-06-29 16:21:58 +09:00
if " class Item " not in items or " function initializeItemTypeState " not in init or " Object.assign(Item.prototype " not in item_render :
fail ( " item construction/init/render split is incomplete " )
2026-06-25 14:51:58 +09:00
if " updateDuplicator " in items or " \n draw(ctx " in items or " if (type === \" grass \" ) " in items :
fail ( " items.js still owns type init, runtime, or render methods " )
defs = read_text ( ROOT / " js/tarinai_action_definitions.js " )
consumable = read_text ( ROOT / " js/tarinai_consumable_behavior.js " )
social = read_text ( ROOT / " js/tarinai_social_action_runtime.js " )
building = read_text ( ROOT / " js/tarinai_building_behavior.js " )
needs = read_text ( ROOT / " js/tarinai_needs_items.js " )
required_pairs = [
( defs , " const TARINAI_ACTION_DEFINITIONS " ) ,
( defs , " const TARINAI_ACTIONS " ) ,
( consumable , " function updateConsumableBehavior " ) ,
( social , " function updateMateBehavior " ) ,
( social , " function updatePanicBehavior " ) ,
( building , " function continueBuildPlan " ) ,
]
for source , token in required_pairs :
if token not in source :
fail ( f " processing split token missing: { token } " )
for token in [ " createConsumableActionSpec " , " function updateConsumableBehavior " , " function continueBuildPlan " ] :
if token in needs :
fail ( f " tarinai_needs_items.js still owns extracted processing: { token } " )
ok ( " item and Tarinai processing split present " )
2026-06-24 13:48:22 +09:00
2026-06-26 12:46:32 +09:00
def command_dispatcher_smoke ( ) - > None :
source = read_text ( ROOT / " js/command_dispatcher.js " )
js = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . World = class World { { } } ;
context . scalableToolIds = ( ) = > [ ' grass ' , ' sweet ' ] ;
context . normalizedToolSizeFor = ( world , tool ) = > ( world . toolSizes & & world . toolSizes [ tool ] ) | | world . toolSize | | ' medium ' ;
vm . createContext ( context ) ;
vm . runInContext ( { source ! r } , context , { { filename : ' command_dispatcher.js ' } } ) ;
const events = [ ] ;
const world = new context . World ( ) ;
world . tool = ' observe ' ;
world . toolSize = ' medium ' ;
world . toolSizes = { { } } ;
world . speed = 1 ;
world . paused = false ;
world . w = 1000 ;
world . h = 700 ;
world . viewportW = 500 ;
world . viewportH = 300 ;
world . clampCamera = ( ) = > { { world . clamped = true ; } } ;
world . emit = ( type , payload ) = > events . push ( [ type , payload ] ) ;
world . handleClick = ( x , y ) = > { { world . clicked = [ x , y ] ; } } ;
const target = { { x : 420 , y : 260 , dead : false } } ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' tool.select ' , toolId : ' grass ' } } ) . ok | | world . tool != = ' grass ' ) throw new Error ( ' tool select failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' tool.size.cycle ' , toolId : ' grass ' , order : [ ' small ' , ' medium ' , ' large ' ] } } ) . ok | | world . toolSize != = ' large ' ) throw new Error ( ' tool size failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' selection.focus ' , target } } ) . ok | | world . selected != = target | | world . cameraX != = 170 | | world . cameraY != = 110 ) throw new Error ( ' focus failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' selection.favorite.toggle ' } } ) . ok | | target . favorite != = true ) throw new Error ( ' favorite failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' simulation.paused.toggle ' } } ) . ok | | world . paused != = true ) throw new Error ( ' pause failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' simulation.speed.cycle ' , speeds : [ 1 , 2 , 4 ] } } ) . ok | | world . speed != = 2 ) throw new Error ( ' speed failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' pointer.primary ' , x : 7 , y : 9 } } ) . ok | | world . clicked [ 0 ] != = 7 | | world . clicked [ 1 ] != = 9 ) throw new Error ( ' pointer failed ' ) ;
2026-06-29 16:21:58 +09:00
if ( typeof world [ ' dispatch ' + ' Command ' ] != = ' undefined ' ) throw new Error ( ' World facade should not be installed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' selection.clear ' } } ) . ok | | world . selected != = null ) throw new Error ( ' selection clear failed ' ) ;
2026-06-26 12:46:32 +09:00
if ( ! events . some ( ( [ type ] ) = > type == = ' selection:changed ' ) ) throw new Error ( ' selection event missing ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " command dispatcher smoke test failed " )
ok ( " command dispatcher smoke test passed " )
def check_system_order_boundary ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [
" js/simulation_runtime_helpers.js " ,
" js/simulation_environment_system.js " ,
" js/simulation_item_ant_system.js " ,
" js/simulation_effects_system.js " ,
" js/simulation_creature_system.js " ,
" js/simulation_maintenance_system.js " ,
" js/simulation_ambient_system.js " ,
" js/simulation_systems.js " ,
" js/system_order.js " ,
" js/simulation.js " ,
" js/world_update.js " ,
]
for rel in required :
if rel not in js_files :
fail ( f " simulation boundary module missing from manifest: { rel } " )
split_order = [
" js/simulation_runtime_helpers.js " ,
" js/simulation_environment_system.js " ,
" js/simulation_item_ant_system.js " ,
" js/simulation_effects_system.js " ,
" js/simulation_creature_system.js " ,
" js/simulation_maintenance_system.js " ,
" js/simulation_ambient_system.js " ,
" js/simulation_systems.js " ,
" js/system_order.js " ,
" js/simulation.js " ,
" js/world_update.js " ,
]
if [ rel for rel in js_files if rel in split_order ] != split_order :
fail ( " simulation boundary load order is invalid " )
system_order = read_text ( ROOT / " js/system_order.js " )
simulation = read_text ( ROOT / " js/simulation.js " )
systems_source = read_text ( ROOT / " js/simulation_systems.js " )
if " TarinaiSimulationSystems " not in systems_source or " const phases = Object.freeze " not in systems_source :
fail ( " concrete simulation systems facade missing exported phase registry " )
concrete_modules = {
" js/simulation_runtime_helpers.js " : " TarinaiSimulationRuntime " ,
" js/simulation_environment_system.js " : " TarinaiEnvironmentSimulationSystem " ,
" js/simulation_item_ant_system.js " : " TarinaiItemAntSimulationSystem " ,
" js/simulation_effects_system.js " : " TarinaiEffectsSimulationSystem " ,
" js/simulation_creature_system.js " : " TarinaiCreatureSimulationSystem " ,
" js/simulation_maintenance_system.js " : " TarinaiMaintenanceSimulationSystem " ,
" js/simulation_ambient_system.js " : " TarinaiAmbientSimulationSystem " ,
}
for rel , token in concrete_modules . items ( ) :
if token not in read_text ( ROOT / rel ) :
fail ( f " concrete simulation split module missing export: { rel } " )
for token in [ " const SYSTEM_ORDER " , " clock " , " environment " , " items_and_ants " , " creatures " , " maintenance " , " diagnostics " ] :
if token not in system_order :
fail ( f " system order token missing: { token } " )
2026-06-29 16:21:58 +09:00
if " TarinaiSystemOrder " not in simulation or " Object.freeze( { update }) " not in simulation :
fail ( " simulation.js does not expose the update boundary " )
2026-06-26 12:46:32 +09:00
ok ( " named simulation system order present " )
def simulation_boundary_smoke ( ) - > None :
sources = { rel : read_text ( ROOT / rel ) for rel in [
" js/simulation_runtime_helpers.js " ,
" js/simulation_environment_system.js " ,
" js/simulation_item_ant_system.js " ,
" js/simulation_effects_system.js " ,
" js/simulation_creature_system.js " ,
" js/simulation_maintenance_system.js " ,
" js/simulation_ambient_system.js " ,
" js/simulation_systems.js " ,
" js/system_order.js " ,
" js/simulation.js " ,
] }
js = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
vm . createContext ( context ) ;
for ( const [ filename , source ] of Object . entries ( { sources ! r } ) ) { {
vm . runInContext ( source , context , { { filename } } ) ;
} }
if ( ! context . TarinaiSimulationSystems ? . phases ? . updateClock ) throw new Error ( ' simulation systems missing ' ) ;
2026-06-29 16:21:58 +09:00
if ( ! context . TarinaiSystemOrder ? . update ) throw new Error ( ' system order missing ' ) ;
if ( ! context . TarinaiSimulation ? . update ) throw new Error ( ' simulation facade missing ' ) ;
2026-06-26 12:46:32 +09:00
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " simulation boundary smoke test failed " )
ok ( " simulation boundary smoke test passed " )
def check_update_policy_boundaries ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [ " js/item_update_policy.js " , " js/tarinai_update_policy.js " ]
for rel in required :
if rel not in js_files :
fail ( f " update policy boundary module missing from manifest: { rel } " )
2026-06-29 16:21:58 +09:00
if not ( js_files . index ( " js/item_lifecycle_support.js " ) < js_files . index ( " js/item_update_policy.js " ) < js_files . index ( " js/item_runtime.js " ) ) :
2026-06-26 12:46:32 +09:00
fail ( " item update policy load order is invalid " )
if not ( js_files . index ( " js/simulation_runtime_helpers.js " ) < js_files . index ( " js/tarinai_update_policy.js " ) < js_files . index ( " js/simulation_creature_system.js " ) ) :
fail ( " Tarinai update policy load order is invalid " )
item_policy = read_text ( ROOT / " js/item_update_policy.js " )
tarinai_policy = read_text ( ROOT / " js/tarinai_update_policy.js " )
scheduler = read_text ( ROOT / " js/item_update_scheduler.js " )
sim_helpers = read_text ( ROOT / " js/simulation_runtime_helpers.js " )
creature_system = read_text ( ROOT / " js/simulation_creature_system.js " )
required_tokens = [
( item_policy , " global.TarinaiItemUpdatePolicy " ) ,
( item_policy , " itemUpdateInterval " ) ,
( item_policy , " forScheduledItems " ) ,
( tarinai_policy , " global.TarinaiCreatureUpdatePolicy " ) ,
2026-06-29 16:21:58 +09:00
( tarinai_policy , " updateCadence " ) ,
2026-06-26 12:46:32 +09:00
( scheduler , " TarinaiItemUpdatePolicy " ) ,
( sim_helpers , " TarinaiItemUpdatePolicy " ) ,
( creature_system , " TarinaiCreatureUpdatePolicy " ) ,
]
for source , token in required_tokens :
if token not in source :
fail ( f " update policy boundary token missing: { token } " )
duplicated_item_policy_sources = [ scheduler , sim_helpers ]
for source in duplicated_item_policy_sources :
if " const REALTIME_ITEM_TYPES " in source or " const PIN_ITEM_TYPES " in source or " SCHEDULED_ITEM_TYPES = " in source :
fail ( " item update policy constants are duplicated outside item_update_policy.js " )
if " function tarinaiUpdateInterval " in creature_system :
fail ( " Tarinai update cadence still lives inside simulation_creature_system.js " )
js = f """
const vm = require ( ' vm ' ) ;
const fs = require ( ' fs ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . isServingFoodType = ( type ) = > type == = ' food ' ;
context . TarinaiItemRuntime = { {
updateOne ( item , dt ) { { item . runs = ( item . runs | | 0 ) + 1 ; item . lastDt = dt ; return true ; } }
} } ;
2026-06-29 16:21:58 +09:00
context . TarinaiPerf = { { begin ( ) { { return ( ) = > undefined ; } } , renderQualityTier ( ) { { return " normal " ; } } } } ;
context . TarinaiPhysicsWorldSystem = { { updateWorld ( ) { { return { { mechanical : 0 , constraintRan : 0 , postConstraintPairs : 0 } } ; } } } } ;
context . TarinaiInputMode = { { snapshot ( ) { { return { { touchFirst : false } } ; } } } } ;
2026-06-26 12:46:32 +09:00
vm . createContext ( context ) ;
for ( const file of [ ' js/item_update_policy.js ' , ' js/item_update_scheduler.js ' ] ) { {
vm . runInContext ( fs . readFileSync ( file , ' utf8 ' ) , context , { { filename : file } } ) ;
} }
const world = { { time : 0 , items : [ { { type : ' ball ' } } , { { type : ' food ' } } , { { type : ' grass ' } } ] , ensureItemBuckets ( ) { { } } , itemBucketRebuildsTotal : 0 } } ;
if ( context . TarinaiItemUpdateScheduler . run ( world , 0.1 ) < 1 ) throw new Error ( ' realtime item did not run ' ) ;
world . time = 5.1 ;
if ( context . TarinaiItemUpdateScheduler . run ( world , 0.1 ) < 1 ) throw new Error ( ' scheduled item did not run ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " update policy scheduler smoke test failed " )
ok ( " update policy boundaries present " )
def check_entity_runtime_boundary ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [ " js/item_runtime.js " , " js/tarinai_runtime.js " ]
for rel in required :
if rel not in js_files :
fail ( f " entity runtime boundary module missing from manifest: { rel } " )
2026-06-29 16:21:58 +09:00
if not ( js_files . index ( " js/item_lifecycle_pipeline.js " ) < js_files . index ( " js/item_runtime.js " ) < js_files . index ( " js/structure_lifecycle.js " ) ) :
2026-06-26 12:46:32 +09:00
fail ( " item runtime boundary load order is invalid " )
if not ( js_files . index ( " js/tarinai_needs_items.js " ) < js_files . index ( " js/tarinai_social_move_life.js " ) < js_files . index ( " js/tarinai_runtime.js " ) < js_files . index ( " js/tarinai_render.js " ) ) :
fail ( " Tarinai runtime boundary load order is invalid " )
item_runtime = read_text ( ROOT / " js/item_runtime.js " )
tarinai_runtime = read_text ( ROOT / " js/tarinai_runtime.js " )
item_scheduler = read_text ( ROOT / " js/item_update_scheduler.js " )
sim_helpers = read_text ( ROOT / " js/simulation_runtime_helpers.js " )
creature_system = read_text ( ROOT / " js/simulation_creature_system.js " )
required_tokens = [
( item_runtime , " global.TarinaiItemRuntime " ) ,
( item_runtime , " installPrototypeBoundary " ) ,
( item_runtime , " updateScheduled " ) ,
( tarinai_runtime , " global.TarinaiCreatureRuntime " ) ,
2026-06-29 16:21:58 +09:00
( item_scheduler , " TarinaiItemRuntime.updateOne " ) ,
( sim_helpers , " TarinaiItemRuntime.updateScheduled " ) ,
( creature_system , " TarinaiCreatureRuntime.updateOne " ) ,
2026-06-26 12:46:32 +09:00
]
for source , token in required_tokens :
if token not in source :
fail ( f " entity runtime boundary token missing: { token } " )
ok ( " entity update runtime boundary present " )
def check_tarinai_update_pipeline_boundary ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [
" js/tarinai_sunbath_system.js " ,
" js/tarinai_cursor_care_system.js " ,
" js/tarinai_local_environment_system.js " ,
" js/tarinai_need_planner_system.js " ,
" js/tarinai_item_interaction_context.js " ,
" js/tarinai_food_interaction_system.js " ,
" js/tarinai_contact_item_system.js " ,
" js/tarinai_item_interaction_system.js " ,
" js/tarinai_update_step_frame.js " ,
" js/tarinai_update_step_ai.js " ,
" js/tarinai_update_step_environment.js " ,
" js/tarinai_update_step_movement.js " ,
" js/tarinai_update_step_health.js " ,
2026-07-01 14:41:05 +09:00
" js/update_step_pipeline_runner.js " ,
2026-06-26 12:46:32 +09:00
" js/tarinai_update_pipeline.js " ,
]
for rel in required :
if rel not in js_files :
fail ( f " Tarinai update pipeline module missing from manifest: { rel } " )
expected_order = [
" js/tarinai_needs_items.js " ,
" js/tarinai_need_planner_system.js " ,
" js/tarinai_item_interaction_context.js " ,
" js/tarinai_food_interaction_system.js " ,
" js/tarinai_contact_item_system.js " ,
" js/tarinai_item_interaction_system.js " ,
" js/tarinai_sunbath_system.js " ,
" js/tarinai_cursor_care_system.js " ,
" js/tarinai_local_environment_system.js " ,
" js/tarinai_social_move_life.js " ,
" js/tarinai_update_step_frame.js " ,
" js/tarinai_update_step_ai.js " ,
" js/tarinai_update_step_environment.js " ,
" js/tarinai_update_step_movement.js " ,
" js/tarinai_update_step_health.js " ,
" js/tarinai_update_pipeline.js " ,
" js/tarinai_runtime.js " ,
]
if [ rel for rel in js_files if rel in expected_order ] != expected_order :
fail ( " Tarinai update pipeline load order is invalid " )
exports = {
" js/tarinai_sunbath_system.js " : " global.TarinaiSunbathSystem " ,
" js/tarinai_cursor_care_system.js " : " global.TarinaiCursorCareSystem " ,
" js/tarinai_local_environment_system.js " : " global.TarinaiLocalEnvironmentSystem " ,
" js/tarinai_need_planner_system.js " : " global.TarinaiNeedPlannerSystem " ,
" js/tarinai_item_interaction_context.js " : " global.TarinaiItemInteractionContext " ,
" js/tarinai_food_interaction_system.js " : " global.TarinaiFoodInteractionSystem " ,
" js/tarinai_contact_item_system.js " : " global.TarinaiContactItemSystem " ,
" js/tarinai_item_interaction_system.js " : " global.TarinaiItemInteractionSystem " ,
" js/tarinai_update_step_frame.js " : " global.TarinaiFrameUpdateStep " ,
" js/tarinai_update_step_ai.js " : " global.TarinaiAiUpdateStep " ,
" js/tarinai_update_step_environment.js " : " global.TarinaiEnvironmentUpdateStep " ,
" js/tarinai_update_step_movement.js " : " global.TarinaiMovementUpdateStep " ,
" js/tarinai_update_step_health.js " : " global.TarinaiHealthUpdateStep " ,
2026-07-01 14:41:05 +09:00
" js/update_step_pipeline_runner.js " : " global.TarinaiUpdateStepPipelineRunner " ,
2026-06-26 12:46:32 +09:00
" js/tarinai_update_pipeline.js " : " global.TarinaiUpdatePipeline " ,
}
for rel , token in exports . items ( ) :
if token not in read_text ( ROOT / rel ) :
fail ( f " Tarinai update pipeline export missing: { rel } " )
runtime = read_text ( ROOT / " js/tarinai_runtime.js " )
2026-06-26 19:04:32 +09:00
if " TarinaiUpdatePipeline?.updateOne " not in runtime and " TarinaiUpdatePipeline.updateOne " not in runtime :
fail ( " Tarinai runtime does not route through TarinaiUpdatePipeline " )
2026-06-26 12:46:32 +09:00
needs_items = read_text ( ROOT / " js/tarinai_needs_items.js " )
sunbath_system = read_text ( ROOT / " js/tarinai_sunbath_system.js " )
cursor_system = read_text ( ROOT / " js/tarinai_cursor_care_system.js " )
local_env_system = read_text ( ROOT / " js/tarinai_local_environment_system.js " )
interaction_system = read_text ( ROOT / " js/tarinai_item_interaction_system.js " )
2026-06-29 16:21:58 +09:00
if " update(dt) " in needs_items or " const minute = dt / 60 " in needs_items :
fail ( " Tarinai.prototype.update residue remains in needs mixin " )
if " TarinaiItemInteractionSystem.updateOne " not in needs_items or " const foodNeed = Number(this.needRaw?.food " in needs_items :
2026-06-26 12:46:32 +09:00
fail ( " Tarinai.prototype.interactWithItems is not a thin facade over the interaction system " )
2026-07-01 14:41:05 +09:00
# These subsystems are now routed by the split update steps rather than the prototype needs mixin.
2026-06-26 12:46:32 +09:00
interaction_context = read_text ( ROOT / " js/tarinai_item_interaction_context.js " )
food_interaction = read_text ( ROOT / " js/tarinai_food_interaction_system.js " )
contact_interaction = read_text ( ROOT / " js/tarinai_contact_item_system.js " )
for source , token in [
( sunbath_system , " this.sunbathFrameTimer " ) ,
( cursor_system , " this.nextCursorHeartAt " ) ,
( local_env_system , " grassComfort " ) ,
( interaction_context , " const foodNeed = Number(tarinai.needRaw?.food " ) ,
( food_interaction , " tarinai.applyParamItemEffect " ) ,
( contact_interaction , " tarinai.applyWaterEffect(dt, it.type) " ) ,
] :
if token not in source :
fail ( f " Tarinai update subsystem missing extracted token: { token } " )
2026-06-29 16:21:58 +09:00
if " TarinaiItemInteractionContext.create " not in interaction_system or " TarinaiFoodInteractionSystem.updateCandidate " not in interaction_system or " TarinaiContactItemSystem.updateCandidate " not in interaction_system :
2026-06-26 12:46:32 +09:00
fail ( " Tarinai item interaction orchestrator does not delegate to context/food/contact candidate subsystems " )
if " for (const item of ctx.foodCandidates " not in interaction_system :
fail ( " Tarinai item interaction orchestrator does not preserve per-candidate interaction order " )
2026-06-29 16:21:58 +09:00
if " TarinaiNeedPlannerSystem.updateOne " not in needs_items or " function resolveNeedsCore " not in needs_items :
2026-06-26 19:04:32 +09:00
fail ( " Tarinai resolveNeeds is not routed through planner system " )
if " TarinaiNeedPlannerCoreSystem " not in needs_items :
fail ( " Tarinai need planner core system export missing " )
2026-06-26 12:46:32 +09:00
if " TarinaiItemInteractionSystem.updateOne " not in read_text ( ROOT / " js/tarinai_update_step_ai.js " ) :
fail ( " Tarinai AI step does not delegate item interactions to subsystem " )
if " TarinaiCursorCareSystem.updateOne " not in read_text ( ROOT / " js/tarinai_update_step_frame.js " ) :
fail ( " Tarinai frame step does not delegate cursor care to subsystem " )
if " TarinaiLocalEnvironmentSystem.updateOne " not in read_text ( ROOT / " js/tarinai_update_step_environment.js " ) :
fail ( " Tarinai environment step does not delegate local environment to subsystem " )
if " TarinaiSunbathSystem.updateOne " not in read_text ( ROOT / " js/tarinai_update_step_movement.js " ) :
fail ( " Tarinai movement step does not delegate sunbath to subsystem " )
pipeline = read_text ( ROOT / " js/tarinai_update_pipeline.js " )
2026-06-26 19:04:32 +09:00
for token in [ " TarinaiFrameUpdateStep " , " TarinaiAiUpdateStep " , " TarinaiEnvironmentUpdateStep " , " TarinaiMovementUpdateStep " , " TarinaiHealthUpdateStep " ] :
2026-06-26 12:46:32 +09:00
if token not in pipeline :
fail ( f " Tarinai update pipeline missing step token: { token } " )
js = f """
const vm = require ( ' vm ' ) ;
const fs = require ( ' fs ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . CONFIG = { { hungerPerMinute : 60 , lonelinessPerMinute : 60 , energyPerMinute : 60 } } ;
context . clamp = ( v , a , b ) = > Math . max ( a , Math . min ( b , v ) ) ;
context . rand = ( a , b ) = > ( a + b ) / 2 ;
context . pick = ( arr ) = > arr [ 0 ] ;
context . stableUnit = ( ) = > 0.5 ;
2026-06-29 16:21:58 +09:00
context . deterministicRange = ( world , salt , min , max ) = > ( min + max ) / 2 ;
context . deterministicSigned = ( ) = > 0 ;
context . deterministicChance = ( ) = > false ;
2026-06-26 12:46:32 +09:00
context . applyNeedShock = ( ) = > undefined ;
context . applyNeedRelief = ( ) = > undefined ;
context . calculateStressFromNeeds = ( ) = > 0 ;
context . applyGroundStressModifier = ( t , v ) = > v ;
context . createDefaultNeeds = ( ) = > ( { { food : 0 , sleep : 0 , health : 0 , safety : 0 , social : 0 , fulfill : 0 } } ) ;
context . tarinaiMaxEnergy = ( ) = > 100 ;
context . tarinaiMetabolicHungerScale = ( ) = > 1 ;
vm . createContext ( context ) ;
for ( const file of { required ! r } ) { {
vm . runInContext ( fs . readFileSync ( file , ' utf8 ' ) , context , { { filename : file } } ) ;
} }
const calls = [ ] ;
const world = { {
time : 1 ,
groundType : ' soil ' ,
weather : ' cloudy ' ,
colonyMood : { { id : ' relaxed ' } } ,
selected : null ,
spendScheduledWork ( ) { { return true ; } } ,
temperatureAt ( ) { { return 0.62 ; } } ,
lightLevel ( ) { { return 0.5 ; } } ,
spawnBleedEffect ( ) { { calls . push ( ' bleed ' ) ; } } ,
resolveFenceCollision ( ) { { calls . push ( ' fence ' ) ; } } ,
} } ;
const t = { {
world , dead : false , x : 10 , y : 12 , vx : 0 , vy : 0 , radius : 20 ,
2026-06-29 16:21:58 +09:00
age : 0 , reproductionTimer : 10 , eatTimer : 0 , eatCooldown : 0 , foodReactTimer : 0 ,
2026-06-26 12:46:32 +09:00
surpriseTimer : 0 , fearTimer : 0 , intimidateTimer : 0 , intimidatedTimer : 0 , stress : 0 ,
hpBarTimer : 0 , stressBarTimer : 0 , loveMochiTimer : 0 , fightMochiTimer : 0 ,
birthRitualTimer : 0 , pokeFlashTimer : 0 , cursorPetting : 0 , pinchThoughtTimer : 0 ,
sunbathCooldown : 0 , fightTimer : 0 , fightCooldown : 0 , defeatedTimer : 0 , stretchTimer : 0 ,
grassEatTimer : 0 , hurtTimer : 0 , fallTimer : 0 , tempTimer : 0 , tempComfort : 0.62 ,
hunger : 10 , loneliness : 0 , energy : 100 , trait : { { hunger : 1 , social : 1 , sleep : 1 } } ,
state : ' idle ' , aiTimer : 0 , envTimer : 0 , needs : context . createDefaultNeeds ( ) , needRaw : context . createDefaultNeeds ( ) ,
updateZunchiDisease ( ) { { calls . push ( ' zunchiDisease ' ) ; } } ,
updateSpecialDiseases ( ) { { calls . push ( ' specialDisease ' ) ; } } ,
freezeSleepDiseaseMotion ( ) { { return false ; } } ,
personalityProfile ( ) { { return { { fear : 1 } } ; } } ,
updateFacing ( ) { { calls . push ( ' facing ' ) ; } } ,
maintainTargetProgress ( ) { { calls . push ( ' targetProgress ' ) ; } } ,
move ( ) { { calls . push ( ' move ' ) ; } } ,
updateNestBoxPresence ( ) { { calls . push ( ' nestBox ' ) ; } } ,
checkLife ( ) { { calls . push ( ' checkLife ' ) ; } } ,
} } ;
if ( ! context . TarinaiUpdatePipeline . updateOne ( t , 0.016 , { { legacyUpdate ( ) { { throw new Error ( ' legacy should not run ' ) ; } } } } ) ) throw new Error ( ' pipeline update failed ' ) ;
for ( const expected of [ ' zunchiDisease ' , ' specialDisease ' , ' facing ' , ' targetProgress ' , ' move ' , ' nestBox ' , ' fence ' , ' checkLife ' ] ) { {
if ( ! calls . includes ( expected ) ) throw new Error ( ' missing pipeline call: ' + expected ) ;
} }
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " Tarinai update pipeline smoke test failed " )
ok ( " Tarinai update pipeline boundary present " )
def check_item_lifecycle_pipeline_boundary ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
dynamic_required = [
" js/item_dynamic_tool_system.js " ,
" js/item_dynamic_ball_system.js " ,
" js/item_dynamic_duplicator_system.js " ,
" js/item_dynamic_pin_system.js " ,
" js/item_dynamic_zunchi_system.js " ,
" js/item_dynamic_system.js " ,
]
required = [
" js/item_lifecycle_support.js " ,
2026-06-29 16:21:58 +09:00
" js/physics_helpers.js " ,
" js/pin_attachment_system.js " ,
2026-06-26 12:46:32 +09:00
* dynamic_required ,
" js/item_lifecycle_decay_system.js " ,
" js/item_lifecycle_growth_system.js " ,
" js/item_lifecycle_step_frame.js " ,
" js/item_lifecycle_step_decay.js " ,
" js/item_lifecycle_step_dynamic.js " ,
" js/item_lifecycle_step_growth.js " ,
2026-07-01 14:41:05 +09:00
" js/update_step_pipeline_runner.js " ,
2026-06-26 12:46:32 +09:00
" js/item_lifecycle_pipeline.js " ,
]
for rel in required :
if rel not in js_files :
fail ( f " Item lifecycle pipeline module missing from manifest: { rel } " )
expected_order = [
2026-06-29 16:21:58 +09:00
" js/physics_helpers.js " ,
" js/pin_attachment_system.js " ,
2026-06-26 12:46:32 +09:00
" js/item_lifecycle_support.js " ,
" js/item_update_policy.js " ,
* dynamic_required ,
" js/item_lifecycle_decay_system.js " ,
" js/item_lifecycle_growth_system.js " ,
" js/item_lifecycle_step_frame.js " ,
" js/item_lifecycle_step_decay.js " ,
" js/item_lifecycle_step_dynamic.js " ,
" js/item_lifecycle_step_growth.js " ,
2026-07-01 14:41:05 +09:00
" js/update_step_pipeline_runner.js " ,
2026-06-26 12:46:32 +09:00
" js/item_lifecycle_pipeline.js " ,
" js/item_runtime.js " ,
]
if [ rel for rel in js_files if rel in expected_order ] != expected_order :
fail ( " Item lifecycle pipeline load order is invalid " )
exports = {
" js/item_lifecycle_support.js " : " global.TarinaiItemLifecycleRuntimeSupport " ,
" js/item_dynamic_tool_system.js " : " global.TarinaiItemDynamicToolSystem " ,
" js/item_dynamic_ball_system.js " : " global.TarinaiItemDynamicBallSystem " ,
" js/item_dynamic_duplicator_system.js " : " global.TarinaiItemDynamicDuplicatorSystem " ,
" js/item_dynamic_pin_system.js " : " global.TarinaiItemDynamicPinSystem " ,
" js/item_dynamic_zunchi_system.js " : " global.TarinaiItemDynamicZunchiSystem " ,
" js/item_dynamic_system.js " : " global.TarinaiItemDynamicSystem " ,
" js/item_lifecycle_decay_system.js " : " global.TarinaiItemDecaySystem " ,
" js/item_lifecycle_growth_system.js " : " global.TarinaiItemGrowthSystem " ,
" js/item_lifecycle_step_frame.js " : " global.TarinaiItemFrameLifecycleStep " ,
" js/item_lifecycle_step_decay.js " : " global.TarinaiItemDecayLifecycleStep " ,
" js/item_lifecycle_step_dynamic.js " : " global.TarinaiItemDynamicLifecycleStep " ,
" js/item_lifecycle_step_growth.js " : " global.TarinaiItemGrowthLifecycleStep " ,
2026-07-01 14:41:05 +09:00
" js/update_step_pipeline_runner.js " : " global.TarinaiUpdateStepPipelineRunner " ,
2026-06-26 12:46:32 +09:00
" js/item_lifecycle_pipeline.js " : " global.TarinaiItemLifecyclePipeline " ,
}
for rel , token in exports . items ( ) :
if token not in read_text ( ROOT / rel ) :
fail ( f " Item lifecycle pipeline export missing: { rel } " )
runtime = read_text ( ROOT / " js/item_runtime.js " )
2026-06-26 19:04:32 +09:00
if " TarinaiItemLifecyclePipeline?.updateOne " not in runtime and " TarinaiItemLifecyclePipeline.updateOne " not in runtime :
fail ( " item runtime does not route through TarinaiItemLifecyclePipeline " )
2026-06-26 12:46:32 +09:00
pipeline = read_text ( ROOT / " js/item_lifecycle_pipeline.js " )
2026-06-26 19:04:32 +09:00
for token in [ " TarinaiItemFrameLifecycleStep " , " TarinaiItemDecayLifecycleStep " , " TarinaiItemDynamicLifecycleStep " , " TarinaiItemGrowthLifecycleStep " ] :
2026-06-26 12:46:32 +09:00
if token not in pipeline :
fail ( f " Item lifecycle pipeline missing step token: { token } " )
dynamic_step = read_text ( ROOT / " js/item_lifecycle_step_dynamic.js " )
dynamic_system = read_text ( ROOT / " js/item_dynamic_system.js " )
2026-06-29 16:21:58 +09:00
if " TarinaiItemDynamicSystem.update " not in dynamic_step :
2026-06-26 12:46:32 +09:00
fail ( " Item dynamic lifecycle step does not delegate to TarinaiItemDynamicSystem " )
for token in [ " TarinaiItemDynamicToolSystem " , " TarinaiItemDynamicBallSystem " , " TarinaiItemDynamicDuplicatorSystem " , " TarinaiItemDynamicPinSystem " , " TarinaiItemDynamicZunchiSystem " ] :
if token not in dynamic_system :
fail ( f " Item dynamic system missing subsystem token: { token } " )
for rel in [ " js/item_dynamic_tool_system.js " , " js/item_dynamic_ball_system.js " , " js/item_dynamic_duplicator_system.js " , " js/item_dynamic_pin_system.js " , " js/item_dynamic_zunchi_system.js " ] :
body = read_text ( ROOT / rel )
2026-06-26 19:04:32 +09:00
for token in [ " item.updateFan " , " item.updateRotator " , " item.updateBall " , " item.updateDuplicator " , " item.updatePushpin " , " item.updateZunchiMotion " , " item.updateZunchi " ] :
2026-06-26 12:46:32 +09:00
if token in body :
fail ( f " dynamic subsystem still delegates to prototype method { token } in { rel } " )
lifecycle_support = read_text ( ROOT / " js/item_lifecycle_support.js " )
lifecycle_decay = read_text ( ROOT / " js/item_lifecycle_step_decay.js " )
lifecycle_growth = read_text ( ROOT / " js/item_lifecycle_step_growth.js " )
2026-06-29 16:21:58 +09:00
if " TarinaiItemLifecycleRuntimeSupport " not in lifecycle_support :
2026-06-26 12:46:32 +09:00
fail ( " item lifecycle support export missing " )
2026-06-29 16:21:58 +09:00
if " TarinaiDuplicatorRuntime " in lifecycle_support :
fail ( " duplicate duplicator runtime export still present " )
if " TarinaiItemDecaySystem.update " not in lifecycle_decay or " TarinaiItemGrowthSystem.update " not in lifecycle_growth :
2026-06-26 12:46:32 +09:00
fail ( " item lifecycle decay/growth steps are not thin system adapters " )
js = f """
const vm = require ( ' vm ' ) ;
const fs = require ( ' fs ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . isServingFoodType = ( type ) = > type == = ' food ' ;
context . passiveFoodDecayInterval = ( ) = > 0.5 ;
context . passiveFoodDecayRate = ( ) = > 0.1 ;
2026-07-18 13:06:02 +09:00
context . updateServingFoodVisualSize = ( item ) = > { { item . r = ( item . r | | 12 ) - 0.1 ; return true ; } } ;
2026-06-29 16:21:58 +09:00
context . TarinaiItemRegistry = { { food : { { } } } } ;
2026-06-26 12:46:32 +09:00
context . isPinType = ( type ) = > type == = ' pushpin ' ;
context . normalizeGrassStage = ( item ) = > { { item . normalized = true ; } } ;
context . stableUnit = ( ) = > 0.5 ;
vm . createContext ( context ) ;
for ( const file of { required ! r } ) { {
vm . runInContext ( fs . readFileSync ( file , ' utf8 ' ) , context , { { filename : file } } ) ;
} }
const events = [ ] ;
const world = { {
itemDropImpact ( item ) { { events . push ( [ ' drop ' , item . type ] ) ; } } ,
markTerrainDirty ( reason ) { { events . push ( [ ' terrain ' , reason ] ) ; } } ,
2026-07-18 13:06:02 +09:00
markItemBucketsDirty ( reason ) { { events . push ( [ ' buckets ' , reason ] ) ; } } ,
markSpatialDirty ( reason ) { { events . push ( [ ' spatial ' , reason ] ) ; } } ,
2026-06-26 12:46:32 +09:00
emit ( type , payload ) { { events . push ( [ ' emit ' , type , payload . type ] ) ; } } ,
} } ;
const food = { { type : ' food ' , dead : false , age : 0 , dropTimer : 0.1 , dropImpactDone : false , foodServingsRemaining : 1 , amount : 1 , passiveFoodDecayTimer : 0 } } ;
if ( ! context . TarinaiItemLifecyclePipeline . updateOne ( food , 0.6 , world , { { legacyUpdate ( ) { { throw new Error ( ' legacy should not run ' ) ; } } } } ) ) throw new Error ( ' food update failed ' ) ;
2026-07-18 13:06:02 +09:00
if ( food . age != = 0.6 | | ! food . dropImpactDone | | ! ( food . foodServingsRemaining < 1 ) | | ! events . some ( ( [ kind , value ] ) = > kind == = ' buckets ' & & value == = ' food-passive-resize ' ) | | ! events . some ( ( [ kind , value ] ) = > kind == = ' spatial ' & & value == = ' food-passive-resize ' ) ) throw new Error ( ' frame/decay step did not run ' ) ;
2026-06-26 12:46:32 +09:00
context . CONFIG = { { worldPadding : 30 } } ;
context . clamp = ( v , min , max ) = > Math . max ( min , Math . min ( max , v ) ) ;
context . distXY = ( ax , ay , bx , by ) = > Math . hypot ( ax - bx , ay - by ) ;
const dynamic = { {
type : ' ball ' , dead : false , age : 0 , x : 80 , y : 80 , prevX : 80 , prevY : 80 ,
vx : 12 , vy : 0 , r : 18 , spin : 0 , spinVelocity : 0
} } ;
const ballWorld = { { . . . world , w : 500 , h : 500 , nearbyItems ( ) { { return [ ] ; } } } } ;
context . TarinaiItemLifecyclePipeline . updateOne ( dynamic , 0.25 , ballWorld , { { legacyUpdate ( ) { { throw new Error ( ' legacy should not run ' ) ; } } } } ) ;
if ( ! ( dynamic . x > 80 ) | | ! ( dynamic . spin > 0 ) ) throw new Error ( ' dynamic step did not run ' ) ;
const grass = { { type : ' grass ' , dead : false , age : 0 } } ;
context . TarinaiItemLifecyclePipeline . updateOne ( grass , 0.1 , world , { { legacyUpdate ( ) { { throw new Error ( ' legacy should not run ' ) ; } } } } ) ;
2026-06-29 16:21:58 +09:00
if ( grass . normalized ) throw new Error ( ' grass should not be normalized by per-item growth step ' ) ;
2026-06-26 12:46:32 +09:00
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " Item lifecycle pipeline smoke test failed " )
ok ( " Item lifecycle pipeline boundary present " )
2026-07-09 16:08:11 +09:00
def mechanical_collision_and_editor_smoke ( ) - > None :
footprints = read_text ( ROOT / " js/collision_footprint_system.js " )
editor_source = read_text ( ROOT / " js/physics_shape_editor_system.js " )
placement_source = read_text ( ROOT / " js/world_placement_log.js " )
for token in ( " rectCorners, " , " rectAxes, " , " projectPoints, " , " aabbOverlap, " , " rectOverlapInfo, " , " rectsOverlap, " ) :
if token not in footprints :
fail ( f " mechanical SAT primitive is not exported: { token } " )
if " const DEFAULT_GRID_SIZE = 20; " not in editor_source :
fail ( " mechanical shape editor grid must use coarse 20px snapping " )
if editor_source . count ( " snapPoint( " ) < 2 or " gridSize: DEFAULT_GRID_SIZE " not in editor_source :
fail ( " mechanical shape editor does not route pointer coordinates through grid snapping " )
if placement_source . count ( " 20px \\ u9593 \\ u9694 " ) < 2 :
fail ( " mechanical editor hints do not disclose 20px grid snapping " )
js = f """
const vm = require ( ' vm ' ) ;
const fs = require ( ' fs ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . TarinaiPerf = { { begin ( ) { { return null ; } } } } ;
context . TarinaiConstraintSystem = { { updateWorld ( ) { { return 0 ; } } } } ;
context . normalizedItemAngle = angle = > angle ;
2026-07-10 16:40:06 +09:00
context . isRotatableItemType = type = > [ ' rotator ' , ' poison_block ' , ' reciprocator ' ] . includes ( String ( type | | ' ' ) ) ;
context . defaultItemAngle = type = > String ( type | | ' ' ) == = ' reciprocator ' ? Math . PI / 2 : 0 ;
context . itemRadiusFor = ( _type , fallback = 12 ) = > fallback ;
context . toolItemType = tool = > tool ;
2026-07-09 16:08:11 +09:00
vm . createContext ( context ) ;
for ( const path of { repr ( [ str ( ROOT / rel ) for rel in [
' js/math.js ' , ' js/geometry_helpers.js ' , ' js/collision_footprint_system.js ' ,
2026-07-10 16:40:06 +09:00
' js/physics_world_system.js ' , ' js/mechanical_system.js ' , ' js/placement_preview_system.js ' , ' js/physics_shape_editor_system.js '
2026-07-09 16:08:11 +09:00
] ] ) } ) vm . runInContext ( fs . readFileSync ( path , ' utf8 ' ) , context , { { filename : path } } ) ;
const fp = context . TarinaiCollisionFootprints ;
for ( const key of [ ' rectCorners ' , ' rectAxes ' , ' projectPoints ' , ' aabbOverlap ' , ' rectOverlapInfo ' , ' rectsOverlap ' ] ) { {
if ( typeof fp [ key ] != = ' function ' ) throw new Error ( ` missing footprint primitive : $ { { key } } ` ) ;
} }
const editor = context . TarinaiPhysicsShapeEditorSystem ;
if ( editor . gridSize != = 20 ) throw new Error ( ' editor grid size mismatch ' ) ;
const snapped = editor . snapPoint ( { { x : 13 , y : 31 } } ) ;
if ( snapped . x != = 20 | | snapped . y != = 40 ) throw new Error ( ' pointer did not snap to grid ' ) ;
const cleaned = editor . cleanSegments ( [ [ 13 , 31 , 91 , 44 ] , [ - 52 , - 20 , 52 , - 20 ] ] , { { fallback : [ - 80 , 0 , 80 , 0 ] } } ) ;
if ( ! cleaned . length | | cleaned . some ( seg = > seg . some ( value = > Math . abs ( value % 20 ) > 1e-9 ) ) ) throw new Error ( ' segment endpoints are not grid aligned ' ) ;
if ( cleaned . some ( seg = > Math . hypot ( seg [ 2 ] - seg [ 0 ] , seg [ 3 ] - seg [ 1 ] ) < 20 ) ) throw new Error ( ' editor retained sub-grid segments ' ) ;
let serial = 0 ;
function makeItem ( type , x , y ) { {
return { { id : ` m $ { { + + serial } } ` , type , x , y , angle : 0 , r : 64 , amount : 999 , dead : false } } ;
} }
function activate ( item ) { {
const api = context . TarinaiPhysicsBodySystem ;
api . setSegments ( item , [ [ - 80 , 0 , 80 , 0 ] ] , ' test-shape ' ) ;
api . setScalar ( item , ' solid ' , true , ' test-solid ' ) ;
if ( item . type == = ' rotator ' ) { { api . setScalar ( item , ' motorOn ' , true , ' test ' ) ; api . setScalar ( item , ' motorSpeed ' , 1.4 , ' test ' ) ; } }
if ( item . type == = ' reciprocator ' ) { { api . setScalar ( item , ' railOn ' , true , ' test ' ) ; api . setScalar ( item , ' railAxis ' , 0 , ' test ' ) ; api . setScalar ( item , ' railMotorSpeed ' , 80 , ' test ' ) ; } }
if ( item . type == = ' poison_block ' ) { { api . setScalar ( item , ' xv ' , 24 , ' test ' ) ; api . setScalar ( item , ' awakeUntil ' , 10 , ' test ' ) ; } }
} }
2026-07-10 16:40:06 +09:00
const reverseRotator = makeItem ( ' rotator ' , 80 , 80 ) ;
reverseRotator . world = { { time : 0 } } ;
context . TarinaiPhysicsBodySystem . ensureBody ( reverseRotator , reverseRotator . world ) ;
context . TarinaiPhysicsBodySystem . setScalar ( reverseRotator , ' motorSpeed ' , - 1.25 , ' reverse-test ' ) ;
if ( Math . abs ( context . TarinaiPhysicsBodySystem . scalar ( reverseRotator , ' motorSpeed ' , 0 ) + 1.25 ) > 1e-9 ) throw new Error ( ' negative rotator motor speed was clamped ' ) ;
reverseRotator . angle = 1.0 ;
reverseRotator . world = { { time : 0 , drawListDirty : false , markSpatialDirty ( ) { { } } , nearbyItems ( ) { { return [ ] ; } } , nearbyObstacles ( ) { { return [ ] ; } } , items : [ ] , tarinai : [ ] } } ;
context . TarinaiMechanicalSystem . updateRotator ( reverseRotator , 0.1 , reverseRotator . world , { { skipInteractions : true } } ) ;
if ( ! ( reverseRotator . angle < 0.99 ) ) throw new Error ( ' negative rotator speed did not rotate backward ' ) ;
2026-07-09 16:08:11 +09:00
function pairResult ( typeA , typeB , disableB = false ) { {
const a = makeItem ( typeA , 120 , 120 ) ;
const b = makeItem ( typeB , 120 , 128 ) ;
const items = [ a , b ] ;
const world = { {
items , tarinai : [ ] , time : 1 , frameCount : 1 , w : 900 , h : 700 , drawListDirty : false ,
itemCounts : { { rotator : 0 , poison_block : 0 , reciprocator : 0 } } ,
ensureItemBuckets ( ) { { } } , markSpatialDirty ( ) { { } } , markItemBucketsDirty ( ) { { } } ,
nearbyObstacles ( ) { { return items ; } } , nearbyItems ( ) { { return items ; } } ,
itemsOfType ( type ) { { return items . filter ( item = > item . type == = type & & ! item . dead ) ; } } ,
} } ;
for ( const item of items ) { { item . world = world ; world . itemCounts [ item . type ] + = 1 ; context . TarinaiPhysicsBodySystem . ensureBody ( item , world ) ; activate ( item ) ; } }
if ( disableB ) context . TarinaiPhysicsBodySystem . setScalar ( b , ' solid ' , false , ' test-disabled ' ) ;
const solved = context . TarinaiMechanicalSystem . resolveMechanicalPairs ( world , 0.016 , { { maxPairs : 40 } } ) ;
return { { solved , a , b } } ;
} }
for ( const [ a , b ] of [ [ ' rotator ' , ' rotator ' ] , [ ' rotator ' , ' reciprocator ' ] , [ ' rotator ' , ' poison_block ' ] , [ ' reciprocator ' , ' reciprocator ' ] , [ ' reciprocator ' , ' poison_block ' ] , [ ' poison_block ' , ' poison_block ' ] ] ) { {
const result = pairResult ( a , b , false ) ;
if ( ! ( result . solved > 0 ) ) throw new Error ( ` $ { { a } } / $ { { b } } collision - on pair did not interact ` ) ;
const disabled = pairResult ( a , b , true ) ;
if ( disabled . solved != = 0 ) throw new Error ( ` $ { { a } } / $ { { b } } collision - off pair still interacted ` ) ;
} }
2026-07-10 16:40:06 +09:00
const orientationWorld = { { toolAngleFor ( ) { { return 0 ; } } } } ;
const previewReciprocator = makeItem ( ' reciprocator ' , 40 , 40 ) ;
context . TarinaiPlacementPreviewSystem . applyToolOrientation ( orientationWorld , previewReciprocator , 0 ) ;
if ( Math . abs ( previewReciprocator . physicsBody . pose . angle - Math . PI / 2 ) > 1e-9 ) throw new Error ( ' reciprocator placement body angle mismatch ' ) ;
if ( Math . abs ( previewReciprocator . physicsBody . rail . axisAngle ) > 1e-9 ) throw new Error ( ' reciprocator placement rail angle mismatch ' ) ;
const rail = makeItem ( ' reciprocator ' , 0 , 0 ) ;
context . TarinaiPlacementPreviewSystem . applyToolOrientation ( orientationWorld , rail , 0 ) ;
activate ( rail ) ;
const fence = { { id : ' fence ' , type : ' fence_v ' , x : 8 , y : 0 , amount : 999 , dead : false } } ;
const collisionWorld = { {
items : [ rail , fence ] , tarinai : [ ] , time : 1 , frameCount : 1 , w : 900 , h : 700 , drawListDirty : false , itemBucketRebuildsTotal : 0 ,
ensureItemBuckets ( ) { { } } , markSpatialDirty ( ) { { } } ,
nearbyObstacles ( ) { { return this . items ; } } , nearbyItems ( ) { { return this . items ; } } ,
itemsOfType ( type ) { { return this . items . filter ( item = > item . type == = type & & ! item . dead ) ; } } ,
isFenceType ( type ) { { return type == = ' fence_v ' ; } } ,
solidObstacleRects ( item ) { {
if ( item != = fence ) return [ ] ;
return [ { { left : 4 , right : 12 , top : - 100 , bottom : 100 , cx : 8 , cy : 0 , halfW : 4 , halfH : 100 , angle : 0 , cos : 1 , sin : 0 , oriented : true , type : ' fence_v ' , item } } ] ;
} } ,
} } ;
rail . world = collisionWorld ; fence . world = collisionWorld ;
context . TarinaiMechanicalSystem . updateWorld ( collisionWorld , 0.016 , { { maxSubsteps : 1 } } ) ;
if ( context . TarinaiPhysicsBodySystem . scalar ( rail , ' railDir ' , 1 ) != = - 1 ) throw new Error ( ' reciprocator did not reverse at a solid obstacle ' ) ;
const oneWayRail = makeItem ( ' reciprocator ' , 0 , 0 ) ;
context . TarinaiPlacementPreviewSystem . applyToolOrientation ( orientationWorld , oneWayRail , 0 ) ;
activate ( oneWayRail ) ;
const oneWayFence = { { id : ' one-way-fence ' , type : ' one_way_fence ' , x : 8 , y : 0 , amount : 999 , dead : false } } ;
const oneWayWorld = { {
items : [ oneWayRail , oneWayFence ] , tarinai : [ ] , time : 1 , frameCount : 2 , w : 900 , h : 700 , drawListDirty : false , itemBucketRebuildsTotal : 0 ,
ensureItemBuckets ( ) { { } } , markSpatialDirty ( ) { { } } ,
nearbyObstacles ( ) { { return this . items ; } } , nearbyItems ( ) { { return this . items ; } } ,
itemsOfType ( type ) { { return this . items . filter ( item = > item . type == = type & & ! item . dead ) ; } } ,
isFenceType ( type ) { { return type == = ' one_way_fence ' ; } } ,
solidObstacleRects ( item ) { {
if ( item != = oneWayFence ) return [ ] ;
return [ { { left : 4 , right : 12 , top : - 100 , bottom : 100 , cx : 8 , cy : 0 , halfW : 4 , halfH : 100 , angle : 0 , cos : 1 , sin : 0 , oriented : true , type : ' one_way_fence ' , item , oneWay : true , oneWayNx : 1 , oneWayNy : 0 } } ] ;
} } ,
} } ;
oneWayRail . world = oneWayWorld ; oneWayFence . world = oneWayWorld ;
context . TarinaiMechanicalSystem . updateWorld ( oneWayWorld , 0.016 , { { maxSubsteps : 1 } } ) ;
if ( ! Number . isFinite ( context . TarinaiPhysicsBodySystem . scalar ( oneWayRail , ' railDir ' , 0 ) ) ) throw new Error ( ' one-way mechanical fence update produced an invalid rail direction ' ) ;
const cargoRail = makeItem ( ' reciprocator ' , 0 , 0 ) ;
context . TarinaiPlacementPreviewSystem . applyToolOrientation ( orientationWorld , cargoRail , 0 ) ;
activate ( cargoRail ) ;
const ball = { { id : ' ball ' , type : ' ball ' , x : 8 , y : 0 , r : 18 , radius : 18 , vx : 0 , vy : 0 , dead : false } } ;
const cargoWorld = { {
items : [ cargoRail , ball ] , tarinai : [ ] , time : 2 , frameCount : 2 , w : 900 , h : 700 , drawListDirty : false , itemBucketRebuildsTotal : 0 ,
ensureItemBuckets ( ) { { } } , markSpatialDirty ( ) { { } } ,
nearbyObstacles ( ) { { return this . items ; } } , nearbyItems ( ) { { return this . items ; } } ,
itemsOfType ( type ) { { return this . items . filter ( item = > item . type == = type & & ! item . dead ) ; } } ,
isFenceType ( ) { { return false ; } } , solidObstacleRects ( ) { { return [ ] ; } } ,
} } ;
cargoRail . world = cargoWorld ; ball . world = cargoWorld ;
context . TarinaiMechanicalSystem . updateWorld ( cargoWorld , 0.016 , { { maxSubsteps : 1 } } ) ;
if ( context . TarinaiPhysicsBodySystem . scalar ( cargoRail , ' railDir ' , 1 ) != = 1 ) throw new Error ( ' reciprocator reversed on movable cargo ' ) ;
if ( ! ( ball . x > 8 | | Math . abs ( ball . vx ) > 0.01 | | Math . abs ( ball . vy ) > 0.01 ) ) throw new Error ( ' reciprocator did not push movable cargo ' ) ;
2026-07-09 16:08:11 +09:00
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " mechanical collision/editor smoke test failed " )
2026-07-10 16:40:06 +09:00
ok ( " mechanical pairs, obstacle-only reciprocator reversal, cargo pushing, placement orientation, and 20px editor snapping verified " )
2026-07-09 16:08:11 +09:00
def colony_limits_and_defaults_smoke ( ) - > None :
tool_defs = read_text ( ROOT / " js/item_tool_definitions.js " )
initializer = read_text ( ROOT / " js/item_type_initializers.js " )
world_budget = read_text ( ROOT / " js/world_spatial_budget.js " )
world_family = read_text ( ROOT / " js/world_family_social.js " )
needs = read_text ( ROOT / " js/tarinai_needs_items.js " )
social = read_text ( ROOT / " js/tarinai_social_action_runtime.js " )
snapshot = read_text ( ROOT / " js/snapshot_system.js " )
codec = read_text ( ROOT / " js/save_codec.js " )
index = read_text ( ROOT / " index.html " )
if ' t === " reciprocator " ) return Math.PI / 2 ' not in tool_defs :
fail ( " reciprocator default angle is not 90 degrees " )
small = " [[-52, -20, 52, -20], [52, -20, 52, 20], [52, 20, -52, 20], [-52, 20, -52, -20]] "
poison_files = [
" js/item_type_initializers.js " , " js/physics_world_system.js " , " js/item_render_runtime.js " ,
" js/mechanical_system.js " , " js/world_placement_log.js " ,
]
for rel in poison_files :
body = read_text ( ROOT / rel )
if not all ( token in body for token in ( " -52 " , " -20 " , " 52 " , " 20 " ) ) :
fail ( f " small poison-block shape missing from { rel } " )
if " [-82, -28, 82, -28] " in body :
fail ( f " legacy large poison-block shape remains in { rel } " )
if ' radius: 44 ' not in tool_defs or ' thickness: 11 ' not in initializer :
fail ( " small poison-block radius/thickness defaults are not synchronized " )
for token in ( " canAddTarinai(count = 1) " , " canAddObjects(count = 1) " , " setTarinaiPopulationLimit " , " setObjectLimit " ) :
if token not in world_budget :
fail ( f " colony limit runtime missing: { token } " )
if " if (!(this.canAddTarinai?.(1) ?? true)) return null; " not in world_family :
fail ( " tarinai creation/birth is not unconditionally guarded by population limit " )
if " if (!this.canAddObjects(1)) return null; " not in world_budget :
fail ( " object creation is not unconditionally guarded by object limit " )
if " ignoreColonyLimit " in world_family or " ignoreColonyLimit " in world_budget :
fail ( " colony limit bypass option remains in central creation APIs " )
unsafe_fallback = re . compile ( r " addItem \ ? \ .[^ \ n;]+ \ | \ |[^ \ n;]*items \ .push " )
for rel in ( ROOT / " js " ) . glob ( " *.js " ) :
if unsafe_fallback . search ( read_text ( rel ) ) :
fail ( f " object-limit bypass fallback remains in { rel . name } " )
direct_item_pushes = [ ]
direct_tarinai_pushes = [ ]
for rel in ( ROOT / " js " ) . glob ( " *.js " ) :
body = read_text ( rel )
if re . search ( r " \ b(?:this|worldRef|world) \ .items \ .push \ ( " , body ) :
direct_item_pushes . append ( rel . name )
if re . search ( r " \ b(?:this|worldRef|world) \ .tarinai \ .push \ ( " , body ) :
direct_tarinai_pushes . append ( rel . name )
if sorted ( direct_item_pushes ) != [ " snapshot_system.js " , " world_spatial_budget.js " ] :
fail ( " unexpected direct world item insertion can bypass object limit: " + " , " . join ( sorted ( direct_item_pushes ) ) )
if sorted ( direct_tarinai_pushes ) != [ " snapshot_system.js " , " world_family_social.js " ] :
fail ( " unexpected direct tarinai insertion can bypass population limit: " + " , " . join ( sorted ( direct_tarinai_pushes ) ) )
guarded_sources = {
" js/item_dynamic_ball_system.js " : " if (!addedDrop) continue; " ,
" js/fire_runtime_system.js " : " if (!addedFire) return null; " ,
" js/tarinai_building_behavior.js " : " canAddObjects?.(1) " ,
" js/world_combat_effects.js " : " if (!spawned) return 0; " ,
}
for rel , token in guarded_sources . items ( ) :
if token not in read_text ( ROOT / rel ) :
fail ( f " object-limit guard missing from { rel } : { token } " )
2026-07-10 16:40:06 +09:00
if " championMate " not in needs or " championPair " not in social or " mateWeight *= 4.0 " not in social :
2026-07-09 16:08:11 +09:00
fail ( " champion-pair breeding preference is incomplete " )
2026-07-10 16:40:06 +09:00
charts = read_text ( ROOT / " js/ui_charts.js " )
family_social = read_text ( ROOT / " js/world_family_social.js " )
if ' key: " births " ' not in charts or ' key: " deaths " ' not in charts or " birthEventCount " not in charts :
fail ( " colony population chart birth/death series are missing " )
if " inheritedChampionPersonality " not in family_social or " parent.isTarinaiChampion " not in family_social or " * 0.70 " not in family_social :
fail ( " champion personality inheritance is missing " )
inheritance_block = family_social . split ( " function inheritedChampionPersonality " , 1 ) [ 1 ] . split ( " (function (global) " , 1 ) [ 0 ]
if " isZunchiSlave " in inheritance_block or " zunchiSlaveLocked " in inheritance_block :
fail ( " champion inheritance must not alter zunchi-slave handling " )
2026-07-09 16:08:11 +09:00
for token in ( " tarinaiPopulationLimit " , " objectLimit " ) :
if token not in snapshot or token not in codec :
fail ( f " colony limit persistence missing: { token } " )
for element_id in ( " tarinaiPopulationLimitInput " , " objectLimitInput " , " colonyLimitStatus " ) :
if f ' id= " { element_id } " ' not in index :
fail ( f " colony limit UI missing: { element_id } " )
js = f """
const vm = require ( ' vm ' ) ;
const fs = require ( ' fs ' ) ;
class World { {
constructor ( ) { {
this . items = [ ] ;
this . tarinai = [ ] ;
this . itemCounts = { { } } ;
this . tarinaiPopulationLimit = 0 ;
this . objectLimit = 0 ;
} }
markItemBucketsDirty ( ) { { } }
markSpatialDirty ( ) { { } }
markTerrainDirty ( ) { { } }
markTerrainDirtyAt ( ) { { } }
} }
const context = { { console , World , CONFIG : { { grassLimit : Infinity } } , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . TarinaiGround = { { grassLimit ( ) { { return Infinity ; } } } } ;
context . TarinaiPerf = { { snapshot ( ) { { return null ; } } , begin ( ) { { return null ; } } } } ;
vm . createContext ( context ) ;
vm . runInContext ( fs . readFileSync ( { str ( ROOT / ' js/world_spatial_budget.js ' ) ! r } , ' utf8 ' ) , context ) ;
const world = new context . World ( ) ;
if ( world . tarinaiPopulationLimit != = 0 | | world . objectLimit != = 0 ) throw new Error ( ' limits must default to unlimited ' ) ;
world . setObjectLimit ( 2.9 ) ;
if ( world . objectLimit != = 2 ) throw new Error ( ' object limit must normalize to a positive integer ' ) ;
if ( ! world . canAddObjects ( 2 ) | | world . canAddObjects ( 3 ) ) throw new Error ( ' object capacity query is incorrect ' ) ;
if ( ! world . addItem ( { { type : ' stone ' } } , ' test-1 ' ) ) throw new Error ( ' first object rejected ' ) ;
if ( ! world . addItem ( { { type : ' stone ' } } , ' test-2 ' ) ) throw new Error ( ' second object rejected ' ) ;
if ( world . addItem ( { { type : ' stone ' } } , ' test-3 ' ) != = null ) throw new Error ( ' object limit not enforced ' ) ;
world . items [ 0 ] . dead = true ;
if ( ! world . addItem ( { { type : ' stone ' } } , ' test-4 ' ) ) throw new Error ( ' dead object should free capacity ' ) ;
world . setObjectLimit ( 1 ) ;
if ( world . addItem ( { { type : ' stone ' } } , ' over-limit-existing ' ) != = null ) throw new Error ( ' existing over-limit world must reject new objects ' ) ;
for ( const item of world . items ) item . dead = true ;
if ( ! world . addItem ( { { type : ' stone ' } } , ' capacity-restored ' ) ) throw new Error ( ' capacity was not restored after all live objects died ' ) ;
world . setTarinaiPopulationLimit ( 1.8 ) ;
if ( world . tarinaiPopulationLimit != = 1 ) throw new Error ( ' tarinai limit must normalize to a positive integer ' ) ;
world . tarinai . push ( { { dead : false } } ) ;
if ( world . canAddTarinai ( 1 ) ) throw new Error ( ' tarinai limit not enforced ' ) ;
world . tarinai [ 0 ] . dead = true ;
if ( ! world . canAddTarinai ( 1 ) ) throw new Error ( ' dead tarinai should free capacity ' ) ;
world . setObjectLimit ( ' ' ) ;
if ( world . objectLimit != = 0 | | ! world . canAddObjects ( 1000 ) ) throw new Error ( ' blank/zero must mean unlimited ' ) ;
2026-07-10 16:40:06 +09:00
context . TarinaiSnapshot = { { version : 27 } } ;
context . TarinaiSaveSchema = { { BINARY_SCHEMA_VERSION : 33 , FIELD_IDS : [ ' garden ' ] , itemTypeValue ( i , fallback = ' ' ) { { return fallback ; } } } } ;
2026-07-09 16:08:11 +09:00
context . CONFIG . dayLength = 120 ;
context . TextEncoder = TextEncoder ;
context . TextDecoder = TextDecoder ;
vm . runInContext ( fs . readFileSync ( { str ( ROOT / ' js/save_codec.js ' ) ! r } , ' utf8 ' ) , context ) ;
( async ( ) = > { {
2026-07-10 16:40:06 +09:00
const source = { { v : 27 , a : ' tj1 ' , m : [ 1 , 0 , ' garden ' ] , w : [ 0 , 0 , 0 , 120 , 0 , - 9990 , 1 , 0 , ' seed ' , 0 , 12 , 345 ] , t : [ ] , i : [ ] } } ;
2026-07-09 16:08:11 +09:00
const encoded = await context . TarinaiSaveCodec . encodeSnapshot ( source ) ;
const decoded = await context . TarinaiSaveCodec . decodeSnapshot ( encoded ) ;
if ( decoded . w [ 10 ] != = 12 | | decoded . w [ 11 ] != = 345 ) throw new Error ( ' colony limits did not survive save roundtrip ' ) ;
} } ) ( ) . catch ( error = > { { console . error ( error ) ; process . exitCode = 1 ; } } ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " colony limit runtime smoke test failed " )
ok ( " champion breeding, mechanical defaults, and colony limits are wired " )
def physics_tuning_smoke ( ) - > None :
editor = read_text ( ROOT / " js/physics_shape_editor_system.js " )
environment = read_text ( ROOT / " js/world_environment.js " )
helpers = read_text ( ROOT / " js/physics_helpers.js " )
constraints = read_text ( ROOT / " js/constraint_system.js " )
dynamic_tools = read_text ( ROOT / " js/item_dynamic_tool_system.js " )
initializer = read_text ( ROOT / " js/item_type_initializers.js " )
2026-07-10 16:40:06 +09:00
for token in ( " BOUNCE_FENCE_RESTITUTION = 4.20 " , " BOUNCE_FENCE_MIN_SPEED = 820 " , " BOUNCE_FENCE_MAX_SPEED = 1680 " ) :
2026-07-09 16:08:11 +09:00
if token not in environment :
fail ( f " strong bounce-fence tuning missing: { token } " )
if " const iterations = 8 " not in constraints or " const compliance = mode === \" rod \" ? 0.085 / (step * 60) : 0 " not in constraints :
fail ( " rope endpoint constraint is still compliant/stretchable " )
if " STICKY_BOMB_BLAST_SCALE = 0.68 " not in dynamic_tools or " item.blastScale = 0.68 " not in initializer :
fail ( " sticky bomb is not synchronized with small-firecracker blast scale " )
js = f """
const vm = require ( ' vm ' ) ;
const editorSource = { editor ! r } ;
const helperSource = { helpers ! r } ;
const constraintSource = { constraints ! r } ;
const dynamicSource = { dynamic_tools ! r } ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . clamp = ( v , lo , hi ) = > Math . max ( lo , Math . min ( hi , v ) ) ;
context . distXY = ( x1 , y1 , x2 , y2 ) = > Math . hypot ( x2 - x1 , y2 - y1 ) ;
context . CONFIG = { { worldPadding : 0 } } ;
context . Effect = function Effect ( ) { { } } ;
context . TarinaiGeometry = { {
pointSegmentDistance ( px , py , x1 , y1 , x2 , y2 ) { {
const dx = x2 - x1 , dy = y2 - y1 ;
const den = dx * dx + dy * dy | | 1 ;
const t = Math . max ( 0 , Math . min ( 1 , ( ( px - x1 ) * dx + ( py - y1 ) * dy ) / den ) ) ;
return Math . hypot ( px - ( x1 + dx * t ) , py - ( y1 + dy * t ) ) ;
} } ,
rectLocalPoint ( rect , x , y ) { { return { { x : x - ( rect . cx | | 0 ) , y : y - ( rect . cy | | 0 ) } } ; } } ,
rectWorldVector ( _rect , x , y ) { { return { { x , y } } ; } } ,
rectWorldNormal ( _rect , x , y ) { { return { { x , y } } ; } } ,
} } ;
context . TarinaiMechanicalSystem = { {
isMechanicalType ( ) { { return false ; } } ,
isPowered ( ) { { return false ; } } ,
applySurfaceVelocityToCircle ( ) { { } } ,
wakeItem ( ) { { } } ,
} } ;
context . TarinaiPhysicsProjectionSystem = { { markSpatialDirty ( ) { { return false ; } } } } ;
context . TarinaiMovementUpdateStep = { { markSleepExternalMotion ( ) { { } } } } ;
context . itemAngleFor = item = > Number ( item ? . angle | | 0 ) | | 0 ;
context . normalizedItemAngle = angle = > Number ( angle | | 0 ) | | 0 ;
vm . createContext ( context ) ;
vm . runInContext ( editorSource , context , { { filename : ' physics_shape_editor_system.js ' } } ) ;
const circle = context . TarinaiPhysicsShapeEditorSystem . templateSegments ( ' circle ' ) ;
if ( ! Array . isArray ( circle ) | | circle . length != = 24 ) throw new Error ( ' circle template is not a 24-sided contour ' ) ;
for ( const segment of circle ) { {
if ( segment . length != = 4 | | segment . some ( value = > value % 20 != = 0 ) ) throw new Error ( ' circle template left the 20px grid ' ) ;
} }
for ( let i = 0 ; i < circle . length ; i + = 1 ) { {
const next = circle [ ( i + 1 ) % circle . length ] ;
if ( circle [ i ] [ 2 ] != = next [ 0 ] | | circle [ i ] [ 3 ] != = next [ 1 ] ) throw new Error ( ' circle contour is not closed ' ) ;
} }
vm . runInContext ( helperSource , context , { { filename : ' physics_helpers.js ' } } ) ;
const ball = { { x : - 4 , y : 5 , prevX : - 14 , prevY : 5 , vx : 100 , vy : 0 , r : 5 , spinVelocity : 0 } } ;
const fence = { { left : 0 , right : 10 , top : 0 , bottom : 10 , bounce : true , restitution : 2.40 , minBounceSpeed : 520 } } ;
if ( ! context . TarinaiPhysics . bounceCircleOffRect ( ball , fence , 5 , fence . restitution , null , { { minBounceSpeed : fence . minBounceSpeed } } ) ) throw new Error ( ' bounce-fence collision did not resolve ' ) ;
if ( ball . vx > - 519 ) throw new Error ( ' bounce-fence launch is not substantially stronger ' ) ;
context . TarinaiPhysicsBodySystem = { {
scalar ( item , key , fallback ) { { return Number ( item . physicsBody ? . [ key ] ? ? fallback ) ; } } ,
setScalar ( item , key , value ) { { item . physicsBody | | = { { } } ; item . physicsBody [ key ] = value ; return true ; } } ,
linkScalar ( item , key , fallback ) { {
const c = item . physicsConstraint | | { { } } ;
const map = { { len : ' length ' , midX : ' midX ' , midY : ' midY ' , midVx : ' midVx ' , midVy : ' midVy ' , awakeUntil : ' awakeUntil ' } } ;
return Number ( c [ map [ key ] | | key ] ? ? fallback ) ;
} } ,
setLinkScalar ( item , key , value ) { {
const c = item . physicsConstraint | | = { { } } ;
const map = { { len : ' length ' , midX : ' midX ' , midY : ' midY ' , midVx : ' midVx ' , midVy : ' midVy ' , awakeUntil : ' awakeUntil ' } } ;
c [ map [ key ] | | key ] = value ;
return true ;
} } ,
endpoint ( item , index ) { { return item . physicsConstraint ? . endpoints ? . [ index ] | | null ; } } ,
particles ( item ) { { return item . physicsConstraint ? . particles | | null ; } } ,
setParticles ( item , list ) { { item . physicsConstraint . particles = list ; return true ; } } ,
ensureConstraint ( item ) { { return item . physicsConstraint ; } } ,
applyConstraintState ( ) { { return true ; } } ,
syncPoseFromItem ( ) { { return true ; } } ,
} } ;
vm . runInContext ( constraintSource , context , { { filename : ' constraint_system.js ' } } ) ;
const a = { { id : ' a ' , type : ' ball ' , x : 0 , y : 0 , r : 10 , dead : false } } ;
const b = { { id : ' b ' , type : ' ball ' , x : 200 , y : 0 , r : 10 , dead : false } } ;
const objects = new Map ( [ [ a . id , a ] , [ b . id , b ] ] ) ;
const world = { {
time : 1 ,
items : [ a , b ] ,
tarinai : [ ] ,
itemCounts : { { rope : 1 } } ,
itemById ( id ) { { return objects . get ( id ) | | null ; } } ,
nearbyTarinai ( ) { { return [ ] ; } } ,
nearbySolidObstacleRects ( ) { { return [ ] ; } } ,
markSpatialDirty ( ) { { } } ,
drawListDirty : false ,
} } ;
const rope = { {
id : ' rope-1 ' , type : ' rope ' , dead : false , amount : 999 , world ,
physicsConstraint : { { endpoints : [ { { kind : ' item ' , id : ' a ' , x : 0 , y : 0 } } , { { kind : ' item ' , id : ' b ' , x : 200 , y : 0 } } ] , length : 100 , particles : null } } ,
} } ;
context . TarinaiConstraintSystem . updateFlexibleLink ( rope , 1 / 60 , world ) ;
const tautDistance = Math . hypot ( b . x - a . x , b . y - a . y ) ;
if ( tautDistance > 100.26 ) throw new Error ( ` rope stretched to $ { { tautDistance } } px ` ) ;
const beforeA = { { x : a . x , y : a . y } } , beforeB = { { x : b . x , y : b . y } } ;
b . x = a . x + 60 ;
context . TarinaiConstraintSystem . updateFlexibleLink ( rope , 1 / 60 , world ) ;
if ( Math . hypot ( a . x - beforeA . x , a . y - beforeA . y ) > 0.01 ) throw new Error ( ' slack rope pushed endpoint A ' ) ;
if ( Math . abs ( ( b . x - a . x ) - 60 ) > 0.01 ) throw new Error ( ' slack rope pushed endpoints apart ' ) ;
context . deterministicChance = ( ) = > false ;
context . Effect = function Effect ( ) { { } } ;
context . audio = { { explode ( ) { { } } } } ;
vm . runInContext ( dynamicSource , context , { { filename : ' item_dynamic_tool_system.js ' } } ) ;
let capturedScale = null ;
const sticky = { { type : ' sticky_bomb ' , dead : false , fuseTimer : 0 , stickyBombCarrierId : ' ' , stickyBombTransferCooldown : 0 , spin : 0 , spinVelocity : 0 } } ;
const stickyWorld = { { tarinai : [ ] , explodeFirecracker ( item ) { { capturedScale = item . blastScale ; return true ; } } } } ;
context . TarinaiItemDynamicToolSystem . update ( sticky , 1 / 60 , stickyWorld ) ;
if ( Math . abs ( capturedScale - 0.68 ) > 1e-9 ) throw new Error ( ' sticky bomb does not match small firecracker blast scale ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " physics tuning smoke test failed " )
ok ( " round circle template, strong bounce fence, inextensible rope, and small sticky-bomb blast verified " )
2026-07-10 16:40:06 +09:00
def champion_inheritance_smoke ( ) - > None :
source = read_text ( ROOT / " js/world_family_social.js " )
start = source . find ( " function inheritedChampionPersonality " )
end = source . find ( " (function (global) " , start )
if start < 0 or end < 0 :
fail ( " champion inheritance helper not found " )
helper = source [ start : end ]
js = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . normalizePersonality = source = > ( { {
aggression : Math . max ( - 1 , Math . min ( 1 , Number ( source ? . aggression | | 0 ) ) ) ,
openness : Math . max ( - 1 , Math . min ( 1 , Number ( source ? . openness | | 0 ) ) ) ,
sociability : Math . max ( - 1 , Math . min ( 1 , Number ( source ? . sociability | | 0 ) ) ) ,
neuroticism : Math . max ( - 1 , Math . min ( 1 , Number ( source ? . neuroticism | | 0 ) ) ) ,
} } ) ;
context . clampPersonalityValue = value = > Math . max ( - 1 , Math . min ( 1 , Number ( value ) | | 0 ) ) ;
context . TarinaiSeedFactory = { { birthProfile ( ) { { return { { birthPersonality : { { aggression : 0 , openness : 0 , sociability : 0 , neuroticism : 0 } } } } ; } } } } ;
vm . createContext ( context ) ;
vm . runInContext ( { helper ! r } , context , { { filename : ' champion_inheritance.js ' } } ) ;
const normal = { { birthSeed : ' normal ' , birthPersonality : { { aggression : 0 , openness : 0 , sociability : 0 , neuroticism : 0 } } , isTarinaiChampion : false } } ;
const champion = { { birthSeed : ' champion ' , birthPersonality : { { aggression : 0 , openness : 0 , sociability : 0 , neuroticism : 0 } } , isTarinaiChampion : true } } ;
const child = context . inheritedChampionPersonality ( ' child ' , [ champion , normal ] , { { } } ) ;
if ( ! child | | child . aggression < 0.5 | | child . openness < 0.5 | | child . sociability < 0.5 ) throw new Error ( ' champion boost was not inherited ' ) ;
if ( Math . abs ( child . neuroticism ) > 1e-9 ) throw new Error ( ' unboosted personality axis was inherited ' ) ;
const descendant = { { birthSeed : ' descendant ' , birthPersonality : child , isTarinaiChampion : false } } ;
const grandchild = context . inheritedChampionPersonality ( ' grandchild ' , [ descendant , normal ] , { { } } ) ;
if ( ! grandchild | | ! ( grandchild . aggression > 0 & & grandchild . aggression < child . aggression ) ) throw new Error ( ' champion lineage did not damp across generations ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " champion inheritance smoke test failed " )
ok ( " champion personality inheritance and generational damping verified " )
def one_way_routing_smoke ( ) - > None :
geometry_source = read_text ( ROOT / " js/geometry_helpers.js " )
source = read_text ( ROOT / " js/world_environment.js " )
render = read_text ( ROOT / " js/render.js " )
js = f """
const vm = require ( ' vm ' ) ;
class World { { } }
const context = { { console , World , globalThis : null , window : null , CONFIG : { { } } ,
TarinaiMechanicalShapeSystem : { { isMechanicalType ( ) { { return false ; } } , reach ( ) { { return 0 ; } } } } ,
TarinaiMechanicalSystem : { { } } , TarinaiPhysicsBodySystem : { { } } ,
} } ;
context . globalThis = context ; context . window = context ;
vm . createContext ( context ) ;
vm . runInContext ( { geometry_source ! r } , context , { { filename : ' geometry_helpers.js ' } } ) ;
vm . runInContext ( { source ! r } , context , { { filename : ' world_environment.js ' } } ) ;
const world = new context . World ( ) ;
world . isFenceType = type = > [ ' fence_h ' , ' one_way_fence ' ] . includes ( type ) ;
const oneWay = { { type : ' one_way_fence ' , item : { { type : ' one_way_fence ' } } , oneWay : true , oneWayNx : 1 , oneWayNy : 0 , oriented : false , left : - 4 , right : 4 , top : - 20 , bottom : 20 } } ;
world . nearbySolidObstacleRects = ( ) = > [ oneWay ] ;
if ( world . pathBlockedByFence ( - 30 , 0 , 30 , 0 , 0 ) ) throw new Error ( ' one-way fence blocked arrow direction ' ) ;
if ( ! world . pathBlockedByFence ( 30 , 0 , - 30 , 0 , 0 ) ) throw new Error ( ' one-way fence allowed reverse direction ' ) ;
if ( world . pointBlockedByObstacle ( 0 , 0 , 0 , { { directionalOneWay : true } } ) ) throw new Error ( ' one-way fence permanently blocked grid point ' ) ;
if ( ! world . pointBlockedByObstacle ( 0 , 0 , 0 ) ) throw new Error ( ' one-way fence lost physical point occupancy ' ) ;
const normal = { { type : ' fence_h ' , item : { { type : ' fence_h ' } } , oriented : false , left : - 4 , right : 4 , top : - 20 , bottom : 20 } } ;
world . nearbySolidObstacleRects = ( ) = > [ normal ] ;
if ( ! world . pathBlockedByFence ( - 30 , 0 , 30 , 0 , 0 ) ) throw new Error ( ' ordinary fence stopped blocking routes ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " one-way fence routing smoke test failed " )
if ' type === " one_way_fence " ' not in render or " arrowLength " not in render :
fail ( " one-way fence placement direction overlay is missing " )
ok ( " one-way fence preview and directional routing verified " )
def check_save_stream_deadlock_guard ( ) - > None :
codec = read_text ( ROOT / " js/save_codec.js " )
ui = read_text ( ROOT / " js/save_system.js " )
read_task = codec . find ( " const readTask = (async () => " )
write_call = codec . find ( " await sink.write( " )
if read_task < 0 or write_call < 0 or read_task > write_call :
fail ( " save compression output must be drained before awaiting writes " )
if ' exportBtn.dataset.busy = " 1 " ' not in ui or ' save text generation failed ' not in ui :
fail ( " save export UI must recover from pending/error states " )
ok ( " save compression stream backpressure guard present " )
2026-07-11 21:13:39 +09:00
def check_electrocution_gate_chart_and_ascii ( ) - > None :
health = read_text ( ROOT / " js/health.js " )
combat_effects = read_text ( ROOT / " js/world_combat_effects.js " )
action_state = read_text ( ROOT / " js/tarinai_action_state.js " )
signal = read_text ( ROOT / " js/signal_system.js " )
charts = read_text ( ROOT / " js/ui_charts.js " )
bind = read_text ( ROOT / " js/ui_bind.js " )
panel = read_text ( ROOT / " css/panel.css " )
if ' \\ u611f \\ u96fb ' not in health or ' \\ u901a \\ u96fb \\ u4e2d \\ u306e \\ u96fb \\ u7dda ' not in health :
fail ( " electrocution cause normalization is missing " )
if ' " gate_fence " ' not in signal or ' node.gateOpen = nextOpen ' not in signal or ' signal-gate-toggle ' not in signal :
fail ( " gate fence signal output handling is missing " )
if ' <button type= " button " class= " chart-series-toggle ' not in charts or ' data-series-key= " $ {s.key} " ' not in charts :
fail ( " chart legend text and icon are not wrapped by a shared button " )
if ' closest?.( " [data-series-key] " ) ' not in bind or ' i[data-series-key] ' in bind :
fail ( " chart legend click delegation still targets only the icon " )
if ' .colony-chart-legend .chart-series-toggle ' not in panel :
fail ( " chart legend button styling is missing " )
runtime = f """
const vm = require ( ' vm ' ) ;
const context = { {
console , globalThis : null , window : null ,
clamp : ( v , lo , hi ) = > Math . max ( lo , Math . min ( hi , Number ( v ) | | 0 ) ) ,
2026-07-17 12:25:23 +09:00
TarinaiGeometry : { {
pointSegmentDistance ( px , py , ax , ay , bx , by ) { {
const sx = bx - ax , sy = by - ay ;
const len2 = sx * sx + sy * sy ;
if ( len2 < = 1e-12 ) return Math . hypot ( px - ax , py - ay ) ;
const t = Math . max ( 0 , Math . min ( 1 , ( ( px - ax ) * sx + ( py - ay ) * sy ) / len2 ) ) ;
return Math . hypot ( px - ( ax + sx * t ) , py - ( ay + sy * t ) ) ;
} } ,
} } ,
2026-07-11 21:13:39 +09:00
audio : { { damage ( ) { { } } , play ( ) { { } } , death ( ) { { } } } } ,
} } ;
context . globalThis = context ;
context . window = context ;
vm . createContext ( context ) ;
vm . runInContext ( { health ! r } , context , { { filename : ' health.js ' } } ) ;
const victim = { {
energy : 2 , maxEnergy : 100 , world : { { time : 10 , items : [ ] , effects : [ ] , effectCounts : { { } } , ownedItemsFor ( ) { { return [ ] ; } } } } ,
recentDamageWindow : [ ] , nextDamageZunchiAt : Infinity , hunger : 0 , stress : 0 , mood : 100 ,
showHpBar ( ) { { } } ,
die ( reason , opts ) { { this . deathReason = context . HEALTH . normalizeDeathReason ( this , reason , opts ) ; } } ,
} } ;
context . HEALTH . applyDamage ( victim , 2.4 , ' \u901a \u96fb \u4e2d \u306e \u96fb \u7dda \u306b \u89e6 \u308c \u305f ' ) ;
if ( victim . deathReason != = ' \u611f \u96fb ' ) throw new Error ( ` electrocution death cause mismatch : $ { { victim . deathReason } } ` ) ;
context . TarinaiPhysicsBodySystem = { { endpoint ( item , index ) { { return item . physicsConstraint . endpoints [ index ] ; } } } } ;
context . TarinaiLinkRuntime = { { endpointWorld ( endpoint ) { { return { { x : endpoint . x | | 0 , y : endpoint . y | | 0 } } ; } } } } ;
context . TarinaiEffect = function ( type , x , y , opts ) { { Object . assign ( this , { { type , x , y } } , opts ) ; } } ;
vm . runInContext ( { signal ! r } , context , { { filename : ' signal_system.js ' } } ) ;
const pressure = { { id : ' pressure ' , type : ' pressure_switch ' , x : 0 , y : 0 , pressureThreshold : 1 , pressureWidth : 100 , pressureHeight : 100 } } ;
const gate = { { id : ' gate ' , type : ' gate_fence ' , x : 80 , y : 0 , gateOpen : false } } ;
const wire = { {
id : ' wire ' , type : ' wire ' , x : 40 , y : 0 , r : 40 ,
physicsConstraint : { { endpoints : [
{ { kind : ' item ' , id : ' pressure ' , x : 0 , y : 0 } } ,
{ { kind : ' item ' , id : ' gate ' , x : 80 , y : 0 } } ,
] } } ,
} } ;
let occupied = true ;
const world = { {
items : [ pressure , gate , wire ] , tarinai : [ ] , ants : [ ] , effects : [ ] , time : 1 , spatialMarks : 0 , drawListDirty : false ,
itemsOfType ( type ) { { return this . items . filter ( item = > item . type == = type ) ; } } ,
itemById ( id ) { { return this . items . find ( item = > item . id == = id ) | | null ; } } ,
nearbyTarinai ( ) { { return occupied ? [ { { x : 0 , y : 0 , radius : 10 , dead : false } } ] : [ ] ; } } ,
nearbyAnts ( ) { { return [ ] ; } } ,
isTarinaiHiddenInNestBox ( ) { { return false ; } } ,
markSpatialDirty ( reason ) { { if ( reason == = ' signal-gate-toggle ' ) this . spatialMarks + = 1 ; } } ,
} } ;
context . TarinaiSignalSystem . updateWorld ( world ) ;
if ( ! gate . gateOpen | | ! gate . _signalDriven | | ! gate . _signalOn | | world . spatialMarks != = 1 ) throw new Error ( ' powered gate did not open ' ) ;
occupied = false ;
world . time + = 1 ;
context . TarinaiSignalSystem . updateWorld ( world ) ;
if ( gate . gateOpen | | ! gate . _signalDriven | | gate . _signalOn | | world . spatialMarks != = 2 ) throw new Error ( ' unpowered gate did not close ' ) ;
context . TarinaiSignalSystem . updateWorld ( world ) ;
if ( world . spatialMarks != = 2 ) throw new Error ( ' unchanged gate state dirtied spatial cache again ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( runtime )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " electrocution/gate signal runtime smoke test failed " )
ascii_paths = [ * ( ROOT / " js " ) . glob ( " *.js " ) , ROOT / " service-worker.js " , ROOT / " index.html " ]
raw = [ ]
for path in ascii_paths :
body = read_text ( path )
if any ( ord ( ch ) > 127 for ch in body ) :
raw . append ( path . relative_to ( ROOT ) . as_posix ( ) )
if raw :
fail ( " unescaped non-ASCII characters remain in application source: " + " , " . join ( raw ) )
ok ( " electrocution cause, powered gate, chart legend buttons, and ASCII-only application source verified " )
def check_connection_overlay_and_time_detector ( ) - > None :
placement = read_text ( ROOT / " js/world_placement_log.js " )
signal = read_text ( ROOT / " js/signal_system.js " )
snapshot = read_text ( ROOT / " js/snapshot_system.js " )
styles = read_text ( ROOT / " css/base.css " )
render = read_text ( ROOT / " js/render.js " )
if ' const CONNECTION_ITEM_TYPES = new Set([ " rope " , " rod " , " spring " , " wire " , " insulated_wire " ]) ' not in placement :
fail ( " connection item grab exclusion set is missing " )
if ' CONNECTION_ITEM_TYPES.has(it.type) ' not in placement :
fail ( " connection items can still become pinch hover/grab targets " )
if ' <option value= " time " > \\ u6642 \\ u523b</option> ' not in placement :
fail ( " detector editor time target option is missing " )
for control_id in ( " pressureSwitchTimeStart " , " pressureSwitchTimeEnd " , " pressureSwitchTimeTrack " ) :
if control_id not in placement :
fail ( f " detector dual time slider is missing { control_id } " )
if ' .pressure-time-thumb-start ' not in styles or ' .pressure-time-thumb-end ' not in styles :
fail ( " detector dual-thumb slider styling is missing " )
2026-07-15 14:44:29 +09:00
if ' const targets = [ " tarinai " , " item " , " time " , " hunger_avg " ' not in snapshot or ' item.pressureTimeStart ' not in snapshot or ' item.pressureTimeEnd ' not in snapshot :
fail ( " expanded detector snapshot persistence is missing " )
2026-07-16 22:12:03 +09:00
if ' data.pressureTarget = item.pressureTarget || " tarinai " ' not in placement or ' targets.has(data.pressureTarget) ' not in placement or ' data.pressureMin ' not in placement or ' data.pressureMax ' not in placement :
2026-07-15 14:44:29 +09:00
fail ( " expanded detector copy persistence is missing " )
if ' item.pressureTarget === " time " || item.pressureTarget === " temperature " ' not in render :
fail ( " non-spatial detectors still render a detection rectangle " )
removed = [ " bio_sensor " , " power_relay " , " automationItemEditor " ]
for token in removed :
if token in placement or token in snapshot or token in signal :
fail ( f " removed automation item remains in runtime source: { token } " )
2026-07-11 21:13:39 +09:00
runtime = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null , window : null , CONFIG : { { dayLength : 120 } } } } ;
context . globalThis = context ;
context . window = context ;
context . TarinaiEffect = function ( ) { { } } ;
vm . createContext ( context ) ;
vm . runInContext ( { signal ! r } , context , { { filename : ' signal_system.js ' } } ) ;
const detector = { {
id : ' clock ' , type : ' pressure_switch ' , pressureTarget : ' time ' ,
pressureTimeStart : 1320 , pressureTimeEnd : 360 ,
pressureThreshold : 1 , pressureWidth : 220 , pressureHeight : 160 ,
signalActive : false ,
} } ;
let minute = 1380 ;
const world = { {
items : [ detector ] , tarinai : [ ] , ants : [ ] , effects : [ ] , drawListDirty : false ,
itemsOfType ( type ) { { return this . items . filter ( item = > item . type == = type ) ; } } ,
dayProgress ( ) { { return minute / 1440 ; } } ,
nearbyTarinai ( ) { { return [ ] ; } } , nearbyAnts ( ) { { return [ ] ; } } ,
isTarinaiHiddenInNestBox ( ) { { return false ; } } , markSpatialDirty ( ) { { } } ,
} } ;
context . TarinaiSignalSystem . updateWorld ( world ) ;
if ( ! detector . signalActive ) throw new Error ( ' overnight range inactive at 23:00 ' ) ;
minute = 355 ;
context . TarinaiSignalSystem . updateWorld ( world ) ;
if ( ! detector . signalActive ) throw new Error ( ' overnight range inactive before 06:00 ' ) ;
minute = 720 ;
context . TarinaiSignalSystem . updateWorld ( world ) ;
if ( detector . signalActive ) throw new Error ( ' overnight range active at noon ' ) ;
detector . pressureTimeStart = 480 ;
detector . pressureTimeEnd = 480 ;
context . TarinaiSignalSystem . updateWorld ( world ) ;
if ( ! detector . signalActive ) throw new Error ( ' equal time handles must mean all day ' ) ;
if ( ! context . TarinaiSignalSystem . isMinuteInRange ( 600 , 480 , 720 ) ) throw new Error ( ' daytime range helper rejected inside time ' ) ;
if ( context . TarinaiSignalSystem . isMinuteInRange ( 800 , 480 , 720 ) ) throw new Error ( ' daytime range helper accepted outside time ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( runtime )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " time detector runtime smoke test failed " )
ok ( " connection pinch overlay exclusion and dual-range time detector verified " )
def check_link_dependency_and_plushie_recovery ( ) - > None :
placement = read_text ( ROOT / " js/world_placement_log.js " )
tools = read_text ( ROOT / " js/item_tool_definitions.js " )
lifecycle = read_text ( ROOT / " js/structure_lifecycle.js " )
maintenance = read_text ( ROOT / " js/simulation_maintenance_system.js " )
styles = read_text ( ROOT / " css/base.css " )
if ' pressure-time-hint ' in placement or ' pressure-time-hint ' in styles or ' \\ u958b \\ u59cb \\ u304c \\ u7d42 \\ u4e86 \\ u3088 \\ u308a \\ u5f8c ' in placement :
fail ( " detector overnight/all-day explanatory hint still exists " )
expected_tooltip = ' tooltip: " \\ u69d8 \\ u3005 \\ u306a \\ u72b6 \\ u6cc1 \\ u3092 \\ u691c \\ u77e5 \\ u3057 \\ u3066 \\ u96fb \\ u7dda \\ u306b \\ u4fe1 \\ u53f7 \\ u3092 \\ u9001 \\ u308b \\ u3002 \\ u30af \\ u30ea \\ u30c3 \\ u30af \\ u3067 \\ u8a73 \\ u7d30 \\ u8a2d \\ u5b9a \\ u3002 " '
if expected_tooltip not in tools :
fail ( " detector tooltip does not match the requested description " )
if ' maintainDependencies?.(worldRef) ' not in maintenance :
fail ( " link/plushie dependency maintenance is not called each simulation frame " )
for token in ( ' function removeOrphanedLinks ' , ' function reconcilePlushies ' , ' maintainDependencies, ' ) :
if token not in lifecycle :
fail ( f " dependency lifecycle implementation missing: { token } " )
runtime = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null , window : null } } ;
context . globalThis = context ;
context . window = context ;
context . TarinaiPhysicsBodySystem = { { endpoint ( item , index ) { { return item . physicsConstraint ? . endpoints ? . [ index ] | | null ; } } } } ;
vm . createContext ( context ) ;
vm . runInContext ( { lifecycle ! r } , context , { { filename : ' structure_lifecycle.js ' } } ) ;
const item = ( id , type = ' stone ' ) = > ( { { id , type , amount : 1 , hp : 1 , dead : false , x : 0 , y : 0 } } ) ;
const link = ( id , type , a , b ) = > ( { { id , type , amount : 999 , hp : 999 , dead : false , physicsConstraint : { { endpoints : [ a , b ] } } } } ) ;
const supportA = item ( ' support-a ' ) ;
const supportB = item ( ' support-b ' ) ;
const supportC = item ( ' support-c ' ) ;
const intact = link ( ' intact ' , ' rod ' , { { kind : ' item ' , id : ' support-b ' } } , { { kind : ' item ' , id : ' support-c ' } } ) ;
const orphan = link ( ' orphan ' , ' rope ' , { { kind : ' item ' , id : ' support-a ' } } , { { kind : ' item ' , id : ' support-b ' } } ) ;
const dependent = link ( ' dependent ' , ' wire ' , { { kind : ' item ' , id : ' orphan ' } } , { { kind : ' item ' , id : ' support-c ' } } ) ;
const deadTarinai = { { id : ' dead-t ' , liveToken : 9 , dead : true , x : 0 , y : 0 } } ;
const tarinaiLink = link ( ' tarinai-link ' , ' spring ' , { { kind : ' tarinai ' , id : ' dead-t ' , liveToken : 9 } } , { { kind : ' item ' , id : ' support-b ' } } ) ;
const deadAnt = { { id : ' dead-ant ' , dead : true , x : 0 , y : 0 } } ;
const antLink = link ( ' ant-link ' , ' insulated_wire ' , { { kind : ' ant ' , id : ' dead-ant ' } } , { { kind : ' item ' , id : ' support-b ' } } ) ;
const owner = { { id : ' owner ' , dead : false , x : 120 , y : 90 , radius : 20 } } ;
const restoredPlushie = { { id : ' plush-restored ' , type : ' plushie ' , isStructure : true , ownerId : ' owner ' , carriedById : ' ' , onHead : false , amount : 1 , hp : 1 , dead : false , x : 10 , y : 20 , vx : 8 , vy : 4 , spinVelocity : 3 } } ;
const flyingPlushie = { { id : ' plush-flying ' , type : ' plushie ' , isStructure : true , ownerId : ' owner ' , carriedById : ' ' , onHead : false , amount : 1 , hp : 1 , dead : false , x : 30 , y : 40 , plushieFlingTimer : 1.2 } } ;
const orphanPlushie = { { id : ' plush-orphan ' , type : ' plushie ' , isStructure : true , ownerId : ' missing-owner ' , carriedById : ' ' , onHead : false , amount : 1 , hp : 1 , dead : false , x : 50 , y : 60 } } ;
const world = { {
items : [ supportA , supportB , supportC , intact , orphan , dependent , tarinaiLink , antLink , restoredPlushie , flyingPlushie , orphanPlushie ] ,
tarinai : [ owner , deadTarinai ] , ants : [ deadAnt ] , pendingLinkEndpoint : { { kind : ' item ' , id : ' support-a ' } } ,
itemById ( id ) { { return this . items . find ( value = > value . id == = id ) | | null ; } } ,
itemsOfType ( type ) { { return this . items . filter ( value = > value . type == = type ) ; } } ,
liveTarinaiById ( id , token = null ) { { return this . tarinai . find ( value = > ! value . dead & & value . id == = id & & ( token == null | | value . liveToken == = token ) ) | | null ; } } ,
markItemBucketsDirty ( ) { { this . bucketDirty = true ; } } , markSpatialDirty ( ) { { this . spatialDirty = true ; } } ,
} } ;
supportA . amount = 0 ;
const result = context . TarinaiStructureLifecycle . maintainDependencies ( world ) ;
if ( result . linksRemoved != = 4 ) throw new Error ( ` unexpected removed link count : $ { { result . linksRemoved } } ` ) ;
if ( orphan . amount != = 0 | | dependent . amount != = 0 | | tarinaiLink . amount != = 0 | | antLink . amount != = 0 ) throw new Error ( ' orphaned links were not removed ' ) ;
if ( intact . amount < = 0 ) throw new Error ( ' intact link was removed ' ) ;
if ( world . pendingLinkEndpoint != = null ) throw new Error ( ' pending endpoint survived support removal ' ) ;
if ( restoredPlushie . carriedById != = owner . id | | ! restoredPlushie . onHead ) throw new Error ( ' detached owned plushie was not restored ' ) ;
if ( restoredPlushie . x != = owner . x | | restoredPlushie . y != = owner . y - 22 | | restoredPlushie . vx != = 0 | | restoredPlushie . vy != = 0 ) throw new Error ( ' restored plushie pose was not normalized ' ) ;
if ( flyingPlushie . carriedById | | flyingPlushie . onHead | | flyingPlushie . dead ) throw new Error ( ' active flying plushie was incorrectly reclaimed ' ) ;
if ( ! orphanPlushie . dead | | orphanPlushie . amount != = 0 ) throw new Error ( ' ownerless plushie remained on the ground ' ) ;
if ( ! world . bucketDirty | | ! world . spatialDirty ) throw new Error ( ' dependency repair did not invalidate world caches ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( runtime )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " link dependency/plushie recovery runtime smoke test failed " )
ok ( " detector copy, orphan-link cascade, and plushie recovery verified " )
def check_splat_link_target_and_appetite_label ( ) - > None :
tools = read_text ( ROOT / " js/world_tool_actions.js " )
needs = read_text ( ROOT / " js/tarinai_needs_core.js " )
charts = read_text ( ROOT / " js/ui_charts.js " )
if ' if (!it || it.dead || it.type === " splat " ) continue; ' not in tools :
fail ( " transient splat items are still eligible as connection endpoints " )
appetite = " \\ u98df \\ u6b32 "
feeding = " \\ u6442 \\ u990c "
if f ' food: " { appetite } " ' not in needs :
fail ( " food need label was not changed to appetite " )
if f ' key: " need_food " , label: " { appetite } " ' not in charts :
fail ( " food need chart label was not changed to appetite " )
if f ' food: " { feeding } " ' in needs or f ' key: " need_food " , label: " { feeding } " ' in charts :
fail ( " legacy feeding need label remains in a user-facing need label " )
runtime = f """
const vm = require ( ' vm ' ) ;
class World { { } }
const context = { {
console , World , globalThis : null , window : null ,
distXY ( x1 , y1 , x2 , y2 ) { { return Math . hypot ( x2 - x1 , y2 - y1 ) ; } } ,
itemAngleFor ( ) { { return 0 ; } } ,
toolLabel ( type ) { { return type ; } } ,
TarinaiPhysicsSoftClear : { { isSoftPlacementType ( ) { { return false ; } } } } ,
TarinaiMechanicalSystem : { { isMechanicalType ( ) { { return false ; } } } } ,
TarinaiCollisionFootprints : { { hitTestItem ( world , item , x , y ) { { return { { hit : true , distance : 0 } } ; } } } } ,
TarinaiGeometry : { { pointSegmentDistance ( ) { { return 0 ; } } } } ,
} } ;
context . globalThis = context ;
context . window = context ;
vm . createContext ( context ) ;
vm . runInContext ( { tools ! r } , context , { { filename : ' world_tool_actions.js ' } } ) ;
const world = new context . World ( ) ;
world . tool = ' rope ' ;
world . tarinai = [ ] ;
world . items = [ { { id : ' splat-1 ' , type : ' splat ' , x : 40 , y : 50 , r : 26 , dead : false } } ] ;
world . nearbyItems = ( ) = > world . items ;
world . isFenceType = ( ) = > false ;
const endpoint = world . findLinkEndpointAt ( 40 , 50 ) ;
if ( endpoint != = null ) throw new Error ( ' splat was returned as a connection endpoint ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( runtime )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " splat connection endpoint runtime smoke test failed " )
ok ( " splat endpoint exclusion and appetite labels verified " )
def check_habitat_food_medicine_additions ( ) :
tool_defs = read_text ( ROOT / " js/item_tool_definitions.js " )
metadata = read_text ( ROOT / " js/item_tool_metadata.js " )
food_defs = read_text ( ROOT / " js/item_food_definitions.js " )
effect_defs = read_text ( ROOT / " js/item_effect_definitions.js " )
needs_items = read_text ( ROOT / " js/tarinai_needs_items.js " )
local_env = read_text ( ROOT / " js/tarinai_local_environment_system.js " )
update_frame = read_text ( ROOT / " js/tarinai_update_step_frame.js " )
disease_nest = read_text ( ROOT / " js/tarinai_disease_nest.js " )
item_effects = read_text ( ROOT / " js/tarinai_item_effects.js " )
consumable = read_text ( ROOT / " js/tarinai_consumable_behavior.js " )
interaction = read_text ( ROOT / " js/tarinai_food_interaction_system.js " )
render_runtime = read_text ( ROOT / " js/item_render_runtime.js " )
render_source = read_text ( ROOT / " js/render.js " )
ball_system = read_text ( ROOT / " js/item_dynamic_ball_system.js " )
initializers = read_text ( ROOT / " js/item_type_initializers.js " )
items = read_text ( ROOT / " js/items.js " )
temperature_system = read_text ( ROOT / " js/world_temperature_system.js " )
health = read_text ( ROOT / " js/health.js " )
combat_effects = read_text ( ROOT / " js/world_combat_effects.js " )
action_state = read_text ( ROOT / " js/tarinai_action_state.js " )
def contains ( text : str , needle : str , message : str ) - > None :
if needle not in text :
fail ( message )
def excludes ( text : str , needle : str , message : str ) - > None :
if needle in text :
fail ( message )
required_tools = [ " toilet " , " rain_shelter " , " food " , " first_aid " , " sedative " ]
for key in required_tools :
contains ( tool_defs , f ' { key } : {{ id: " { key } " ' , f " missing tool definition for { key } " )
contains ( metadata , ' toolIds: [ " food " , " sweet " ' , " pet food must be exposed in food category " )
contains ( metadata , ' toolIds: [ " first_aid " , " sedative " , " sleep_drug " ' , " new medicines must be exposed before legacy medicines " )
contains ( metadata , ' toolIds: [ " zunchi " , " toilet " , " rain_shelter " ' , " new habitat tools must be exposed " )
contains ( food_defs , ' food: { label: " \\ u30da \\ u30c3 \\ u30c8 \\ u30d5 \\ u30fc \\ u30c9 " ' , " pet food label must replace generic food label " )
contains ( food_defs , ' first_aid: { label: " \\ u6551 \\ u6025 \\ u85ac " , nutrition: 1.0, hungerRelief: 0 ' , " first aid must not provide nutrition or hunger relief " )
contains ( food_defs , ' sedative: { label: " \\ u93ae \\ u9759 \\ u85ac " ' , " sedative food definition missing " )
contains ( effect_defs , ' Object.assign(rawDefinitions.first_aid ' , " first aid effect behavior missing " )
contains ( effect_defs , ' tarinai.recoverHealth?.(amount); ' , " first aid must immediately recover HP " )
excludes ( effect_defs , ' applyNeedRelief(tarinai, { health: -22, safety: -4 }); ' , " first aid must not modify needs " )
contains ( effect_defs , ' tarinai.setTimedItemEffect?.( " sedative " , duration); ' , " sedative timed effect missing " )
contains ( effect_defs , ' speedMultiplier: 0.72 ' , " sedative speed modifier missing " )
contains ( item_effects , ' this.hasItemEffect( " sedative " ) ' , " sedative speed modifier is not applied " )
contains ( consumable , ' const hpOnlyMedicine = foodType === " first_aid " ; ' , " behavior consumption lacks first-aid isolation " )
contains ( interaction , ' const hpOnlyMedicine = foodType === " first_aid " ; ' , " contact consumption lacks first-aid isolation " )
excludes ( needs_items , ' nearestUsableToilet ' , " toilet sand must not be sought as an action target " )
excludes ( needs_items , ' beginToiletUrge ' , " toilet waiting behavior must be removed " )
excludes ( needs_items , ' tryUseToiletForPoop ' , " toilet-specific defecation routing must be removed " )
excludes ( action_state , ' seek_toilet: " use_toilet " ' , " legacy toilet action mapping remains active " )
contains ( needs_items , ' if (this.state === " seek_toilet " || this.toiletUrgeTargetId) ' , " legacy saved toilet behavior is not cleared " )
contains ( combat_effects , ' toiletSandAt(x, y) ' , " toilet sand footprint lookup missing " )
contains ( combat_effects , ' const sand = this.toiletSandAt?.(spawnX, spawnY) || null; ' , " zunchi spawn is not intercepted by toilet sand " )
contains ( combat_effects , ' return null; ' , " toilet sand must suppress zunchi creation " )
excludes ( metadata , ' " stove " , " toilet " , " rain_shelter " ' , " toilet sand must be walkable " )
contains ( tool_defs , ' label: \" \\ u30c8 \\ u30a4 \\ u30ec \\ u306e \\ u7802 \" ' , " toilet was not renamed to toilet sand " )
contains ( tool_defs , ' tooltip: \" \\ u4e0a \\ u306b \\ u6392 \\ u6cc4 \\ u3055 \\ u308c \\ u305f \\ u305a \\ u3093 \\ u3061 \\ u304c \\ u76f4 \\ u3061 \\ u306b \\ u6d88 \\ u6ec5 \\ u3059 \\ u308b \\ u3002 \" ' , " toilet sand description mismatch " )
contains ( initializers , ' if (type === " toilet " ) ' , " toilet sand initializer missing " )
contains ( initializers , ' if (type === " rain_shelter " ) ' , " rain shelter initializer missing " )
if initializers . index ( ' if (type === " toilet " ) ' ) > initializers . index ( ' if (type === " bed " ) ' ) :
fail ( " toilet initializer is incorrectly nested after bed initialization " )
contains ( local_env , ' global.TarinaiParasolSystem ' , " parasol geometry system missing " )
contains ( local_env , ' global.TarinaiRainShelterSystem = parasolSystem ' , " parasol compatibility alias missing " )
contains ( tool_defs , ' label: " \\ u30d1 \\ u30e9 \\ u30bd \\ u30eb " ' , " rain shelter was not renamed to parasol " )
contains ( tool_defs , ' tooltip: " \\ u96e8 \\ u3068 \\ u65e5 \\ u5dee \\ u3057 \\ u3092 \\ u9632 \\ u304e \\ u3001 \\ u4f53 \\ u611f \\ u6c17 \\ u6e29 \\ u3092 \\ u7a4f \\ u3084 \\ u304b \\ u306b \\ u3059 \\ u308b \\ u3002 " ' , " parasol description mismatch " )
contains ( temperature_system , ' if (weather === " sunny " ) return 3.5; ' , " sunny felt-temperature increase missing " )
contains ( temperature_system , ' if (weather === " light_rain " ) return -3.5; ' , " rain felt-temperature decrease missing " )
contains ( temperature_system , ' weatherBlocked ? 0 ' , " parasol does not neutralize weather felt-temperature correction " )
contains ( temperature_system , ' types.push( " rain_shelter " ) ' , " parasol is not a temperature-comfort target " )
contains ( temperature_system , ' c.tempOverride != null && Number.isFinite ' , " null temperature override still collapses candidates to zero degrees " )
contains ( update_frame , ' shelteredFromRain ' , " rain exposure is not blocked by shelter " )
contains ( disease_nest , ' && !shelteredFromRain ' , " rain washing still occurs under shelter " )
contains ( render_runtime , ' this.type === " toilet " ' , " toilet renderer missing " )
contains ( render_runtime , ' this.type === " rain_shelter " ' , " rain shelter renderer missing " )
contains ( initializers , ' globalThis.TarinaiToiletSandSystem ' , " shared toilet-sand geometry system missing " )
contains ( combat_effects , ' TarinaiToiletSandSystem?.contains?.(item, x, y) ' , " toilet absorption does not use the visible sand footprint " )
contains ( render_runtime , ' TarinaiToiletSandSystem?.footprint?.(this) ' , " toilet renderer does not use the absorption footprint " )
contains ( render_source , ' it.type === " grass_bed " || it.type === " toilet " ' , " toilet sand is not layered with the grass bed " )
contains ( local_env , ' centerY: Number(item.y || 0) + r * 0.84 ' , " parasol shade is not centered on the pole foot " )
contains ( local_env , ' radiusX: r * 1.58 ' , " parasol shade footprint width mismatch " )
contains ( ball_system , ' fanWindAt, ' , " fan wind geometry is not exported for temperature calculations " )
contains ( temperature_system , ' fanWindAt?.(it, x, y, { range: fanRange }) ' , " fan felt-temperature effect does not use the visible wind cone " )
contains ( render_runtime , ' ctx.ellipse(footprint.centerX - this.x, footprint.centerY - this.y, footprint.radiusX, footprint.radiusY ' , " parasol ground shadow missing " )
contains ( items , ' if (key === " food " ) ' , " pet food bowl renderer missing " )
# New item types must be appended after all pre-existing item IDs so current-schema saves keep their old indices.
ant_pos = tool_defs . index ( ' ant_corpse: { id: " ant_corpse " ' )
for key in ( " toilet " , " rain_shelter " , " first_aid " , " sedative " ) :
if tool_defs . index ( f ' { key } : {{ id: " { key } " ' ) < ant_pos :
fail ( f " { key } was inserted into the middle of the save item-type ordering " )
runtime = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null , window : null , Math , Object } } ;
context . globalThis = context ;
context . window = context ;
context . clamp = ( v , lo , hi ) = > Math . max ( lo , Math . min ( hi , v ) ) ;
context . distXY = ( x1 , y1 , x2 , y2 ) = > Math . hypot ( x2 - x1 , y2 - y1 ) ;
context . rand = ( a , b ) = > ( a + b ) / 2 ;
context . setGrassStage = ( ) = > false ;
context . isServingFoodType = ( ) = > false ;
context . isParamEffectItemType = ( ) = > false ;
context . isPinType = ( ) = > false ;
context . isPinType = ( ) = > false ;
context . isRotatableItemType = ( ) = > false ;
context . defaultItemAngle = ( ) = > 0 ;
context . foodServingsForSize = ( ) = > 3 ;
context . passiveFoodDecayInterval = ( ) = > 60 ;
context . globalThis . TarinaiItemDynamicToolSystem = { { robotCleanerDefaultMask ( ) { { return 3 ; } } , robotCleanerMoveSpeed : 60 } } ;
context . globalThis . TarinaiPhysicsBodySystem = { { isPhysicsType ( ) { { return false ; } } , invalidateItem ( ) { { } } } } ;
vm . createContext ( context ) ;
vm . runInContext ( { initializers ! r } , context , { { filename : ' item_type_initializers.js ' } } ) ;
const toilet = { { roles : { { } } , type : ' toilet ' , x : 0 , y : 0 , r : 48 , dead : false } } ;
context . initializeItemTypeState ( toilet , ' toilet ' , 0 , 0 ) ;
if ( toilet . amount != = 999 ) throw new Error ( ' toilet sand initializer failed ' ) ;
const shelter = { { roles : { { } } , r : 50 } } ;
context . initializeItemTypeState ( shelter , ' rain_shelter ' , 0 , 0 ) ;
if ( shelter . amount != = 999 | | ! ( shelter . rainShelterRange > = 76 ) ) throw new Error ( ' rain shelter initializer failed ' ) ;
const localContext = { { console , globalThis : null , window : null , Math , Object } } ;
localContext . globalThis = localContext ;
localContext . window = localContext ;
localContext . distXY = context . distXY ;
localContext . clamp = context . clamp ;
localContext . applyNeedRelief = ( ) = > { { } } ;
vm . createContext ( localContext ) ;
vm . runInContext ( { local_env ! r } , localContext , { { filename : ' tarinai_local_environment_system.js ' } } ) ;
const shelterItem = { { type : ' rain_shelter ' , x : 100 , y : 100 , r : 50 , dead : false } } ;
const world = { { weather : ' light_rain ' , itemsOfType ( type ) { { return type == = ' rain_shelter ' ? [ shelterItem ] : [ ] ; } } } } ;
const shelterFootprint = localContext . TarinaiParasolSystem . parasolFootprint ( shelterItem ) ;
if ( ! localContext . TarinaiRainShelterSystem . isSheltered ( world , shelterFootprint . centerX , shelterFootprint . centerY ) ) throw new Error ( ' parasol did not cover its shadow center ' ) ;
if ( localContext . TarinaiRainShelterSystem . isSheltered ( world , shelterItem . x , shelterItem . y - 10 ) ) throw new Error ( ' parasol covered a point outside the visible shadow ' ) ;
if ( localContext . TarinaiRainShelterSystem . isSheltered ( world , 500 , 500 ) ) throw new Error ( ' parasol covered a distant point ' ) ;
if ( ! localContext . TarinaiParasolSystem . blocksWeatherAt ( world , shelterFootprint . centerX , shelterFootprint . centerY ) ) throw new Error ( ' parasol did not block weather in its shadow ' ) ;
const tempContext = { { console , globalThis : null , window : null , Math , Object } } ;
tempContext . globalThis = tempContext ;
tempContext . window = tempContext ;
tempContext . World = class World { { } } ;
tempContext . CONFIG = { { standardTemperature : 15 , temperatureItemRange : 190 , climateTemperatureMin : - 20 , climateTemperatureMax : 50 } } ;
tempContext . clamp = context . clamp ;
tempContext . distXY = context . distXY ;
tempContext . itemAngleFor = ( ) = > 0 ;
tempContext . TarinaiParasolSystem = localContext . TarinaiParasolSystem ;
tempContext . TarinaiRainShelterSystem = localContext . TarinaiRainShelterSystem ;
tempContext . TarinaiItemDynamicBallSystem = { { fanWindAt ( fan , x , y , opts = { { } } ) { {
const range = opts . range | | 320 ; const dx = x - fan . x ; const dy = y - fan . y ; const a = fan . angle | | 0 ;
const along = dx * Math . cos ( a ) + dy * Math . sin ( a ) ; if ( along < 8 | | along > range ) return null ;
const lateral = Math . abs ( - dx * Math . sin ( a ) + dy * Math . cos ( a ) ) ; const halfWidth = 26 + along * Math . tan ( 0.62 ) ;
if ( lateral > halfWidth ) return null ; return { { falloff : Math . pow ( Math . max ( 0 , 1 - along / range ) , 0.64 ) * Math . pow ( Math . max ( 0 , 1 - lateral / halfWidth ) , 0.34 ) } } ;
} } } } ;
vm . createContext ( tempContext ) ;
vm . runInContext ( { temperature_system ! r } , tempContext , { { filename : ' world_temperature_system.js ' } } ) ;
const tempWorld = new tempContext . World ( ) ;
tempWorld . currentTemperature = 15 ;
tempWorld . temperatureAuto = true ;
tempWorld . groundType = ' soil ' ;
tempWorld . weather = ' sunny ' ;
tempWorld . nearbyItems = ( ) = > [ ] ;
tempWorld . itemsOfType = ( ) = > [ ] ;
const actor = { { x : 0 , y : 0 , state : ' idle ' , sunbathTimer : 0 } } ;
if ( Math . abs ( tempWorld . temperatureAt ( 0 , 0 , actor ) - 18.5 ) > 0.001 ) throw new Error ( ' sunny felt temperature did not rise ' ) ;
tempWorld . weather = ' light_rain ' ;
if ( Math . abs ( tempWorld . temperatureAt ( 0 , 0 , actor ) - 11.5 ) > 0.001 ) throw new Error ( ' rain felt temperature did not fall ' ) ;
const localShelter = { { type : ' rain_shelter ' , x : 0 , y : 0 , r : 50 , dead : false } } ;
const localShade = localContext . TarinaiParasolSystem . parasolFootprint ( localShelter ) ;
tempWorld . itemsOfType = type = > type == = ' rain_shelter ' ? [ localShelter ] : [ ] ;
if ( Math . abs ( tempWorld . temperatureAt ( localShade . centerX , localShade . centerY , actor ) - 15 ) > 0.001 ) throw new Error ( ' parasol did not neutralize rain felt-temperature correction ' ) ;
tempWorld . weather = ' sunny ' ;
if ( Math . abs ( tempWorld . temperatureAt ( localShade . centerX , localShade . centerY , actor ) - 15 ) > 0.001 ) throw new Error ( ' parasol did not neutralize sunny felt-temperature correction ' ) ;
tempWorld . weather = ' cloudy ' ;
tempWorld . currentTemperature = 30 ;
const fan = { { type : ' fan ' , x : 0 , y : 0 , r : 26 , angle : 0 , dead : false } } ;
tempWorld . itemsOfType = ( ) = > [ ] ;
tempWorld . nearbyItems = ( x , y , radius ) = > Math . hypot ( fan . x - x , fan . y - y ) < = radius ? [ fan ] : [ ] ;
const fanNear = tempWorld . temperatureAt ( 100 , 0 , actor ) ;
const fanFar = tempWorld . temperatureAt ( 280 , 0 , actor ) ;
const fanOutside = tempWorld . temperatureAt ( 330 , 0 , actor ) ;
if ( ! ( fanNear < 30 & & fanFar < 30 & & Math . abs ( fanOutside - 30 ) < 0.001 ) ) throw new Error ( ' fan felt-temperature cone does not match the 320px wind range ' ) ;
tempWorld . nearbyItems = ( ) = > [ ] ;
tempWorld . w = 1000 ;
tempWorld . h = 720 ;
tempWorld . currentTemperature = 24 ;
tempWorld . weather = ' sunny ' ;
tempWorld . itemsOfType = type = > type == = ' rain_shelter ' ? [ { { type : ' rain_shelter ' , x : 300 , y : 300 , r : 50 , dead : false } } ] : [ ] ;
tempWorld . pointBlockedByObstacle = ( ) = > false ;
tempWorld . pathBlockedByFence = ( ) = > false ;
const hotActor = { { x : 100 , y : 300 , radius : 20 , dead : false , state : ' idle ' , sunbathTimer : 0 , genetics : { { temperatureOffset : 0 } } } } ;
const sunnyTarget = tempWorld . findComfortTemperatureSpot ( hotActor , { { force : true , requireComfort : false } } ) ;
if ( ! sunnyTarget | | ! localContext . TarinaiParasolSystem . isSheltered ( tempWorld , sunnyTarget . x , sunnyTarget . y ) ) throw new Error ( ' hot sunny actor did not choose parasol shade ' ) ;
if ( sunnyTarget . temperature == = 0 ) throw new Error ( ' null temperature override was interpreted as zero ' ) ;
tempWorld . currentTemperature = 9 ;
tempWorld . weather = ' light_rain ' ;
const coldTarget = tempWorld . findComfortTemperatureSpot ( hotActor , { { force : true , requireComfort : false } } ) ;
if ( ! coldTarget | | ! localContext . TarinaiParasolSystem . isSheltered ( tempWorld , coldTarget . x , coldTarget . y ) ) throw new Error ( ' cold rainy actor did not choose parasol cover ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( runtime )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " new habitat item runtime smoke test failed " )
method_start = needs_items . index ( " dropPoopNow(source = \" normal \" ) { " )
method_end = needs_items . index ( " interactWithItems(dt) { " , method_start )
poop_methods = needs_items [ method_start : method_end ]
toilet_runtime = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null , window : null , Math , Object } } ;
context . globalThis = context ;
context . window = context ;
context . NORMAL_POOP_MEAL_THRESHOLD = 4 ;
context . distXY = ( x1 , y1 , x2 , y2 ) = > Math . hypot ( x2 - x1 , y2 - y1 ) ;
context . rand = ( a , b ) = > ( a + b ) / 2 ;
context . clamp = ( v , lo , hi ) = > Math . max ( lo , Math . min ( hi , v ) ) ;
context . applyNeedShock = ( ) = > false ;
vm . createContext ( context ) ;
const methods = vm . runInContext ( ' ( {{ ' + { poop_methods ! r } + ' }}) ' , context ) ;
let spawned = 0 ;
const actor = { {
. . . methods ,
world : { { spawnZunchi ( ) { { spawned + = 1 ; } } , log ( ) { { } } } } ,
name : ' A ' , x : 100 , y : 100 , radius : 20 , facing : 1 , digest : 0 , poopCount : 0 , state : ' idle ' , target : null ,
facingDir ( ) { { return 1 ; } } , bubble ( ) { { } } , hasLodgedPinEffect ( ) { { return false ; } } ,
} } ;
if ( ! actor . makePoop ( 4 ) ) throw new Error ( ' ordinary poop did not occur ' ) ;
if ( spawned != = 1 | | actor . digest != = 0 | | actor . poopCount != = 1 ) throw new Error ( ' ordinary poop accounting failed ' ) ;
if ( actor . state != = ' idle ' | | actor . target != = null ) throw new Error ( ' toilet sand caused special movement behavior ' ) ;
const combatContext = { { console , globalThis : null , window : null , Math , Object } } ;
combatContext . globalThis = combatContext ;
combatContext . window = combatContext ;
combatContext . World = class World { { } } ;
combatContext . Item = class Item { { constructor ( type , x , y ) { { this . type = type ; this . x = x ; this . y = y ; } } } } ;
combatContext . Effect = class Effect { { } } ;
combatContext . audio = { { place ( ) { { } } } } ;
combatContext . clamp = context . clamp ;
combatContext . distXY = context . distXY ;
combatContext . TarinaiToiletSandSystem = { { contains ( item , x , y ) { {
const r = Math . max ( 24 , Number ( item . r | | 48 ) | | 48 ) ; const cx = item . x ; const cy = item . y + r * 0.08 ;
const lx = x - cx ; const ly = y - cy ; const hw = r * 1.55 ; const hh = r * 0.72 ; const cr = r * 0.18 ;
const qx = Math . max ( 0 , Math . abs ( lx ) - ( hw - cr ) ) ; const qy = Math . max ( 0 , Math . abs ( ly ) - ( hh - cr ) ) ;
return qx * qx + qy * qy < = cr * cr ;
} } } } ;
vm . createContext ( combatContext ) ;
vm . runInContext ( { combat_effects ! r } , combatContext , { { filename : ' world_combat_effects.js ' } } ) ;
const world = new combatContext . World ( ) ;
world . w = 800 ; world . h = 600 ; world . time = 12 ;
const sand = { { id : ' sand-1 ' , type : ' toilet ' , x : 200 , y : 200 , r : 48 , dead : false , angle : 0 , toiletFlash : 0 } } ;
world . itemsOfType = type = > type == = ' toilet ' ? [ sand ] : [ ] ;
let added = 0 ;
world . addItem = item = > { { added + = 1 ; return item ; } } ;
world . markTerrainDirtyAt = ( ) = > { { } } ;
if ( world . spawnZunchi ( 200 , 200 , null ) != = null | | added != = 0 | | sand . toiletFlash != = 1 ) throw new Error ( ' toilet sand did not immediately remove poop ' ) ;
const insideEdge = world . spawnZunchi ( 270 , 200 , null ) ;
if ( insideEdge != = null | | added != = 0 ) throw new Error ( ' toilet sand visible rectangle did not absorb poop near its edge ' ) ;
const outside = world . spawnZunchi ( 278 , 200 , null ) ;
if ( ! outside | | added != = 1 ) throw new Error ( ' toilet sand removed poop outside its footprint ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( toilet_runtime )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " toilet sand runtime smoke test failed " )
ok ( " toilet sand footprint/layering, parasol shade geometry, fan felt-temperature, weather targeting, pet food, first aid, and sedative additions verified " )
def check_parasol_rain_and_preset_updates ( ) - > None :
placement = read_text ( ROOT / " js/placement_preview_system.js " )
render = read_text ( ROOT / " js/render.js " )
ambient = read_text ( ROOT / " js/simulation_ambient_system.js " )
weather = read_text ( ROOT / " js/weather_system.js " )
presets = read_text ( ROOT / " js/world_reset_presets.js " )
required = [
( placement , ' overlay: overlayForItem(tmp) ' , " parasol placement preview overlay is not exported " ) ,
2026-07-16 22:12:03 +09:00
( placement , ' shape: " roundedRect " , centerX: footprint.centerX, centerY: footprint.centerY ' , " parasol preview does not reuse shade geometry " ) ,
( render , ' overlay?.shape === " roundedRect " ' , " shape-matched placement overlays are not rendered " ) ,
2026-07-11 21:13:39 +09:00
( weather , ' drop.rainDrop = opts?.rainDrop === true || (!hasX && !hasY); ' , " rain-created water is not tagged " ) ,
( ambient , ' const sand = worldRef.toiletSandAt?.(drop.x, drop.y) || null; ' , " toilet sand does not intercept rain water " ) ,
( ambient , ' " toilet-sand-rain-absorb " ' , " rain absorption does not invalidate toilet sand rendering " ) ,
( presets , ' segments: [[-66, 0, 66, 0]] ' , " athletic rotator was not simplified " ) ,
( presets , ' rotationEnvelope: true ' , " athletic rotator does not reserve its full rotation envelope " ) ,
( presets , ' " preset-athletic-water-balloon " ' , " athletic preset lacks water balloons " ) ,
( presets , ' " preset-happy-parasol " ' , " happy preset lacks parasols " ) ,
( presets , ' " preset-happy-water-balloon " ' , " happy preset lacks water balloons " ) ,
( presets , ' " preset-happy-toilet-sand " ' , " happy preset lacks toilet sand " ) ,
( presets , ' " preset-war-rotating-fan " ' , " war preset lacks rotating fans " ) ,
( presets , ' fanSwingOn: true ' , " war preset fans do not rotate/swing " ) ,
]
for text , needle , message in required :
if needle not in text :
fail ( message )
js = f """
const vm = require ( ' vm ' ) ;
const fs = require ( ' fs ' ) ;
const overlayContext = { { console , globalThis : null , window : null , Math , Object } } ;
overlayContext . globalThis = overlayContext ; overlayContext . window = overlayContext ;
overlayContext . clamp = ( v , a , b ) = > Math . max ( a , Math . min ( b , v ) ) ;
overlayContext . distXY = ( a , b , x , y ) = > Math . hypot ( x - a , y - b ) ;
overlayContext . applyNeedRelief = ( ) = > { { } } ;
overlayContext . itemRadiusFor = ( type , fallback ) = > type == = ' rain_shelter ' ? 50 : fallback ;
overlayContext . toolItemType = tool = > tool ;
overlayContext . isRotatableItemType = ( ) = > false ;
overlayContext . defaultItemAngle = ( ) = > 0 ;
overlayContext . foodServingsForSize = ( ) = > 3 ;
overlayContext . isServingFoodType = ( ) = > false ;
overlayContext . CONFIG = { { worldPadding : 30 } } ;
overlayContext . TarinaiCollisionFootprints = { { placementRectsFor ( ) { { return [ ] ; } } , itemFootprintRadius ( _world , item ) { { return item . r ; } } } } ;
overlayContext . TarinaiMechanicalSystem = { { isMechanicalType ( ) { { return false ; } } } } ;
overlayContext . TarinaiPhysicsSoftClear = { { collectForPlacement ( ) { { return [ ] ; } } } } ;
vm . createContext ( overlayContext ) ;
vm . runInContext ( { read_text ( ROOT / ' js/tarinai_local_environment_system.js ' ) ! r } , overlayContext , { { filename : ' tarinai_local_environment_system.js ' } } ) ;
vm . runInContext ( { placement ! r } , overlayContext , { { filename : ' placement_preview_system.js ' } } ) ;
const previewWorld = { { tool : ' rain_shelter ' , pointer : { { inside : true , x : 100 , y : 120 } } , toolSizeScale ( ) { { return 1 ; } } , toolSizeFor ( ) { { return ' medium ' ; } } , placementBlocked ( ) { { return false ; } } , w : 800 , h : 600 } } ;
const preview = overlayContext . TarinaiPlacementPreviewSystem . forWorld ( previewWorld ) ;
const shade = overlayContext . TarinaiParasolSystem . parasolFootprint ( preview . item ) ;
2026-07-16 22:12:03 +09:00
if ( ! preview . overlay | | preview . overlay . centerX != = shade . centerX | | preview . overlay . centerY != = shade . centerY | | preview . overlay . halfWidth != = shade . radiusX | | preview . overlay . halfHeight != = shade . radiusY | | preview . overlay . shape != = ' roundedRect ' ) throw new Error ( ' parasol preview overlay and shade diverged ' ) ;
2026-07-11 21:13:39 +09:00
const ambientContext = { { console , globalThis : null , window : null , Math , Object } } ;
ambientContext . globalThis = ambientContext ; ambientContext . window = ambientContext ;
ambientContext . TarinaiWeatherSystem = { { shouldDropRainWater ( ) { { return true ; } } , createRainWaterItem ( ) { { return { { type : ' water ' , x : 20 , y : 30 , rainDrop : true } } ; } } } } ;
vm . createContext ( ambientContext ) ;
vm . runInContext ( { ambient ! r } , ambientContext , { { filename : ' simulation_ambient_system.js ' } } ) ;
let rainAdded = 0 , rainDirty = 0 ;
const sand = { { type : ' toilet ' , x : 20 , y : 30 , r : 48 , toiletFlash : 0 } } ;
ambientContext . TarinaiAmbientSimulationSystem . phases . spawnRainWater ( { { time : 1 , toiletSandAt ( ) { { return sand ; } } , addItem ( ) { { rainAdded + = 1 ; } } , markTerrainDirtyAt ( ) { { rainDirty + = 1 ; } } } } , 1 ) ;
if ( rainAdded != = 0 | | rainDirty != = 1 | | sand . toiletFlash != = 1 ) throw new Error ( ' toilet sand did not absorb ambient rain water ' ) ;
const presetContext = { { console , globalThis : null , window : null , Math , Object } } ;
presetContext . globalThis = presetContext ; presetContext . window = presetContext ;
let serial = 0 ;
presetContext . rand = ( a , b ) = > { { serial = ( serial + 0.371 ) % 1 ; return a + ( b - a ) * serial ; } } ;
presetContext . clamp = ( v , a , b ) = > Math . max ( a , Math . min ( b , v ) ) ;
presetContext . CONFIG = { { initialPopulation : 8 } } ;
presetContext . toolLabel = type = > type ;
presetContext . stableUnit = ( ) = > 0.42 ;
presetContext . TarinaiGrass = { { setStage ( ) { { } } } } ;
presetContext . Item = class Item { {
constructor ( type , x , y ) { {
this . id = ` i $ { { + + serial } } ` ; this . type = type ; this . x = x ; this . y = y ; this . amount = 999 ; this . dead = false ; this . roles = { { } } ; this . angle = 0 ;
this . r = type == = ' rain_shelter ' ? 50 : ( type == = ' toilet ' ? 48 : ( type == = ' fan ' ? 26 : 20 ) ) ;
if ( type == = ' rotator ' | | type == = ' reciprocator ' ) this . physicsBody = { { pose : { { x , y , angle : 0 } } , velocity : { { } } , shape : { { } } , motor : { { } } , rail : { { } } } } ;
} }
} } ;
presetContext . TarinaiPhysicsBodySystem = { { invalidateItem ( ) { { } } } } ;
presetContext . TarinaiMechanicalSystem = { { invalidateGeometry ( ) { { } } , reach ( item ) { { let reach = 0 ; for ( const seg of item . physicsBody ? . shape ? . segments | | [ ] ) reach = Math . max ( reach , Math . hypot ( seg [ 0 ] , seg [ 1 ] ) , Math . hypot ( seg [ 2 ] , seg [ 3 ] ) ) ; return reach ; } } } } ;
presetContext . TarinaiCollisionFootprints = { { itemFootprintRadius ( _world , item ) { { return item . r | | 20 ; } } , placementRectsFor ( ) { { return [ ] ; } } , rectCircleOverlap ( ) { { return false ; } } , rectsOverlap ( ) { { return false ; } } } } ;
function makeWorld ( ) { { return { { w : 1000 , h : 720 , items : [ ] , tarinai : [ ] , addItem ( item ) { { this . items . push ( item ) ; return item ; } } , addTarinai ( t ) { { this . tarinai . push ( t ) ; return t ; } } , nearbyItems ( ) { { return [ ] ; } } , placementBlocked ( ) { { return false ; } } , solidObstacleRects ( ) { { return [ ] ; } } , isFenceType ( type ) { { return String ( type ) . includes ( ' fence ' ) ; } } , placementClampPointFor ( _item , x , y ) { { return { { x , y } } ; } } } } ; } }
vm . createContext ( presetContext ) ;
vm . runInContext ( { presets ! r } , presetContext , { { filename : ' world_reset_presets.js ' } } ) ;
let presetWorld = makeWorld ( ) ;
presetContext . TarinaiResetPresets . generatePresetWorld ( presetWorld , { { id : ' garden ' } } , ' happy ' ) ;
const duplicators = presetWorld . items . filter ( item = > item . type == = ' duplicator ' ) ;
const toilets = presetWorld . items . filter ( item = > item . type == = ' toilet ' ) ;
if ( ! duplicators . length | | toilets . length != = duplicators . length | | ! duplicators . every ( dup = > toilets . some ( sandItem = > sandItem . x == = dup . x & & sandItem . y == = dup . y ) ) ) throw new Error ( ' happy toilet sand is not colocated with duplicators ' ) ;
if ( ! presetWorld . items . some ( item = > item . type == = ' rain_shelter ' ) | | ! presetWorld . items . some ( item = > item . type == = ' balloon ' ) ) throw new Error ( ' happy preset additions missing ' ) ;
presetWorld = makeWorld ( ) ;
presetContext . TarinaiResetPresets . generatePresetWorld ( presetWorld , { { id : ' garden ' } } , ' athletic ' ) ;
const rotators = presetWorld . items . filter ( item = > item . type == = ' rotator ' ) ;
if ( ! rotators . length | | rotators . some ( item = > item . physicsBody ? . shape ? . segments ? . length != = 1 ) | | ! presetWorld . items . some ( item = > item . type == = ' balloon ' ) ) throw new Error ( ' athletic preset shape/balloon additions missing ' ) ;
presetWorld = makeWorld ( ) ;
presetContext . TarinaiResetPresets . generatePresetWorld ( presetWorld , { { id : ' garden ' } } , ' war ' ) ;
const fans = presetWorld . items . filter ( item = > item . type == = ' fan ' ) ;
if ( ! fans . length | | fans . some ( item = > ! item . fanSwingOn | | ! ( item . fanSwingRange > 0 ) | | ! ( item . fanSwingSpeed > 0 ) ) ) throw new Error ( ' war preset rotating fans missing ' ) ;
const warBounceFences = presetWorld . items . filter ( item = > item . type == = ' bounce_fence ' | | item . type == = ' bounce_fence_v ' ) ;
if ( warBounceFences . length < 2 ) throw new Error ( ' war preset bounce fences missing ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " parasol/rain/preset runtime smoke test failed " )
ok ( " parasol preview alignment, rain absorption, and happy/athletic/war preset additions verified " )
def check_war_fence_sleep_placement_delete_fixes ( ) - > None :
def contains_local ( text : str , needle : str , message : str ) - > None :
if needle not in text :
fail ( message )
metadata = read_text ( ROOT / " js/item_tool_metadata.js " )
footprints = read_text ( ROOT / " js/collision_footprint_system.js " )
placement = read_text ( ROOT / " js/world_placement_log.js " )
environment = read_text ( ROOT / " js/world_environment.js " )
collisions = read_text ( ROOT / " js/collision_response_system.js " )
render = read_text ( ROOT / " js/render.js " )
tools = read_text ( ROOT / " js/world_tool_actions.js " )
presets = read_text ( ROOT / " js/world_reset_presets.js " )
contains_local ( metadata , ' freeStackingMovable: new Set([ " ball " , " balloon " , " pushpin " , " oshibyo " ' , " movable placement stacking trait missing " )
contains_local ( footprints , ' freeStacking?.has(item.type) && freeStacking.has(other.type) ' , " movable-vs-movable placement overlap is not ignored " )
if " function ballPlacementBlocked " in placement or " ballPlacementBlocked(this, item) " in placement :
fail ( " legacy ball-on-ball placement rejection remains " )
contains_local ( environment , " const ORDINARY_FENCE_RESTITUTION = 0.38; " , " ordinary fence restitution was not reduced " )
contains_local ( environment , " restitution: bounce ? BOUNCE_FENCE_RESTITUTION : ORDINARY_FENCE_RESTITUTION " , " ordinary fence restitution is not exported through fence geometry " )
contains_local ( collisions , ' const sleepingContact = t.state === " sleep " || t.sleeping; ' , " sleeping Tarinai are still excluded from ball collision candidates " )
contains_local ( collisions , ' markSleepExternalMotion?.(t, frameDt, " ball-contact " ) ' , " sleeping ball collision does not enter passive physical motion " )
contains_local ( render , ' const targetGapScale = 1.45; ' , " world tooltips do not share the closer offset " )
contains_local ( presets , ' reason: " preset-war-bounce-fence " ' , " war preset bounce fences missing " )
delete_section = tools [ tools . find ( " findDeleteToolTargetAt " ) : tools . find ( " deleteItemAt " ) ]
if " wireTool " in delete_section :
fail ( " delete target lookup still references undefined wireTool " )
js = f """
const fs = require ( ' fs ' ) ;
const vm = require ( ' vm ' ) ;
const collisionContext = { { console , Math , Object , globalThis : null , window : null } } ;
collisionContext . globalThis = collisionContext ; collisionContext . window = collisionContext ;
collisionContext . TarinaiImpactCoreSystem = { { PHYSICAL_DAMAGE_SPEED_THRESHOLD : 165 , physicalDamageFromImpactSpeed ( ) { { return 5 ; } } , itemHasAttachedLink ( ) { { return false ; } } } } ;
collisionContext . clamp = ( v , a , b ) = > Math . max ( a , Math . min ( b , v ) ) ;
collisionContext . distXY = ( ax , ay , bx , by ) = > Math . hypot ( ax - bx , ay - by ) ;
collisionContext . deterministicRange = ( ) = > 0 ;
collisionContext . deterministicAngle = ( ) = > 0 ;
collisionContext . pick = values = > values [ 0 ] ;
collisionContext . CONFIG = { { worldPadding : 0 } } ;
collisionContext . Effect = class Effect { { constructor ( . . . args ) { { this . args = args ; } } } } ;
collisionContext . tarinaiMaxEnergy = ( ) = > 100 ;
collisionContext . applyNeedRelief = ( ) = > { { } } ;
collisionContext . applyNeedShock = ( ) = > { { } } ;
let sleepMotionMarks = 0 ;
collisionContext . TarinaiMovementUpdateStep = { { markSleepExternalMotion ( ) { { sleepMotionMarks + = 1 ; } } } } ;
collisionContext . TarinaiItemDynamicPinSystem = { { transferBallPinsToTarinai ( ) { { return 0 ; } } } } ;
vm . createContext ( collisionContext ) ;
vm . runInContext ( { collisions ! r } , collisionContext , { { filename : ' collision_response_system.js ' } } ) ;
const ball = { { id : ' ball ' , type : ' ball ' , x : 9 , y : 0 , prevX : - 9 , prevY : 0 , r : 10 , vx : 100 , vy : 0 , spinVelocity : 0 , dead : false } } ;
const sleeper = { { id : ' sleeper ' , name : ' sleeper ' , x : 20 , y : 0 , radius : 20 , vx : 0 , vy : 0 , state : ' sleep ' , sleeping : true , dead : false , energy : 100 } } ;
2026-07-18 13:06:02 +09:00
const collisionWorld = { { time : 1 , w : 500 , h : 500 , itemCounts : { { ball : 1 , balloon : 0 } } , items : [ ball ] , effects : [ ] , spawnEffect ( ) { { return null ; } } , itemsOfType ( type ) { { return type == = ' ball ' ? [ ball ] : [ ] ; } } , nearbyTarinai ( ) { { return [ sleeper ] ; } } , isTarinaiHiddenInNestBox ( ) { { return false ; } } , markSpatialDirty ( ) { { } } , relationNotice ( ) { { return false ; } } , applyImpulse ( ) { { } } , applyImpactDamage ( ) { { } } } } ;
2026-07-11 21:13:39 +09:00
collisionContext . TarinaiCollisionResponseSystem . resolveBallInteractions ( collisionWorld , 0.016 ) ;
if ( ! ( ball . vx < 0 ) | | ! ( sleeper . vx > 0 ) | | sleepMotionMarks < 1 ) throw new Error ( ' sleeping Tarinai did not physically collide with ball ' ) ;
const placementContext = { { console , Math , Object , globalThis : null , window : null } } ;
placementContext . globalThis = placementContext ; placementContext . window = placementContext ;
placementContext . TarinaiGeometry = { { num ( v , fallback = 0 ) { { const n = Number ( v ) ; return Number . isFinite ( n ) ? n : fallback ; } } , rectLocalPoint ( ) { { } } , rectWorldVector ( ) { { } } , rectCorners ( ) { { return [ ] ; } } , rectAxes ( ) { { return [ ] ; } } , projectPoints ( ) { { } } , aabbOverlap ( ) { { return false ; } } , rectOverlapInfo ( ) { { } } , rectsOverlap ( ) { { return false ; } } } } ;
placementContext . TarinaiMechanicalSystem = { { isMechanicalType ( ) { { return false ; } } } } ;
placementContext . isFenceItemType = ( ) = > false ;
placementContext . itemRadiusFor = ( ) = > 14 ;
placementContext . TarinaiItemToolMetadata = { { ITEM_TRAITS : { { freeStackingMovable : new Set ( [ ' ball ' , ' balloon ' , ' pushpin ' , ' oshibyo ' , ' zunchi ' ] ) } } } } ;
vm . createContext ( placementContext ) ;
vm . runInContext ( { footprints ! r } , placementContext , { { filename : ' collision_footprint_system.js ' } } ) ;
const stackedWorld = { { nearbyItems ( ) { { return [ { { type : ' zunchi ' , x : 0 , y : 0 , r : 14 , dead : false } } ] ; } } , solidObstacleRects ( ) { { return [ ] ; } } } } ;
if ( placementContext . TarinaiCollisionFootprints . itemPlacementOverlapBlocked ( stackedWorld , { { type : ' zunchi ' , x : 0 , y : 0 , r : 14 , dead : false } } ) ) throw new Error ( ' movable items still block movable placement ' ) ;
const toolContext = { { console , Math , Object , globalThis : null , window : null } } ;
toolContext . globalThis = toolContext ; toolContext . window = toolContext ;
toolContext . World = class World { { constructor ( ) { { this . items = [ { { id : ' i1 ' , type : ' ball ' , x : 10 , y : 10 , r : 18 , dead : false } } ] ; } } nearbyItems ( ) { { return this . items ; } } compactItems ( ) { { } } updateItemCounts ( ) { { } } rebuildSpatial ( ) { { } } log ( ) { { } } } } ;
toolContext . distXY = ( ax , ay , bx , by ) = > Math . hypot ( ax - bx , ay - by ) ;
toolContext . TarinaiCollisionFootprints = { { hitTestItem ( _world , item , x , y ) { { return { { hit : Math . hypot ( item . x - x , item . y - y ) < 30 , distance : Math . hypot ( item . x - x , item . y - y ) } } ; } } } } ;
toolContext . TarinaiGeometry = { { pointSegmentDistance ( ) { { return 999 ; } } } } ;
toolContext . TarinaiStructureLifecycle = { { deleteItem ( _world , item ) { { item . dead = true ; return true ; } } } } ;
toolContext . showToast = ( ) = > { { } } ;
toolContext . toolLabel = type = > type ;
toolContext . isPinType = ( ) = > false ;
toolContext . itemRadiusFor = ( ) = > 18 ;
toolContext . audio = { { delete ( ) { { } } } } ;
toolContext . render = ( ) = > { { } } ;
vm . createContext ( toolContext ) ;
vm . runInContext ( { tools ! r } , toolContext , { { filename : ' world_tool_actions.js ' } } ) ;
const deleteWorld = new toolContext . World ( ) ;
const found = deleteWorld . findDeleteToolTargetAt ( 10 , 10 ) ;
if ( ! found | | found . type != = ' ball ' ) throw new Error ( ' delete tool target lookup failed ' ) ;
deleteWorld . deleteItemAt ( 10 , 10 ) ;
if ( ! deleteWorld . items [ 0 ] . dead ) throw new Error ( ' delete tool did not delete target item ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " war/fence/sleep/placement/delete runtime smoke test failed " )
ok ( " war bounce fences, modest ordinary restitution, sleeping ball collision, closer nest tooltip, movable stacking, and delete tool verified " )
def check_hair_trigger_tooltips_and_physics_icons ( ) - > None :
def contains_local ( text : str , needle : str , message : str ) - > None :
if needle not in text :
fail ( message )
render = read_text ( ROOT / " js/render.js " )
presets = read_text ( ROOT / " js/world_reset_presets.js " )
layout = read_text ( ROOT / " js/ui_layout_dialogs.js " )
tools = read_text ( ROOT / " js/ui_tools.js " )
index = read_text ( ROOT / " index.html " )
contains_local ( render , ' const targetGapScale = 1.45; ' , " all world tooltips were not moved to the nest-box offset " )
if ' info?.kind === " nest_box " ? ' in render :
fail ( " nest-box-only tooltip offset remains " )
contains_local ( presets , ' hair_trigger: { label: " \\ u4E00 \\ u89E6 \\ u5373 \\ u767A " } ' , " hair-trigger preset definition missing " )
contains_local ( presets , ' function addPresetBounceFenceBox ' , " hair-trigger bounce enclosure helper missing " )
contains_local ( presets , ' storedFoodType: " food " ' , " hair-trigger duplicators are not loaded with pet food " )
contains_local ( layout , ' " hair_trigger " ' , " field dialog preset fallback omits hair-trigger preset " )
contains_local ( index , ' data-reset-preset= " hair_trigger " ' , " hair-trigger preset button missing " )
contains_local ( tools , ' function drawPhysicsToolIcon(canvas, toolId) ' , " dedicated physics icon renderer missing " )
2026-07-16 22:12:03 +09:00
contains_local ( tools , ' const hasPhysicsIcon = attachPhysicsToolIcon(btn, toolId); ' , " dedicated item silhouettes are not attached across categories " )
2026-07-11 21:13:39 +09:00
for tool_id in [ " rope " , " rod " , " spring " , " wire " , " insulated_wire " , " pressure_switch " , " glass_wall " , " bounce_fence " , " gate_fence " , " rotator " , " poison_block " , " reciprocator " , " one_way_fence " , " fence_h " ] :
contains_local ( tools , f ' toolId === " { tool_id } " ' , f " distinct physics icon branch missing: { tool_id } " )
js = f """
const fs = require ( ' fs ' ) ;
const vm = require ( ' vm ' ) ;
const source = { presets ! r } ;
let idCounter = 0 ;
const ctx = { { console , Math , Object , globalThis : null , window : null } } ;
ctx . globalThis = ctx ; ctx . window = ctx ;
ctx . CONFIG = { { initialPopulation : 16 } } ;
ctx . clamp = ( v , a , b ) = > Math . max ( a , Math . min ( b , v ) ) ;
ctx . rand = ( a , b ) = > ( Number ( a ) + Number ( b ) ) / 2 ;
ctx . Item = class Item { { constructor ( type , x , y ) { { this . id = ` i $ { { + + idCounter } } ` ; this . type = type ; this . x = x ; this . y = y ; this . r = type == = ' bounce_fence ' ? 42 : type == = ' ball ' ? 18 : 22 ; this . amount = 999 ; this . roles = { { } } ; } } } } ;
ctx . toolLabel = id = > id ;
ctx . TarinaiGrass = { { setStage ( ) { { } } } } ;
ctx . TarinaiPhysicsBodySystem = { { invalidateItem ( ) { { } } } } ;
ctx . TarinaiMechanicalSystem = { { invalidateGeometry ( ) { { } } , reach ( ) { { return 0 ; } } } } ;
vm . createContext ( ctx ) ;
vm . runInContext ( source , ctx , { { filename : ' world_reset_presets.js ' } } ) ;
const world = { {
w : 1000 , h : 700 , items : [ ] , tarinai : [ ] ,
addItem ( item ) { { this . items . push ( item ) ; return item ; } } ,
addTarinai ( data ) { { const t = { { . . . data , id : ` t $ { { this . tarinai . length + 1 } } ` } } ; this . tarinai . push ( t ) ; return t ; } } ,
nearbyItems ( ) { { return [ ] ; } } ,
placementClampPointFor ( _item , x , y ) { { return { { x , y } } ; } } ,
placementBlocked ( ) { { return false ; } } ,
isFenceType ( type ) { { return type . includes ( ' fence ' ) ; } } ,
} } ;
ctx . TarinaiResetPresets . generatePresetWorld ( world , { { id : ' garden ' } } , ' hair_trigger ' , 16 ) ;
const cx = world . w * 0.5 , cy = world . h * 0.5 ;
const halfW = Math . max ( 132 , Math . min ( 238 , world . w * 0.18 ) ) ;
const halfH = Math . max ( 104 , Math . min ( 182 , world . h * 0.18 ) ) ;
const fences = world . items . filter ( item = > item . type == = ' bounce_fence ' ) ;
const balls = world . items . filter ( item = > item . type == = ' ball ' ) ;
const duplicators = world . items . filter ( item = > item . type == = ' duplicator ' ) ;
if ( fences . length < 8 ) throw new Error ( ' hair-trigger enclosure has too few bounce fences ' ) ;
if ( balls . length < 4 | | ! balls . every ( item = > Math . abs ( item . x - cx ) < halfW & & Math . abs ( item . y - cy ) < halfH ) ) throw new Error ( ' hair-trigger balls are not inside enclosure ' ) ;
if ( ! world . tarinai . length | | ! world . tarinai . every ( t = > Math . abs ( t . x - cx ) > halfW + 20 | | Math . abs ( t . y - cy ) > halfH + 20 ) ) throw new Error ( ' hair-trigger Tarinai are not outside enclosure ' ) ;
if ( ! duplicators . length | | ! duplicators . every ( item = > item . storedFoodType == = ' food ' & & ( Math . abs ( item . x - cx ) > halfW + 20 | | Math . abs ( item . y - cy ) > halfH + 20 ) ) ) throw new Error ( ' hair-trigger pet-food duplicators are not outside enclosure ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " hair-trigger preset runtime smoke test failed " )
ok ( " uniform tooltip spacing, hair-trigger preset, and distinct physics icons verified " )
2026-06-24 13:48:22 +09:00
def main ( ) - > None :
data = manifest ( )
check_files_exist ( data )
2026-06-25 14:51:58 +09:00
check_unified_item_registry ( )
check_docs_removed ( )
2026-06-24 13:48:22 +09:00
check_versions ( data )
check_index_order ( data )
check_service_worker_order ( data )
node_check ( [ * ( ROOT / " js " ) . glob ( " *.js " ) , ROOT / " service-worker.js " ] )
action_spec_smoke ( )
behavior_state_smoke ( )
2026-07-09 16:08:11 +09:00
need_action_runtime_bridge_smoke ( )
mechanical_collision_and_editor_smoke ( )
physics_tuning_smoke ( )
2026-07-10 16:40:06 +09:00
champion_inheritance_smoke ( )
one_way_routing_smoke ( )
2026-07-09 16:08:11 +09:00
colony_limits_and_defaults_smoke ( )
2026-06-25 14:51:58 +09:00
check_basic_action_spec_runtime ( )
check_social_action_spec_runtime ( )
check_action_spec_no_legacy_hooks ( )
2026-06-24 13:48:22 +09:00
check_runtime_diagnostics_hooks ( )
check_social_balance_guards ( )
check_item_bucket_polish ( )
2026-06-25 14:51:58 +09:00
check_current_save_format ( )
2026-07-10 16:40:06 +09:00
check_save_stream_deadlock_guard ( )
2026-06-25 14:51:58 +09:00
check_phase_and_input_split ( )
2026-06-26 12:46:32 +09:00
check_system_order_boundary ( )
simulation_boundary_smoke ( )
check_entity_runtime_boundary ( )
check_item_lifecycle_pipeline_boundary ( )
check_tarinai_update_pipeline_boundary ( )
check_update_policy_boundaries ( )
2026-06-25 14:51:58 +09:00
check_processing_split ( )
2026-06-26 12:46:32 +09:00
command_dispatcher_smoke ( )
2026-06-24 13:48:22 +09:00
check_legacy_behavior_refs ( )
2026-06-27 19:22:38 +09:00
check_legacy_physics_props_removed ( )
2026-07-10 16:40:06 +09:00
check_redundant_correction_paths_removed ( )
2026-07-11 21:13:39 +09:00
check_collision_reason_and_projection_dirty_removed ( )
direct_feeding_smoke ( )
check_electrocution_gate_chart_and_ascii ( )
check_connection_overlay_and_time_detector ( )
check_link_dependency_and_plushie_recovery ( )
check_splat_link_target_and_appetite_label ( )
check_habitat_food_medicine_additions ( )
check_parasol_rain_and_preset_updates ( )
check_war_fence_sleep_placement_delete_fixes ( )
check_hair_trigger_tooltips_and_physics_icons ( )
2026-06-24 13:48:22 +09:00
if __name__ == " __main__ " :
main ( )