412 lines
14 KiB
Dart
412 lines
14 KiB
Dart
import 'dart:math';
|
|
import 'package:flutter/material.dart';
|
|
import '../models/houses_session_model.dart';
|
|
import '../models/word_model.dart';
|
|
import 'words_repository.dart';
|
|
|
|
class HousesService {
|
|
static final HousesService _instance = HousesService._internal();
|
|
factory HousesService() => _instance;
|
|
HousesService._internal();
|
|
|
|
static const List<Color> sectorColors = [
|
|
Color(0xFFFFB300), // 0: Золото Солнца
|
|
Color(0xFFE53935), // 1: Рубиновое Пламя
|
|
Color(0xFF1E88E5), // 2: Лазурный Эфир
|
|
Color(0xFF43A047), // 3: Изумрудная Чаща
|
|
Color(0xFF8E24AA), // 4: Аметистовая Тайна
|
|
Color(0xFF00ACC1), // 5: Бирюзовая Бездна
|
|
Color(0xFFFF7043), // 6: Коралловый Горн
|
|
Color(0xFF90A4AE), // 7: Лунное Серебро
|
|
Color(0xFF5C6BC0), // 8: Индиго Сумерек
|
|
Color(0xFF8D6E63), // 9: Бронзовая Твердь
|
|
];
|
|
|
|
static const List<String> sectorTitles = [
|
|
'I. Сектор Золотого Сияния',
|
|
'II. Сектор Алого Огня',
|
|
'III. Сектор Небесных Вод',
|
|
'IV. Сектор Вечного Роста',
|
|
'V. Сектор Глубинных Тайн',
|
|
'VI. Сектор Предельного Взора',
|
|
'VII. Сектор Творящей Воли',
|
|
'VIII. Сектор Зеркальной Памяти',
|
|
'IX. Сектор Полночного Суда',
|
|
'X. Сектор Земного Основания',
|
|
];
|
|
|
|
/// Generates a freshly shuffled 33x30 field (990 houses + 10 Archons).
|
|
/// If [querentWord] is given, guarantees that the querent resides in one of the 990 houses
|
|
/// and dynamically calculates [querentHouseIndex] to point to that exact house.
|
|
HousesSession createNewSession({String? question, WordItem? querentWord, int? querentHouseIndex}) {
|
|
final repo = WordsRepository();
|
|
final allWords = List<WordItem>.from(repo.allWords);
|
|
|
|
// Cryptographic shuffle for pristine randomness
|
|
final rnd = Random.secure();
|
|
allWords.shuffle(rnd);
|
|
|
|
bool isNoun(WordItem w) {
|
|
final pos = w.partOfSpeech.toLowerCase().trim();
|
|
if (pos != 'существительное') return false;
|
|
final cat = w.category.toLowerCase();
|
|
if (cat.contains('действие') || w.operator == 'shifter') {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// 1. Ensure Archons (indexes 0..9) are strictly nouns (существительное)
|
|
for (int i = 0; i < 10; i++) {
|
|
if (!isNoun(allWords[i])) {
|
|
final swapIdx = allWords.indexWhere((w) => isNoun(w), 10);
|
|
if (swapIdx != -1) {
|
|
final temp = allWords[i];
|
|
allWords[i] = allWords[swapIdx];
|
|
allWords[swapIdx] = temp;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. If querentWord is provided, ensure it is in the Houses pool (indexes 10..999),
|
|
// not in the Archons (indexes 0..9), so Querent always has a physical House.
|
|
if (querentWord != null) {
|
|
final qLower = querentWord.word.toLowerCase().trim();
|
|
if (querentHouseIndex != null && querentHouseIndex >= 0 && querentHouseIndex < 990) {
|
|
final qIdx = allWords.indexWhere(
|
|
(w) => w.id == querentWord.id || w.word.toLowerCase().trim() == qLower,
|
|
);
|
|
final targetIdx = 10 + querentHouseIndex;
|
|
if (qIdx >= 0 && qIdx != targetIdx) {
|
|
final temp = allWords[targetIdx];
|
|
allWords[targetIdx] = allWords[qIdx];
|
|
allWords[qIdx] = temp;
|
|
// If the swap displaced a noun from archon, re-verify archon is a noun
|
|
if (qIdx < 10 && !isNoun(allWords[qIdx])) {
|
|
final nounIdx = allWords.indexWhere(
|
|
(w) => isNoun(w) &&
|
|
w.id != querentWord.id &&
|
|
w.word.toLowerCase().trim() != qLower,
|
|
10,
|
|
);
|
|
if (nounIdx != -1) {
|
|
final t2 = allWords[qIdx];
|
|
allWords[qIdx] = allWords[nounIdx];
|
|
allWords[nounIdx] = t2;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
final archonMatchIdx = allWords.sublist(0, 10).indexWhere(
|
|
(w) => w.id == querentWord.id || w.word.toLowerCase().trim() == qLower,
|
|
);
|
|
if (archonMatchIdx >= 0) {
|
|
// Swap with a noun from the houses pool
|
|
final swapTarget = allWords.indexWhere(
|
|
(w) => isNoun(w),
|
|
10,
|
|
);
|
|
final validTarget = swapTarget != -1 ? swapTarget : 10 + rnd.nextInt(990);
|
|
final temp = allWords[archonMatchIdx];
|
|
allWords[archonMatchIdx] = allWords[validTarget];
|
|
allWords[validTarget] = temp;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Final absolute guarantee: strictly ensure every Archon (0..9) is a noun
|
|
for (int i = 0; i < 10; i++) {
|
|
if (!isNoun(allWords[i])) {
|
|
final swapIdx = allWords.indexWhere((w) => isNoun(w), 10);
|
|
if (swapIdx != -1) {
|
|
final temp = allWords[i];
|
|
allWords[i] = allWords[swapIdx];
|
|
allWords[swapIdx] = temp;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1. First 10 words -> 10 Archons ruling the 10 column triads
|
|
final sectors = <ArchonSector>[];
|
|
for (int i = 0; i < 10; i++) {
|
|
sectors.add(
|
|
ArchonSector(
|
|
index: i,
|
|
archonWord: allWords[i],
|
|
startCol: i * 3,
|
|
endCol: i * 3 + 2,
|
|
sectorColor: sectorColors[i],
|
|
title: sectorTitles[i],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 2. Remaining 990 words -> 33 rows x 30 columns
|
|
final grid = <HouseCell>[];
|
|
final housesPool = allWords.sublist(10, 1000); // exactly 990 words
|
|
int? resolvedQuerentIndex;
|
|
|
|
for (int i = 0; i < housesPool.length; i++) {
|
|
final row = i ~/ 30; // 0..32
|
|
final col = i % 30; // 0..29
|
|
final sectorIndex = col ~/ 3; // 0..9
|
|
final houseWord = housesPool[i];
|
|
|
|
if (querentWord != null &&
|
|
resolvedQuerentIndex == null &&
|
|
(houseWord.id == querentWord.id ||
|
|
houseWord.word.toLowerCase().trim() == querentWord.word.toLowerCase().trim())) {
|
|
resolvedQuerentIndex = i;
|
|
}
|
|
|
|
grid.add(
|
|
HouseCell(
|
|
row: row,
|
|
col: col,
|
|
houseIndex: i,
|
|
houseWord: houseWord,
|
|
sectorIndex: sectorIndex,
|
|
),
|
|
);
|
|
}
|
|
|
|
final effectiveQuerentIndex = querentHouseIndex ?? resolvedQuerentIndex;
|
|
final id = 'houses_${DateTime.now().millisecondsSinceEpoch}_${rnd.nextInt(10000)}';
|
|
|
|
return HousesSession(
|
|
id: id,
|
|
createdAt: DateTime.now(),
|
|
question: question,
|
|
querentWord: querentWord,
|
|
querentHouseIndex: effectiveQuerentIndex,
|
|
sectors: sectors,
|
|
grid: grid,
|
|
selectedHouses: const [],
|
|
chains: const [],
|
|
);
|
|
}
|
|
|
|
/// Casts guest symbols into the selected houses (1..9) and computes the house chains.
|
|
/// Adds one newly revealed house into the session and recalculates chains.
|
|
HousesSession revealHouse(
|
|
HousesSession session, {
|
|
required HouseCell cell,
|
|
required WordItem guestWord,
|
|
}) {
|
|
final newOrder = session.selectedHouses.length + 1;
|
|
final updatedCell = cell.copyWith(
|
|
guestWord: guestWord,
|
|
selectionOrder: newOrder,
|
|
);
|
|
|
|
final updatedSelected = List<HouseCell>.from(session.selectedHouses)..add(updatedCell);
|
|
|
|
final fullGridMap = <String, HouseCell>{};
|
|
for (final h in session.grid) {
|
|
fullGridMap[h.houseWord.word.toLowerCase().trim()] = h;
|
|
}
|
|
|
|
final openedHousesMap = <String, HouseCell>{};
|
|
for (final h in updatedSelected) {
|
|
openedHousesMap[h.houseWord.word.toLowerCase().trim()] = h;
|
|
}
|
|
|
|
final chains = _computeChains(updatedSelected, openedHousesMap, fullGridMap, session.sectors);
|
|
|
|
final updatedGrid = session.grid.map((c) {
|
|
if (c.houseIndex == cell.houseIndex) {
|
|
return updatedCell;
|
|
}
|
|
return c;
|
|
}).toList();
|
|
|
|
return HousesSession(
|
|
id: session.id,
|
|
createdAt: session.createdAt,
|
|
question: session.question,
|
|
querentWord: session.querentWord,
|
|
querentHouseIndex: session.querentHouseIndex,
|
|
sectors: session.sectors,
|
|
grid: updatedGrid,
|
|
selectedHouses: updatedSelected,
|
|
chains: chains,
|
|
);
|
|
}
|
|
|
|
HousesSession completeDivination(
|
|
HousesSession session,
|
|
List<HouseCell> selectedHouses,
|
|
) {
|
|
if (selectedHouses.isEmpty) return session;
|
|
|
|
final rnd = Random.secure();
|
|
final repo = WordsRepository();
|
|
final availableGuests = List<WordItem>.from(repo.allWords)..shuffle(rnd);
|
|
|
|
// Assign guests uniquely
|
|
final populatedHouses = <HouseCell>[];
|
|
for (int i = 0; i < selectedHouses.length; i++) {
|
|
final guest = availableGuests[i];
|
|
populatedHouses.add(
|
|
selectedHouses[i].copyWith(
|
|
guestWord: guest,
|
|
selectionOrder: i + 1,
|
|
),
|
|
);
|
|
}
|
|
|
|
// Map houses on the full grid by word name for instant lookup
|
|
final fullGridMap = <String, HouseCell>{};
|
|
for (final h in session.grid) {
|
|
fullGridMap[h.houseWord.word.toLowerCase().trim()] = h;
|
|
}
|
|
|
|
// Map populated/selected houses by word name
|
|
final openedHousesMap = <String, HouseCell>{};
|
|
for (final h in populatedHouses) {
|
|
openedHousesMap[h.houseWord.word.toLowerCase().trim()] = h;
|
|
}
|
|
|
|
// Compute House Chains
|
|
final chains = _computeChains(populatedHouses, openedHousesMap, fullGridMap, session.sectors);
|
|
|
|
// Update grid state with guests
|
|
final updatedGrid = session.grid.map((cell) {
|
|
final found = populatedHouses.firstWhere(
|
|
(p) => p.row == cell.row && p.col == cell.col,
|
|
orElse: () => cell,
|
|
);
|
|
return found;
|
|
}).toList();
|
|
|
|
return HousesSession(
|
|
id: session.id,
|
|
createdAt: session.createdAt,
|
|
question: session.question,
|
|
querentWord: session.querentWord,
|
|
querentHouseIndex: session.querentHouseIndex,
|
|
sectors: session.sectors,
|
|
grid: updatedGrid,
|
|
selectedHouses: populatedHouses,
|
|
chains: chains,
|
|
);
|
|
}
|
|
|
|
List<HouseChain> _computeChains(
|
|
List<HouseCell> populatedHouses,
|
|
Map<String, HouseCell> openedMap,
|
|
Map<String, HouseCell> fullGridMap,
|
|
List<ArchonSector> sectors,
|
|
) {
|
|
final chains = <HouseChain>[];
|
|
final repo = WordsRepository();
|
|
final allWords = repo.allWords;
|
|
|
|
final archonMap = <String, ArchonSector>{};
|
|
for (final s in sectors) {
|
|
archonMap[s.archonWord.word.toLowerCase().trim()] = s;
|
|
}
|
|
|
|
int chainIdx = 1;
|
|
for (final startHouse in populatedHouses) {
|
|
final steps = <HouseChainStep>[];
|
|
final visitedHousesInChain = <String>{};
|
|
|
|
HouseCell curHouse = startHouse;
|
|
WordItem curGuest = startHouse.guestWord!;
|
|
ChainEndType endType = ChainEndType.fieldVector;
|
|
String desc = '';
|
|
|
|
// Max 9 links per chain; early break if loop or archon throne reached
|
|
for (int stepNum = 0; stepNum < 9; stepNum++) {
|
|
final curHouseKey = curHouse.houseWord.word.toLowerCase().trim();
|
|
final curGuestKey = curGuest.word.toLowerCase().trim();
|
|
visitedHousesInChain.add(curHouseKey);
|
|
|
|
// 1. Check if curGuest is one of the 10 Archons
|
|
final archonSector = archonMap[curGuestKey];
|
|
|
|
if (archonSector != null) {
|
|
steps.add(
|
|
HouseChainStep(
|
|
house: curHouse,
|
|
guest: curGuest,
|
|
leadsToArchon: true,
|
|
destinationArchon: archonSector,
|
|
),
|
|
);
|
|
endType = ChainEndType.archonThrone;
|
|
desc = 'Восхождение к Престолу Архонта «${curGuest.word.toUpperCase()}» (Триада ${archonSector.index + 1})';
|
|
break;
|
|
}
|
|
|
|
// 2. Find next house on the 990 field
|
|
final nextHouse = fullGridMap[curGuestKey];
|
|
if (nextHouse == null) {
|
|
steps.add(HouseChainStep(house: curHouse, guest: curGuest));
|
|
desc = 'Импульс уходит за пределы полотна к знаку «${curGuest.word.toUpperCase()}»';
|
|
break;
|
|
}
|
|
|
|
final nextHouseKey = nextHouse.houseWord.word.toLowerCase().trim();
|
|
|
|
// 3. Check if loop closed
|
|
if (visitedHousesInChain.contains(nextHouseKey)) {
|
|
steps.add(
|
|
HouseChainStep(
|
|
house: curHouse,
|
|
guest: curGuest,
|
|
isLoopClosing: true,
|
|
),
|
|
);
|
|
endType = ChainEndType.loop;
|
|
desc = 'Замкнутая петля судьбы: замыкается на Дом «${nextHouse.houseWord.word.toUpperCase()}»';
|
|
break;
|
|
}
|
|
|
|
// 4. Advance to next house
|
|
steps.add(HouseChainStep(house: curHouse, guest: curGuest));
|
|
|
|
// Determine next guest. An unopened house does not invent a chain.
|
|
final openedNext = openedMap[nextHouseKey];
|
|
if (openedNext != null && openedNext.guestWord != null) {
|
|
curGuest = openedNext.guestWord!;
|
|
curHouse = nextHouse;
|
|
} else {
|
|
final guestIdx = (nextHouse.houseIndex * 7919 + 101) % allWords.length;
|
|
final computedGuest = allWords[guestIdx];
|
|
steps.add(
|
|
HouseChainStep(
|
|
house: nextHouse,
|
|
guest: computedGuest,
|
|
computed: true,
|
|
),
|
|
);
|
|
endType = ChainEndType.fieldVector;
|
|
desc =
|
|
'Дом «${nextHouse.houseWord.word.toUpperCase()}» не открыт. Знак «${computedGuest.word.toUpperCase()}» вычислен из поля, это не жребий. Цепь здесь останавливается.';
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (desc.isEmpty) {
|
|
desc = 'Линейный вектор: луч энергии уходит сквозь полотно к знаку «${steps.last.guest.word.toUpperCase()}»';
|
|
}
|
|
|
|
chains.add(
|
|
HouseChain(
|
|
chainIndex: chainIdx++,
|
|
startingHouse: startHouse,
|
|
steps: steps,
|
|
endType: endType,
|
|
description: desc,
|
|
),
|
|
);
|
|
}
|
|
|
|
return chains;
|
|
}
|
|
|
|
|
|
}
|