Files

127 lines
6.1 KiB
Dart

import 'room_page.dart';
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() => runApp(const GlitchLab());
class GlitchLab extends StatelessWidget {
const GlitchLab({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Glitch GM Lab',
theme: ThemeData.dark(useMaterial3: true).copyWith(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.cyan, brightness: Brightness.dark)),
home: Uri.base.queryParameters['room'] == '1' ? const RoomPage() : const CabinetPage(),
);
}
class CabinetPage extends StatefulWidget {
const CabinetPage({super.key, this.client});
final http.Client? client;
@override
State<CabinetPage> createState() => _CabinetPageState();
}
class _CabinetPageState extends State<CabinetPage> {
late final http.Client client;
Timer? timer;
Map<String, dynamic>? cabinet;
String? error;
String notice = 'Connecting to the local simulation...';
bool pending = false;
bool polling = false;
int sequence = 0;
int revision = 0;
static const base = 'http://127.0.0.1:18766';
@override
void initState() {
super.initState();
client = widget.client ?? http.Client();
refresh();
timer = Timer.periodic(const Duration(seconds: 1), (_) => refresh());
}
Future<void> refresh() async {
if (polling || pending) return;
polling = true;
final requestRevision = revision;
try {
final response = await client.get(Uri.parse('$base/state')).timeout(const Duration(seconds: 3));
if (response.statusCode != 200) throw Exception('Cabinet or controller unavailable');
final data = jsonDecode(response.body) as Map<String, dynamic>;
if (mounted && !pending && requestRevision == revision) setState(() { cabinet = data['state']; error = null; });
} catch (_) {
if (mounted && requestRevision == revision) setState(() => error = 'Disconnected - controls disabled. Check the lab terminals.');
} finally { polling = false; }
}
Future<void> command(String action) async {
if (pending || error != null || cabinet == null) return;
revision++;
setState(() { pending = true; notice = 'Waiting for cabinet acknowledgement...'; });
try {
final response = await client.post(Uri.parse('$base/command'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'id': 'gm-${DateTime.now().microsecondsSinceEpoch}-${sequence++}',
'session': cabinet!['session'],
'expires_ms': DateTime.now().millisecondsSinceEpoch + 10000,
'action': action,
})).timeout(const Duration(seconds: 4));
if (response.statusCode != 200) throw Exception('No acknowledgement');
final data = jsonDecode(response.body) as Map<String, dynamic>;
if (mounted) setState(() {
cabinet = data['state'];
notice = '${data['accepted'] == true ? 'Accepted' : 'Rejected'}: ${data['message']}';
error = null;
});
} catch (_) {
if (mounted) setState(() {
error = 'Delivery unconfirmed. Refresh state before trying again.';
notice = 'No success acknowledgement received.';
});
} finally { if (mounted) setState(() => pending = false); }
}
@override
void dispose() { timer?.cancel(); client.close(); super.dispose(); }
@override
Widget build(BuildContext context) {
final phase = cabinet?['phase'] ?? 'unknown';
final available = cabinet != null && error == null && !pending;
Widget control(String label, String action, bool enabled) => FilledButton(
onPressed: available && enabled ? () => command(action) : null, child: Text(label));
return Scaffold(
appBar: AppBar(title: const Text('Escape Gallery / Glitch: the Dark Machine')),
body: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 900),
child: ListView(padding: const EdgeInsets.all(24), children: [
Text(cabinet?['simulated'] == false ? 'INTEGRATION LAB - REAL GODOT CABINET' : 'INTEGRATION LAB - SIMULATED CABINET', style: const TextStyle(color: Colors.amber)),
const SizedBox(height: 16),
const Text('Cabinet 1 / Pac-Man', style: TextStyle(fontSize: 28)),
Text(cabinet?['simulated'] == false ? 'Real Pac-Man. Target: 500 points. No reward outputs. RFID injection: ${cabinet?['rfid_injection'] == true ? 'enabled' : 'disabled'}.' : 'No physical RFID, LEDs, GPIO, or arcade game is connected.'),
const SizedBox(height: 24),
Card(child: Padding(padding: const EdgeInsets.all(24), child: Column(
crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(phase.replaceAll('_', ' ').toUpperCase(), style: const TextStyle(fontSize: 32)),
Text('Credits: ${cabinet?['credits'] ?? '-'} Accepted scans: ${cabinet?['scans'] ?? '-'}'),
const SizedBox(height: 12),
Text(phase == 'powered_off' ? 'Waiting for operator' :
phase == 'booting' ? 'Four-second boot sequence...' :
phase == 'ready' ? 'Scan a card, then press Start' :
phase == 'playing' ? 'Game in progress' :
phase == 'won' ? 'Objective completed' : phase == 'lost' ? 'Game lost - reset the test to retry' : 'Awaiting state'),
]))),
const SizedBox(height: 16),
Wrap(spacing: 12, runSpacing: 12, children: [
control('Power on', 'power_on', phase == 'powered_off'),
control('Simulate RFID scan', 'scan', phase == 'ready' && (cabinet?['simulated'] == true || cabinet?['rfid_injection'] == true)),
control('Start game', 'start', phase == 'ready' && cabinet?['credits'] == 1),
control('Simulate win', 'win', phase == 'playing' && cabinet?['simulated'] == true),
control('Reset test', 'reset', true),
]),
const SizedBox(height: 24),
Text(error ?? notice, style: TextStyle(color: error == null ? Colors.white70 : Colors.orange)),
const SizedBox(height: 16),
const Text('Flutter → local JSON gateway → NATS / MessagePack → simulated cabinet'),
]))),
);
}
}