feat: Random.org dice and daily word matrix, interpreter voices, curated lexicon

Wire cleromancy rolls and the once-per-UTC-day 000-999 word shuffle to RANDOM.ORG JSON-RPC, with a local CSPRNG fallback. Add settings for interpreter personas and a cleaner default oracle prompt. Replace proper names and bureaucratic filler in the 1000-word lexicon with sacred symbols.
This commit is contained in:
2026-09-08 13:46:11 +05:00
parent fbf04d90db
commit 7f91366201
26 changed files with 8904 additions and 7077 deletions
+82 -32
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../services/true_random_service.dart';
import 'gradient_star_badge.dart';
class D10DiceRollerWidget extends StatefulWidget {
@@ -23,63 +24,108 @@ class D10DiceRollerWidget extends StatefulWidget {
}
class D10DiceRollerWidgetState extends State<D10DiceRollerWidget> {
final _random = Random();
final _flicker = Random.secure();
Timer? _rollTimer;
int? _animatedDigit0;
int? _animatedDigit1;
int? _animatedDigit2;
bool _internalRolling = false;
RandomSource? _lastSource;
void triggerRollAnimation() {
@override
void initState() {
super.initState();
TrueRandomService.instance.prefetch();
}
Future<void> triggerRollAnimation() async {
if (_internalRolling) return;
setState(() {
_internalRolling = true;
_animatedDigit0 = _random.nextInt(10);
_animatedDigit1 = _random.nextInt(10);
_animatedDigit2 = _random.nextInt(10);
_animatedDigit0 = _flicker.nextInt(10);
_animatedDigit1 = _flicker.nextInt(10);
_animatedDigit2 = _flicker.nextInt(10);
});
final target0 = _random.nextInt(10);
final target1 = _random.nextInt(10);
final target2 = _random.nextInt(10);
// Local CSPRNG is the fallback. RANDOM.ORG replaces all three faces
// together if it arrives before the first slot locks (up to ~960ms).
var target0 = _flicker.nextInt(10);
var target1 = _flicker.nextInt(10);
var target2 = _flicker.nextInt(10);
var source = RandomSource.local;
DiceRoll? remoteRoll;
int ticks = 0;
unawaited(
TrueRandomService.instance.take(3).then((roll) {
remoteRoll = roll;
}, onError: (Object _, StackTrace _) {}),
);
var locked0 = false;
var locked1 = false;
var locked2 = false;
var lock0At = 10;
var lock1At = 18;
var lock2At = 26;
var ticks = 0;
const interval = Duration(milliseconds: 60);
_rollTimer?.cancel();
_rollTimer = Timer.periodic(interval, (timer) {
if (!mounted) {
timer.cancel();
return;
}
ticks++;
// Slot 0 stops after 10 ticks (~600ms)
if (ticks < 10) {
setState(() => _animatedDigit0 = _random.nextInt(10));
} else if (ticks == 10) {
setState(() => _animatedDigit0 = target0);
HapticFeedback.mediumImpact();
if (!locked0) {
final ready = remoteRoll != null && remoteRoll!.digits.length == 3;
if (ticks >= 10 && (ready || ticks >= 16)) {
if (ready) {
target0 = remoteRoll!.digits[0];
target1 = remoteRoll!.digits[1];
target2 = remoteRoll!.digits[2];
source = remoteRoll!.source;
}
locked0 = true;
lock0At = ticks;
lock1At = lock0At + 8;
lock2At = lock1At + 8;
setState(() => _animatedDigit0 = target0);
HapticFeedback.mediumImpact();
} else {
setState(() => _animatedDigit0 = _flicker.nextInt(10));
}
}
// Slot 1 stops after 18 ticks (~1080ms)
if (ticks < 18) {
setState(() => _animatedDigit1 = _random.nextInt(10));
} else if (ticks == 18) {
setState(() => _animatedDigit1 = target1);
HapticFeedback.mediumImpact();
if (!locked1) {
if (locked0 && ticks >= lock1At) {
locked1 = true;
setState(() => _animatedDigit1 = target1);
HapticFeedback.mediumImpact();
} else {
setState(() => _animatedDigit1 = _flicker.nextInt(10));
}
}
// Slot 2 stops after 26 ticks (~1560ms)
if (ticks < 26) {
setState(() => _animatedDigit2 = _random.nextInt(10));
} else if (ticks == 26) {
setState(() => _animatedDigit2 = target2);
HapticFeedback.heavyImpact();
if (!locked2) {
if (locked1 && ticks >= lock2At) {
locked2 = true;
setState(() => _animatedDigit2 = target2);
HapticFeedback.heavyImpact();
} else {
setState(() => _animatedDigit2 = _flicker.nextInt(10));
}
}
if (ticks >= 28) {
if (locked2 && ticks >= lock2At + 2) {
timer.cancel();
setState(() => _internalRolling = false);
final finalNumber = target0 * 100 + target1 * 10 + target2;
widget.onRollCompleted(finalNumber);
setState(() {
_internalRolling = false;
_lastSource = source;
});
widget.onRollCompleted(target0 * 100 + target1 * 10 + target2);
}
});
}
@@ -141,7 +187,11 @@ class D10DiceRollerWidgetState extends State<D10DiceRollerWidget> {
Text(
_internalRolling
? 'Бросок...'
: (widget.digits.length == 3 ? 'Зафиксировано' : '3 цифры'),
: (widget.digits.length == 3
? (_lastSource == RandomSource.randomOrg
? 'RANDOM.ORG'
: 'Зафиксировано')
: '3 цифры'),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,