Files
22e1372-efootballnexus/lib/services/formation_generator.dart

486 lines
14 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String, dynamic> generate({
required ConstraintMode mode,
bool rareStyleOn = false,
String? selectedLeague,
String? selectedCardType,
List<Map<String, dynamic>> playerPool = const [],
Map<String, int>? 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 = <Map<String, String>>[];
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<String> teamStyles = [
'ポゼッション',
'ショートカウンター',
'ロングカウンター',
'サイドアタック',
'ロングボール',
];
static String _pickTeamStyle(Map<String, int>? 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<int>(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<String> _assignNamesByRole({
required List<String> roles,
required List<Map<String, dynamic>> pool,
}) {
final remaining = pool
.map((p) => {
'name': (p['name'] ?? '').toString().trim(),
'positions': List<String>.from(p['positions'] ?? const <String>[]),
})
.where((p) => (p['name'] as String).isNotEmpty)
.toList();
final assigned = <String>[];
for (final role in roles) {
final candidates = remaining
.where((p) => (p['positions'] as List<String>).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<String> styles, bool rareStyleOn) {
const rareStyles = {
'デコイラン',
'ナンバー10',
'ターゲットマン',
'インナーラップサイドバック',
'オーバーラップ',
'インサイドレシーバー',
};
if (rareStyleOn) {
return styles[_rand.nextInt(styles.length)];
}
final weights = <double>[];
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<String> _assignRolesForFormation(
String formation,
List<List<Map<String, double>>> 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 = <String>[];
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<String> _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 = <String>['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<String> _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<String> _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<String> _applyMFConstraints(
List<String> 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<String> _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<String> 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<String> 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<String> 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<String> 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);
}
}