Initial commit with gitignore
This commit is contained in:
485
lib/services/formation_generator.dart
Normal file
485
lib/services/formation_generator.dart
Normal file
@@ -0,0 +1,485 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user