import 'dart:math'; import '../data/formation_positions.dart'; import '../data/player_attributes.dart'; import '../data/league_data.dart'; import '../data/cardtype_data.dart'; enum ConstraintMode { playstyle, league, cardType, pool } class FormationGenerator { static final Random _rand = Random(); /// playerPool: /// [{'name':'A', 'positions':['CF','ST']}, ...] static Map generate({ required ConstraintMode mode, bool rareStyleOn = false, String? selectedLeague, String? selectedCardType, List> playerPool = const [], Map? teamStyleWeights, }) { final formation = formations[_rand.nextInt(formations.length)]; final layout = getFormationLayout(formation); final roles = _assignRolesForFormation(formation, layout); final teamStyle = _pickTeamStyle(teamStyleWeights); // poolモードのときだけ名前を割り当てる(混ぜない) final names = (mode == ConstraintMode.pool) ? _assignNamesByRole(roles: roles, pool: playerPool) : List.filled(roles.length, ''); final result = >[]; for (int i = 0; i < roles.length; i++) { final pos = roles[i]; String styleOrValue; switch (mode) { case ConstraintMode.playstyle: styleOrValue = _pickStyle(pos, rareStyleOn); // ←プレスタはそのまま break; case ConstraintMode.league: // ランダムなら「ポジションごと」に抽選 styleOrValue = (selectedLeague == null) ? leagues[_rand.nextInt(leagues.length)] : selectedLeague; break; case ConstraintMode.cardType: styleOrValue = (selectedCardType == null) ? cardTypes[_rand.nextInt(cardTypes.length)] : selectedCardType; break; case ConstraintMode.pool: // poolモードでは「style」は表示用に空(混ぜない) styleOrValue = ''; break; } result.add({ 'pos': pos, 'style': styleOrValue, 'name': names[i], }); } return { 'formation': formation, 'mode': mode.name, 'constraintLabel': _constraintLabel(mode, selectedLeague, selectedCardType), 'positions': result, 'teamStyle':teamStyle }; } // ===================== // チームスタイル抽選 // ===================== static const List teamStyles = [ 'ポゼッション', 'ショートカウンター', 'ロングカウンター', 'サイドアタック', 'ロングボール', ]; static String _pickTeamStyle(Map? weights) { final w = weights ?? { 'ポゼッション': 50, 'ショートカウンター': 50, 'ロングカウンター': 50, 'サイドアタック': 50, 'ロングボール': 50, }; final entries = teamStyles .map((s) => MapEntry(s, w[s] ?? 0)) .where((e) => e.value > 0) .toList(); if (entries.isEmpty) { return teamStyles[_rand.nextInt(teamStyles.length)]; } final total = entries.fold(0, (sum, e) => sum + e.value); int r = _rand.nextInt(total); for (final e in entries) { r -= e.value; if (r < 0) return e.key; } return entries.last.key; } static String _constraintLabel( ConstraintMode mode, String? selectedLeague, String? selectedCardType, ) { switch (mode) { case ConstraintMode.playstyle: return 'プレースタイル縛り'; case ConstraintMode.league: return selectedLeague != null ? 'リーグ縛り: $selectedLeague' : 'リーグ縛り: ランダム(ポジション別)'; case ConstraintMode.cardType: return selectedCardType != null ? 'カードタイプ縛り: $selectedCardType' : 'カードタイプ縛り: ランダム(ポジション別)'; case ConstraintMode.pool: return '選手プール抽選'; } } // ============================ // プール割り当て(完全一致) // ============================ static List _assignNamesByRole({ required List roles, required List> pool, }) { final remaining = pool .map((p) => { 'name': (p['name'] ?? '').toString().trim(), 'positions': List.from(p['positions'] ?? const []), }) .where((p) => (p['name'] as String).isNotEmpty) .toList(); final assigned = []; for (final role in roles) { final candidates = remaining .where((p) => (p['positions'] as List).contains(role)) .toList(); if (candidates.isEmpty) { assigned.add('未登録'); continue; } final pick = candidates[_rand.nextInt(candidates.length)]; assigned.add(pick['name'] as String); remaining.remove(pick); } return assigned; } // ============================ // プレスタ(触らない) // ============================ static String _pickStyle(String pos, bool rareStyleOn) { final list = playerAttributes[pos]; final baseList = (list == null || list.isEmpty) ? allPlayStyle : list; return _weightedRandomStyle(baseList, rareStyleOn); } static String _weightedRandomStyle(List styles, bool rareStyleOn) { const rareStyles = { 'デコイラン', 'ナンバー10', 'ターゲットマン', 'インナーラップサイドバック', 'オーバーラップ', 'インサイドレシーバー', }; if (rareStyleOn) { return styles[_rand.nextInt(styles.length)]; } final weights = []; double total = 0; for (final s in styles) { final w = rareStyles.contains(s) ? 0.3 : 1.0; weights.add(w); total += w; } final r = _rand.nextDouble() * total; double acc = 0; for (int i = 0; i < styles.length; i++) { acc += weights[i]; if (r <= acc) return styles[i]; } return styles.last; } // ===================================== // フォメ + レイアウトに応じて役割を割り当て // ===================================== static List _assignRolesForFormation( String formation, List>> layout, ) { final parts = formation.split('-').map(int.parse).toList(); final int dfCount = parts[0]; final int fwCount = parts.last; final bool isFourLineFormation = parts.length == 4; // 4-1-4-1, 4-2-3-1 など // 中盤総人数(「4-4-2」なら 4、『4-2-3-1』なら 2+3=5) int mfPlayers = 0; if (parts.length > 2) { mfPlayers = parts.sublist(1, parts.length - 1).reduce((a, b) => a + b); } final roles = []; int mfRowIndex = 0; // MF行の中での何列目か (0 = 2列目 = FW直下) for (int row = 0; row < layout.length; row++) { final slots = layout[row].length; final isTop = row == 0; final isBottom = row == layout.length - 1; final isDfRow = row == layout.length - 2; if (isTop) { // 一番上の行 → FW roles.addAll(_assignFW(fwCount)); } else if (isBottom) { // 一番下の行 → GK(スロットすべて GK 扱いだが基本1人) for (int i = 0; i < slots; i++) { roles.add('GK'); } } else if (isDfRow) { // DF行 roles.addAll(_assignDF(dfCount)); } else { // MF行 roles.addAll(_assignMFRow( formation: formation, isFourLineFormation: isFourLineFormation, mfRowIndex: mfRowIndex, slots: slots, mfPlayers: mfPlayers, )); mfRowIndex++; } } return roles; } // ===================================== static List _assignFW(int count) { // 1トップ → LWG/RWG は出さない(CF/STだけ) if (count == 1) { return [_rand.nextDouble() < 0.8 ? 'CF' : 'ST']; } // 2トップ → サイド禁止(CF/STだけ) if (count == 2) { final pool = ['CF', 'CF', 'CF', 'ST']; return List.generate(2, (_) => pool[_rand.nextInt(pool.length)]); } // 3トップ → 左LWG / 中央CF or ST / 右RWG に固定 if (count == 3) { return [ 'LWG', _rand.nextBool() ? 'CF' : 'ST', 'RWG', ]; } // 想定外 → とりあえずCFで埋める return List.filled(count, 'CF'); } // ===================================== // DF ロジック // ===================================== static List _assignDF(int count) { if (count == 5) { // 5バック return ['LSB', 'CB', 'CB', 'CB', 'RSB']; } if (count == 4) { // 4バック(3パターンから抽選) final patterns = [ ['LSB', 'CB', 'CB', 'RSB'], ['LSB', 'CB', 'CB', 'CB'], ['CB', 'CB', 'CB', 'RSB'], ]; return patterns[_rand.nextInt(patterns.length)]; } if (count == 3) { // 3バック return ['CB', 'CB', 'CB']; } // それ以外 → 全部CBで埋める return List.filled(count, 'CB'); } // ===================================== // MF行ごとの割り当て // ===================================== static List _assignMFRow({ required String formation, required bool isFourLineFormation, required int mfRowIndex, // 0 = 最前線の一つ後ろ(2列目) required int slots, // そのMF行の人数(レイアウト上) required int mfPlayers, // 中盤総人数 }) { final parts = formation.split('-').map(int.parse).toList(); bool allowWideMF = false; if (isFourLineFormation) { final int v2 = parts[1]; // 2列目MF final int v3 = parts[2]; // 3列目MF // ★ 4列フォメの3列目(mfRowIndex == 1)には LMF/RMF を絶対出さない if (mfRowIndex == 1) { allowWideMF = false; } else { // 2列目(mfRowIndex == 0)のみチェック if (v2 + v3 >= 4 && mfRowIndex == 0 && slots >= 3) { allowWideMF = true; } } } else { // 3列フォメ final int mid = parts[1]; if (mid >= 4 && mfRowIndex == 0 && slots >= 3) { allowWideMF = true; } } return _assignMFLogic( formation: formation, isFourLineFormation: isFourLineFormation, mfRowIndex: mfRowIndex, slots: slots, mfPlayers: mfPlayers, allowWideMF: allowWideMF, ); } // ===================================== // OMF / DMF の制約 // ===================================== static List _applyMFConstraints( List row, bool isFourLineFormation, int mfRowIndex) { // OMF制約 row = row.map((p) { if (p == 'OMF') { if (!isFourLineFormation) return 'CMF'; if (mfRowIndex != 0) return 'CMF'; } return p; }).toList(); // DMF制約:4列フォメの2列目のみ禁止 if (isFourLineFormation && mfRowIndex == 0) { row = row.map((p) => p == 'DMF' ? 'CMF' : p).toList(); } return row; } // ===================================== // MF ロジック本体 // ===================================== static List _assignMFLogic({ required String formation, required bool isFourLineFormation, required int mfRowIndex, required int slots, required int mfPlayers, required bool allowWideMF, }) { final r = _rand; final bool useWide = allowWideMF; if (slots == 1) { final row = ['DMF']; return _applyMFConstraints(row, isFourLineFormation, mfRowIndex); } if (useWide && slots >= 3) { List row = List.filled(slots, 'CMF'); row[0] = 'LMF'; row[slots - 1] = 'RMF'; for (int i = 1; i < slots - 1; i++) { if (isFourLineFormation && mfRowIndex == 0) { final centerPool = ['OMF', 'OMF', 'OMF', 'CMF']; row[i] = centerPool[r.nextInt(centerPool.length)]; } else if (isFourLineFormation) { final centerPool = ['DMF', 'DMF', 'DMF', 'CMF']; row[i] = centerPool[r.nextInt(centerPool.length)]; } else { final centerPool = ['DMF', 'DMF', 'DMF', 'CMF']; row[i] = centerPool[r.nextInt(centerPool.length)]; } } return _applyMFConstraints(row, isFourLineFormation, mfRowIndex); } if (slots == 2) { List row; if (isFourLineFormation) { if (mfRowIndex == 0) { final pool = ['OMF', 'OMF', 'OMF', 'CMF']; row = [ pool[r.nextInt(pool.length)], pool[r.nextInt(pool.length)], ]; } else { final pool = ['DMF', 'DMF', 'DMF', 'CMF']; row = [ pool[r.nextInt(pool.length)], pool[r.nextInt(pool.length)], ]; } } else { final pool = ['DMF', 'CMF', 'CMF']; row = [ pool[r.nextInt(pool.length)], pool[r.nextInt(pool.length)], ]; } return _applyMFConstraints(row, isFourLineFormation, mfRowIndex); } if (slots == 3 && !useWide) { List row; if (isFourLineFormation) { if (mfRowIndex == 0) { final pool = ['OMF', 'OMF', 'CMF']; row = [ pool[r.nextInt(pool.length)], pool[r.nextInt(pool.length)], pool[r.nextInt(pool.length)], ]; } else { row = ['DMF', 'DMF', 'CMF']; } } else { row = ['CMF', 'DMF', 'CMF']; } return _applyMFConstraints(row, isFourLineFormation, mfRowIndex); } List row = List.filled(slots, 'CMF'); final center = slots ~/ 2; row[center] = 'DMF'; if (center - 1 >= 0) row[center - 1] = 'DMF'; if (center + 1 < slots) row[center + 1] = 'DMF'; return _applyMFConstraints(row, isFourLineFormation, mfRowIndex); } }