Files
999/lib/services/houses_service.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;
}
}