Added chatterbox connections

This commit is contained in:
Jeff Luke committed 2026-10-18 10:54:38 -07:00
1 parent 41ef12f954
commit e58ceeb192
58 files changed
+8931 -8

No files matched your search

+1
View File
@@ -0,0 +1 @@
{"state":{"active_ms":10000,"armed":[],"assisted":false,"boss":"DARK","cell_faults":["SOCKET01 needs CELL01","CELL01 is not charged","SOCKET02 needs CELL02","CELL02 is not charged","SOCKET03 needs CELL03","CELL03 is not charged","SOCKET04 needs CELL04","CELL04 is not charged","SOCKET05 needs CELL05","CELL05 is not charged","SOCKET06 needs CELL06","CELL06 is not charged"],"config_hash":"38b54e5bdcdc9df3b9852cf204bcb5b1973018d94bc4ad04565b7bb959f68d20","countdown":null,"effects":[{"id":"sim-1792303569839-next:token.dispense:T01","kind":"token.dispense","status":"OBSERVED","target":"T01"}],"groups":[],"hold":"","last_progress":0,"launch_epoch":0,"launches":0,"lifecycle":"RUNNING","limit_ms":3600000,"location_since":{},"locations":{"BAY01":"CELL01","BAY02":"CELL02","BAY03":"CELL03","BAY04":"CELL04","BAY05":"CELL05","BAY06":"CELL06"},"operators":5,"outcome":null,"party":6,"passage_open":false,"passage_since":null,"phase":"ARCADE","power_since":null,"profile":"standard","puzzles":[{"delivered":false,"id":"P01","solved":true,"token":""},{"delivered":true,"id":"P02A","solved":true,"token":"T01"},{"delivered":false,"id":"P02B","solved":false,"token":"T02"},{"delivered":false,"id":"P03","solved":false,"token":"T03"},{"delivered":false,"id":"P04","solved":false,"token":"T04"},{"delivered":false,"id":"P05","solved":false,"token":"T05"},{"delivered":false,"id":"P06","solved":false,"token":"T06"}],"ready_deadline":null,"reveal_at":0,"revision":100,"run_id":"sim-1792303569839-next","seed":42,"ship_power":false,"simulation":true,"stations":[{"attempt":1,"authorized":true,"bay_open":false,"cell":"CELL01","charge_ms":10000,"charging":true,"game":"pacman","id":"cabinet_01","last_target":null,"losses":0,"online":true,"passes":0,"progress":0,"retry_at":0,"socket":"SOCKET01","status":"PLAYING","target":500,"token":"T01","won":false},{"attempt":0,"authorized":false,"bay_open":false,"cell":"CELL02","charge_ms":0,"charging":false,"game":"galaga","id":"cabinet_02","last_target":null,"losses":0,"online":true,"passes":0,"progress":0,"retry_at":0,"socket":"SOCKET02","status":"TOKEN_WAIT","target":500,"token":"T02","won":false},{"attempt":0,"authorized":false,"bay_open":false,"cell":"CELL03","charge_ms":0,"charging":false,"game":"tempest","id":"cabinet_03","last_target":null,"losses":0,"online":true,"passes":0,"progress":0,"retry_at":0,"socket":"SOCKET03","status":"TOKEN_WAIT","target":500,"token":"T03","won":false},{"attempt":0,"authorized":false,"bay_open":false,"cell":"CELL04","charge_ms":0,"charging":false,"game":"missile_command","id":"cabinet_04","last_target":null,"losses":0,"online":true,"passes":0,"progress":0,"retry_at":0,"socket":"SOCKET04","status":"TOKEN_WAIT","target":500,"token":"T04","won":false},{"attempt":0,"authorized":false,"bay_open":false,"cell":"CELL05","charge_ms":0,"charging":false,"game":"frogger","id":"cabinet_05","last_target":null,"losses":0,"online":true,"passes":0,"progress":0,"retry_at":0,"socket":"SOCKET05","status":"TOKEN_WAIT","target":1,"token":"T05","won":false},{"attempt":0,"authorized":false,"bay_open":false,"cell":"CELL06","charge_ms":0,"charging":false,"game":"space_invaders","id":"cabinet_06","last_target":null,"losses":0,"online":true,"passes":0,"progress":0,"retry_at":0,"socket":"SOCKET06","status":"TOKEN_WAIT","target":500,"token":"T06","won":false}],"teddy":{"current":null,"hint_level":0,"mode":"GUIDE","muted":false,"next_voice_at":0,"online":true,"played":["welcome"],"queue":[],"takeover":false,"transcript":"I remember this place full of people. The machines remember too. Find their tokens. Each one belongs to only one machine."},"tutorial":0,"wall_ms":10000,"wave":null,"wave_serial":0}}
@@ -0,0 +1,35 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:glitch_gm_lab/hint_panel.dart';
void main() {
test('suggestions follow parallel branches and claw prerequisites', () {
final room=<String,dynamic>{'phase':'ARCADE','puzzles':[
{'id':'P01','solved':false,'delivered':false},
{'id':'P02A','solved':false,'delivered':false},
{'id':'P02B','solved':false,'delivered':false},
{'id':'P03','solved':false,'delivered':false},
{'id':'P04','solved':false,'delivered':false},
]};
Map<String,dynamic> hint(String target) => {'phase':'ARCADE','target':target,'condition':'incomplete'};
expect(hintReason(hint('P01'),room),isNotNull);
expect(hintReason(hint('P03'),room),isNotNull);
expect(hintReason(hint('P04'),room),isNotNull);
expect(hintReason(hint('P02A'),room),isNull);
(room['puzzles'] as List)[0]['solved']=true;
expect(hintReason(hint('P01'),room),isNull);
expect(hintReason(hint('P02A'),room),isNotNull);
expect(hintReason(hint('P02B'),room),isNull);
(room['puzzles'] as List)[1]['delivered']=true;
expect(hintReason(hint('P02B'),room),isNotNull);
room['phase']='CELLS';
expect(hintReason(hint('P03'),room),isNull);
expect(hintReason({'phase':'ALL','target':'room','condition':'any'},room),isNotNull);
});
test('cabinet first wins remove incomplete hints', () {
final room=<String,dynamic>{'phase':'ARCADE','stations':[{'id':'cabinet_01','won':false}]};
final hint=<String,dynamic>{'phase':'ALL','target':'cabinet_01','condition':'incomplete'};
expect(hintReason(hint,room),isNotNull);
(room['stations'] as List)[0]['won']=true;
expect(hintReason(hint,room),isNull);
});
}
+68
View File
@@ -0,0 +1,68 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:glitch_gm_lab/room_page.dart';
void main() {
for (final width in [390.0, 900.0, 1400.0]) {
testWidgets('room panels fit at width $width and use authoritative revision', (tester) async {
WidgetController.hitTestWarningShouldBeFatal = true;
addTearDown(() => WidgetController.hitTestWarningShouldBeFatal = false);
tester.view.devicePixelRatio = 1;
tester.view.physicalSize = Size(width, 1000);
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final data = jsonDecode(File('test/fixtures/room.json').readAsStringSync());
final state = data['state'] as Map<String, dynamic>;
Map<String, dynamic>? sent;
final client = MockClient((request) async {
if (request.method == 'POST') {
sent = jsonDecode(request.body);
return http.Response(jsonEncode({'accepted':true,'message':'test accepted','state':state}), 200);
}
if (request.url.path.endsWith('trace')) {
return http.Response(jsonEncode({'trace':[],'final_hash':'fixture'}),200);
}
return http.Response(jsonEncode({'state':state}),200);
});
await tester.pumpWidget(MaterialApp(home:RoomPage(client:client)));
await tester.pump();
expect(find.textContaining('Connected. The room timer'), findsOneWidget);
expect(tester.takeException(), isNull);
Future<void> select(String name) async {
if (width < 900) { await tester.tap(find.byTooltip('Open navigation menu')); await tester.pumpAndSettle(); }
await tester.ensureVisible(find.text(name)); await tester.tap(find.text(name)); await tester.pumpAndSettle();
}
await select('Diagnostics');
await tester.ensureVisible(find.text('+ 1 second'));
await tester.tap(find.text('+ 1 second'));
await tester.pump();
expect(sent?['expected_revision'], state['revision']);
expect(sent?['run_id'], state['run_id']);
for (final tab in ['Cabinets','Ship & Cells','Calibration & Launch','Teddy','Diagnostics','Room Flow']) {
await select(tab);
expect(tester.takeException(), isNull, reason:tab);
}
state['phase']='CALIBRATION';
state['wave']={'id':99,'targets':{'cabinet_01':2},'ready':{},'deadline':40000,'retry_at':0};
await tester.pump(const Duration(seconds:1));
await tester.pump();
await select('Calibration & Launch');
expect(find.text('Simulate held Ready'), findsOneWidget);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox());
});
}
testWidgets('disconnected room disables mutating controls', (tester) async {
await tester.pumpWidget(MaterialApp(home:RoomPage(client:MockClient((_) async => http.Response('{}',503)))));
await tester.pump();
expect(find.textContaining('disconnected'),findsOneWidget);
for (final button in tester.widgetList<OutlinedButton>(find.byType(OutlinedButton))) {
expect(button.onPressed,isNull);
}
await tester.pumpWidget(const SizedBox());
});
}
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:glitch_gm_lab/main.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
void main() {
testWidgets('commands remain disabled without cabinet state', (tester) async {
final client = MockClient((_) async => http.Response('{}', 503));
await tester.pumpWidget(MaterialApp(home: CabinetPage(client: client)));
await tester.pump();
expect(find.text('INTEGRATION LAB - SIMULATED CABINET'), findsOneWidget);
expect(find.text('Power on'), findsOneWidget);
for (final button in tester.widgetList<FilledButton>(find.byType(FilledButton))) {
expect(button.onPressed, isNull);
}
expect(find.textContaining('Disconnected'), findsOneWidget);
await tester.pumpWidget(const SizedBox());
});
}