UNPKG

@zerospacegg/iolin

Version:

Pure TypeScript implementation of ZeroSpace game data processing (PKL-free)

304 lines 11.5 kB
/** * 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