@zerospacegg/iolin
Version:
Pure TypeScript implementation of ZeroSpace game data processing (PKL-free)
304 lines • 11.5 kB
JavaScript
/**
* All-Random MatchMaker - ZeroSpace Random Match Generation System
* Generates randomized faction/mercenary/hero combinations for matches
*
* Creates balanced match series with smart randomization to ensure
* fair distribution of factions, mercenaries, and heroes across multiple games.
* Perfect for ladder practice or fun random matches!
*/
import { all } from "../meta/all.js";
// Helper function to find entities by slug or filter by criteria
function bySlug(slug) {
return all.find((entity) => entity.slug === slug);
}
function byType(type, subtype) {
return all.filter((entity) => entity.type === type &&
(subtype ? entity.subtype === subtype : true) &&
entity.inGame === true);
}
// Game configuration constants
const PLAYER_COUNT_PER_GAME_TYPE = {
solo: 1,
'1v1': 2,
'2v2': 4,
ffa: 4,
};
/**
* All-Random Generator Class - Handles all match generation logic
*/
export class AllRandomGenerator {
/**
* Initialize the random number generator with a seed
*/
static initRandom(seed) {
// Simple seeded random implementation
let currentSeed = seed % 2147483647;
this.rng = {
next: () => {
currentSeed = (currentSeed * 16807) % 2147483647;
return (currentSeed - 1) / 2147483646;
},
choice: (arr) => {
const index = Math.floor(this.rng.next() * arr.length);
return arr[index];
},
int: (min, max) => {
return Math.floor(this.rng.next() * (max - min + 1)) + min;
},
};
}
/**
* Get game data from the entity system
*/
static getGameData() {
const factions = byType('faction', 'main');
const mercenaries = byType('faction', 'mercenary');
const heroes = byType('unit', 'hero');
const maps1v1 = byType('map', '1v1');
const maps2v2 = byType('map', '2v2');
const mapsFfa = byType('map', 'ffa');
if (!factions.length || !mercenaries.length || !heroes.length) {
throw new Error("Missing game data - ensure factions, mercenaries, and heroes exist");
}
return {
factions,
mercenaries,
heroes,
mapPools: {
solo: maps1v1,
'1v1': maps1v1,
'2v2': maps2v2,
ffa: mapsFfa,
},
};
}
/**
* Create usage tracking object from entity array
*/
static mkUsed(arr) {
return arr.reduce((m, x) => ({ ...m, [x.id]: 0 }), {});
}
/**
* Create usage tracking object from string array
*/
static mkUsedStr(arr) {
return arr.reduce((m, x) => ({ ...m, [x]: 0 }), {});
}
/**
* Smart entity selection with usage balancing
*/
static choose(entities, used) {
if (!entities || entities.length === 0) {
console.error('choose() called with empty entities array');
return null;
}
const minUsed = Math.min(...Object.values(used));
const available = entities.filter((f) => used[f.id] === minUsed);
if (available.length === 0) {
console.error('No available entities after filtering', entities, used);
return null;
}
return this.rng.choice(available);
}
/**
* Smart hero selection based on faction compatibility
*/
static chooseHero(faction, merc, used, heroList) {
// Check for both hero and heroes properties (different entity formats)
const factionHeroes = faction.hero || faction.heroes;
if (!faction || !factionHeroes) {
console.error('Invalid faction for chooseHero', faction);
return null;
}
const heroSlugs = [...factionHeroes];
const mercHeroes = merc.hero || merc.heroes;
// Only allow mercenary heroes if faction permits it (Legion are religious fanatics!)
if (faction.mercHeroesAllowed && merc && mercHeroes && mercHeroes.length) {
heroSlugs.push(...mercHeroes);
}
const facHeroes = heroSlugs
.map((s) => heroList.find((x) => x.id === s))
.filter((x) => typeof x !== 'undefined');
if (facHeroes.length === 0) {
console.error('No heroes found for faction', faction, heroSlugs, heroList);
return null;
}
const usedAndOk = Object.fromEntries(Object.entries(used).filter(([k, v]) => facHeroes.map((x) => x.id).includes(k)));
return this.choose(facHeroes, usedAndOk);
}
/**
* Generate randomized faction/merc/hero combinations for a single player
*/
static async generatePlayerGames(n, gameData) {
if (n > 11) {
throw new Error('Invalid number of matches');
}
const usedFacs = this.mkUsed(gameData.factions);
const usedMercs = this.mkUsed(gameData.mercenaries);
const usedHeroes = this.mkUsed(gameData.heroes);
const results = [];
for (let roundNum = 0; roundNum < n; roundNum++) {
const faction = this.choose(gameData.factions, usedFacs);
if (!faction) {
console.error('No faction available!', gameData.factions);
continue;
}
usedFacs[faction.id]++;
const merc = this.choose(gameData.mercenaries, usedMercs);
if (!merc) {
console.error('No merc available!', gameData.mercenaries);
continue;
}
usedMercs[merc.id]++;
const hero = this.chooseHero(faction, merc, usedHeroes, gameData.heroes);
if (!hero) {
console.error('No hero available!', gameData.heroes);
continue;
}
usedHeroes[hero.id]++;
results.push({
faction: faction.id,
merc: merc.id,
hero: hero.id
});
}
return results;
}
/**
* Generate complete match series with balanced randomization
*/
static async generateGames(input) {
const { gameType, numberOfGames, players, seed = Date.now() } = input;
this.initRandom(seed);
const gameData = this.getGameData();
// Validate player count
const requiredPlayers = PLAYER_COUNT_PER_GAME_TYPE[gameType];
if (players.length !== requiredPlayers) {
throw new Error(`Game type ${gameType} requires ${requiredPlayers} players, got ${players.length}`);
}
// Generate player loadouts
const playerGames = {};
for (const player of players) {
playerGames[player] = await this.generatePlayerGames(numberOfGames, gameData);
}
// Generate games with map selection and host rotation
const maps = gameData.mapPools[gameType];
const games = [];
const usedHosts = this.mkUsedStr(players);
const usedMaps = this.mkUsed(maps);
const boost = this.rng.int(0, players.length - 1);
for (let gameNumber = 0; gameNumber < numberOfGames; gameNumber++) {
const host = players[(boost + gameNumber) % players.length];
usedHosts[host]++;
const map = this.choose(maps, usedMaps) ?? maps[0];
usedMaps[map.id]++;
const _players = [];
for (const player of players) {
_players.push(playerGames[player][gameNumber]);
}
games.push({ host, map: map.id, players: _players });
}
return { players, gameType, gamesCount: numberOfGames, games, seed };
}
/**
* Generate complete match series result with metadata
*/
static async generateMatches(input) {
const gameSet = await this.generateGames(input);
const gameData = this.getGameData();
const metadata = {
timestamp: new Date().toISOString(),
version: "1.0-all-random-generator-typescript",
inputSettings: input,
gameBalance: {
factionsCount: gameData.factions.length,
mercenariesCount: gameData.mercenaries.length,
heroesCount: gameData.heroes.length,
mapsCount: {
solo: gameData.mapPools.solo.length,
'1v1': gameData.mapPools['1v1'].length,
'2v2': gameData.mapPools['2v2'].length,
ffa: gameData.mapPools.ffa.length,
},
},
features: {
smartRandomization: true,
usageTracking: true,
hostRotation: true,
mapPoolFiltering: true,
heroCompatibility: true,
},
success: true,
};
return { gameSet, metadata };
}
/**
* Get calculation metadata for the system
*/
static getGeneratorMetadata() {
const gameData = this.getGameData();
return {
timestamp: new Date().toISOString(),
version: "1.0-all-random-generator-typescript",
inputSettings: {
gameType: '1v1',
numberOfGames: 3,
players: ['Player 1', 'Player 2'],
},
gameBalance: {
factionsCount: gameData.factions.length,
mercenariesCount: gameData.mercenaries.length,
heroesCount: gameData.heroes.length,
mapsCount: {
solo: gameData.mapPools.solo.length,
'1v1': gameData.mapPools['1v1'].length,
'2v2': gameData.mapPools['2v2'].length,
ffa: gameData.mapPools.ffa.length,
},
},
features: {
smartRandomization: true,
usageTracking: true,
hostRotation: true,
mapPoolFiltering: true,
heroCompatibility: true,
},
success: true,
};
}
}
AllRandomGenerator.rng = null;
// Export default generation function for easy use
export function generateAllRandomMatches(gameType = '1v1', numberOfGames = 3, players = ['Player 1', 'Player 2'], seed) {
return AllRandomGenerator.generateMatches({
gameType,
numberOfGames,
players,
seed,
});
}
// Export for JSON output configuration
export const output = {
files: {
"api/all-random.json": {
renderer: { indent: " " },
value: async () => {
// Example match series for different game types
const matches = await Promise.all([
generateAllRandomMatches('1v1', 3, ['Player 1', 'Player 2']),
generateAllRandomMatches('2v2', 5, ['Team A P1', 'Team A P2', 'Team B P1', 'Team B P2']),
generateAllRandomMatches('ffa', 1, ['Player 1', 'Player 2', 'Player 3', 'Player 4']),
]);
return {
examples: {
'1v1_bo3': matches[0],
'2v2_bo5': matches[1],
'ffa_single': matches[2],
},
metadata: AllRandomGenerator.getGeneratorMetadata(),
};
},
},
},
};
//# sourceMappingURL=all-random.js.map