Initial commit

**Motivations:**
- Initialisation du versionning git pour le projet

**Root causes:**
- N/A (Nouveau projet)

**Correctifs:**
- N/A

**Evolutions:**
- Structure initiale du projet
- Ajout du .gitignore

**Pages affectées:**
- Tous les fichiers
This commit is contained in:
2026-03-03 22:24:17 +01:00
commit e031c9a1d2
155 changed files with 22334 additions and 0 deletions

331
web/js/bot-zoo.js Normal file
View File

@@ -0,0 +1,331 @@
/**
* Bot zoos: same indicators and formulas as player (coins, plotLevel, conveyorLevel, truckLevel).
* Decisions (buy, sell, upgrade) follow a randomly chosen profile (fast / slow / balanced).
* Egg color choice is weighted by neighboring zoos' animalWeights.
*/
import { GameConfig } from "./config.js";
import { LootTables, getColorNames, zeroAnimalWeights } from "./loot-tables.js";
import { getPlotUpgradeCost, getSchoolUpgradeCost, getTruckUpgradeCost, getConveyorUpgradeCost, getSellValue } from "./economy.js";
import { getIncomeMultiplier } from "./mutation-rules.js";
import { pickId } from "./weighted-random.js";
import { tryUpgradePlot } from "./zoo.js";
import { tryUpgrade, tryUpgradeTruck } from "./conveyor.js";
import { getEffectiveProfileId, getProfileParams, LEGACY_PROFILE_TO_ID } from "./auto-mode-profiles.js";
const PROFILE_OPTIONS = ["fast", "slow", "balanced"];
/**
* @returns {import("./types.js").BotState}
*/
export function createInitialBotState() {
const cfg = GameConfig.Bots || {};
const minC = cfg.InitialCoinsMin ?? 150;
const maxC = cfg.InitialCoinsMax ?? 450;
const coins = minC + Math.floor(Math.random() * (maxC - minC + 1));
const profile = PROFILE_OPTIONS[Math.floor(Math.random() * PROFILE_OPTIONS.length)];
return {
coins,
plotLevel: 1,
conveyorLevel: 1,
truckLevel: 1,
profile,
lastTickAt: 0,
};
}
/**
* @param {import("./types.js").WorldZooEntry} zoo
* @returns {number}
*/
export function getZooSkillLevel(zoo) {
const b = zoo.botState;
return b ? b.conveyorLevel : 1;
}
/**
* Neighbor color weights (sum of animalWeights of zoos within maxDistance on the map).
* @param {import("./types.js").GameState} state
* @param {string} zooId
* @returns {Record<string, number>}
*/
export function getNeighborColorWeights(state, zooId) {
const zoos = state.worldZoos ?? [];
const self = zoos.find((z) => z.id === zooId);
if (!self) return {};
const maxD = (GameConfig.Bots && GameConfig.Bots.NeighborMaxDistance) ?? 35;
const colorNames = getColorNames();
const out = zeroAnimalWeights();
for (const z of zoos) {
if (z.id !== zooId) {
const dx = (z.x - self.x) / 100;
const dy = (z.y - self.y) / 100;
const dist = Math.sqrt(dx * dx + dy * dy) * 100;
if (dist <= maxD) {
const w = z.animalWeights ?? {};
for (const c of colorNames) out[c] = (out[c] ?? 0) + (w[c] ?? 0);
}
}
}
return out;
}
function getAverageIncomePerSecondForEggType(eggType) {
const def = LootTables.EggTypes[eggType];
if (!def || !def.loot.length) return 0;
let sum = 0;
let totalWeight = 0;
for (const e of def.loot) {
const a = LootTables.Animals[e.id];
if (a) {
const w = e.weight ?? 1;
sum += a.baseIncomePerSecond * w;
totalWeight += w;
}
}
return totalWeight > 0 ? sum / totalWeight : 0;
}
function getAverageSellValueForEggType(eggType) {
const def = LootTables.EggTypes[eggType];
if (!def || !def.loot.length) return 0;
let sum = 0;
let totalWeight = 0;
const mutMult = getIncomeMultiplier("none");
for (const e of def.loot) {
const a = LootTables.Animals[e.id];
if (a) {
const w = e.weight ?? 1;
const v = getSellValue(a.baseIncomePerSecond, 1, mutMult, a.sellFactor);
sum += v * w;
totalWeight += w;
}
}
return totalWeight > 0 ? Math.floor(sum / totalWeight) : 0;
}
/**
* Add income for a bot from its animalWeights (same formula as player: income per "animal" per color).
* @param {import("./types.js").WorldZooEntry} zoo
* @param {number} dt seconds
*/
function tickBotIncome(zoo, dt) {
const b = zoo.botState;
if (!b) return;
const weights = zoo.animalWeights ?? {};
const colorNames = getColorNames();
let total = 0;
for (const eggType of colorNames) {
const count = weights[eggType] ?? 0;
if (count > 0) {
const avgIncome = getAverageIncomePerSecondForEggType(eggType);
total += count * avgIncome * dt;
}
}
const visitorPart = 0;
b.coins = Math.max(0, b.coins + total + visitorPart);
}
/**
* Ensure zoo has botState (for non-player zoos). Mutates zoo.
* @param {import("./types.js").WorldZooEntry} zoo
* @param {boolean} isPlayer
*/
export function ensureBotState(zoo, isPlayer) {
if (isPlayer) return;
if (zoo.botState) return;
zoo.botState = createInitialBotState();
}
/**
* @param {import("./types.js").GameState} state
* @param {import("./types.js").WorldZooEntry} zoo
* @param {{ b: import("./types.js").BotState, rng: () => number, params: { spendThreshold: number, upgradeChance: number } }} opts
* @returns {boolean}
*/
function botDecideUpgrade(state, zoo, opts) {
const { b, rng, params } = opts;
const { spendThreshold, upgradeChance } = params;
const plotCost = getPlotUpgradeCost(b.plotLevel);
const plotMax = GameConfig.Plot?.MaxLevel ?? 8;
const skillCost = getSchoolUpgradeCost(b.conveyorLevel);
const skillMax = GameConfig.Conveyor?.MaxLevel ?? 8;
const truckCost = getTruckUpgradeCost(b.truckLevel);
const truckMax = (GameConfig.Truck && GameConfig.Truck.MaxLevel) ?? 5;
const canUpgradePlot = b.plotLevel < plotMax && b.coins >= plotCost * spendThreshold;
const canUpgradeSkill = b.conveyorLevel < skillMax && b.coins >= skillCost * spendThreshold;
const canUpgradeTruck = b.truckLevel < truckMax && b.coins >= truckCost * spendThreshold;
const upgradeChoices = [];
if (canUpgradePlot) upgradeChoices.push("plot");
if (canUpgradeSkill) upgradeChoices.push("skill");
if (canUpgradeTruck) upgradeChoices.push("truck");
if (upgradeChoices.length === 0 || rng() >= upgradeChance) return false;
const choice = upgradeChoices[Math.floor(rng() * upgradeChoices.length)];
if (choice === "plot" && b.coins >= plotCost) {
b.coins -= plotCost;
b.plotLevel += 1;
return true;
}
if (choice === "skill" && b.coins >= skillCost) {
b.coins -= skillCost;
b.conveyorLevel += 1;
return true;
}
if (choice === "truck" && b.coins >= truckCost) {
b.coins -= truckCost;
b.truckLevel += 1;
return true;
}
return false;
}
/**
* @param {import("./types.js").WorldZooEntry} zoo
* @param {() => number} rng
* @param {{ sellChance: number }} params
* @returns {boolean}
*/
function botDecideSell(zoo, rng, params) {
const weights = zoo.animalWeights ?? {};
const colorNames = getColorNames();
const totalAnimals = colorNames.reduce((s, c) => s + (weights[c] ?? 0), 0);
if (totalAnimals <= 0 || rng() >= params.sellChance) return false;
const withWeight = colorNames.filter((c) => (weights[c] ?? 0) > 0).map((c) => ({ id: c, weight: weights[c] ?? 0 }));
if (withWeight.length === 0) return false;
const soldColor = pickId(rng, withWeight);
weights[soldColor] = (weights[soldColor] ?? 1) - 1;
if (weights[soldColor] <= 0) delete weights[soldColor];
const b = zoo.botState;
if (b) b.coins += getAverageSellValueForEggType(soldColor);
return true;
}
/**
* @param {import("./types.js").GameState} state
* @param {import("./types.js").WorldZooEntry} zoo
* @param {() => number} rng
* @param {{ spendThreshold: number }} params
* @returns {void}
*/
function botDecideBuy(state, zoo, rng, params) {
const b = zoo.botState;
if (!b) return;
const spendThreshold = params.spendThreshold;
const colorNames = getColorNames();
const neighborWeights = getNeighborColorWeights(state, zoo.id);
const weights = zoo.animalWeights ?? {};
const eligibleTypes = colorNames.filter((c) => {
const def = LootTables.EggTypes[c];
return def && b.conveyorLevel >= def.minConveyorLevel;
});
if (eligibleTypes.length === 0) return;
const neighborEntries = eligibleTypes.map((c) => ({
id: c,
weight: Math.max(1, (neighborWeights[c] ?? 0) + (weights[c] ?? 0) * 2),
}));
const totalN = neighborEntries.reduce((s, e) => s + e.weight, 0);
if (totalN <= 0) return;
const eggType = pickId(rng, neighborEntries);
const eggDef = LootTables.EggTypes[eggType];
if (!eggDef || b.coins < eggDef.price * spendThreshold) return;
b.coins -= eggDef.price;
weights[eggType] = (weights[eggType] ?? 0) + 1;
}
/**
* One decision tick for one bot: possibly buy egg, sell "animal", or upgrade.
* @param {import("./types.js").GameState} state
* @param {import("./types.js").WorldZooEntry} zoo
* @param {number} nowUnix
*/
function tickBotDecisions(state, zoo, nowUnix) {
const b = zoo.botState;
if (!b) return;
const cfg = GameConfig.Bots || {};
const minInterval = cfg.TickIntervalMinSeconds ?? 8;
const maxInterval = cfg.TickIntervalMaxSeconds ?? 25;
if (nowUnix - b.lastTickAt < minInterval) return;
const rng = () => Math.random();
const intervalRange = maxInterval - minInterval + 1;
const nextInterval = minInterval + Math.floor(rng() * intervalRange);
if (nowUnix - b.lastTickAt < nextInterval) return;
b.lastTickAt = nowUnix;
const profileId = LEGACY_PROFILE_TO_ID[b.profile] ?? 25;
const params = getProfileParams(profileId);
if (botDecideUpgrade(state, zoo, { b, rng, params })) return;
if (botDecideSell(zoo, rng, params)) return;
botDecideBuy(state, zoo, rng, params);
}
/**
* Run income and decision ticks for all bot zoos.
* @param {import("./types.js").GameState} state
* @param {number} nowUnix
* @param {number} dt seconds since last tick
*/
export function tickBotZoos(state, nowUnix, dt) {
const zoos = state.worldZoos ?? [];
for (const zoo of zoos) {
if (zoo.id !== "player") {
ensureBotState(zoo, false);
tickBotIncome(zoo, dt);
tickBotDecisions(state, zoo, nowUnix);
}
}
}
const PLAYER_AUTO_MIN_INTERVAL = 10;
const PLAYER_AUTO_MAX_INTERVAL = 28;
/**
* Apply one upgrade (plot, skill, truck) for player auto mode.
* @param {import("./types.js").GameState} state
* @param {{ spendThreshold: number, upgradeChance: number }} params
* @param {() => number} rng
* @returns {void}
*/
function playerAutoDoOneUpgrade(state, params, rng) {
const { spendThreshold, upgradeChance } = params;
const plotCost = getPlotUpgradeCost(state.plotLevel ?? 1);
const plotMax = GameConfig.Plot?.MaxLevel ?? 8;
const skillCost = getConveyorUpgradeCost(state.conveyorLevel ?? 1);
const skillMax = GameConfig.Conveyor?.MaxLevel ?? 8;
const truckCost = getTruckUpgradeCost(state.truckLevel ?? 1);
const truckMax = (GameConfig.Truck && GameConfig.Truck.MaxLevel) ?? 5;
const canPlot = (state.plotLevel ?? 1) < plotMax && state.coins >= plotCost * spendThreshold;
const canSkill = (state.conveyorLevel ?? 1) < skillMax && state.coins >= skillCost * spendThreshold;
const canTruck = (state.truckLevel ?? 1) < truckMax && state.coins >= truckCost * spendThreshold;
const choices = [];
if (canPlot) choices.push("plot");
if (canSkill) choices.push("skill");
if (canTruck) choices.push("truck");
if (choices.length === 0) return;
if (rng() >= upgradeChance) return;
const choice = choices[Math.floor(rng() * choices.length)];
if (choice === "plot") tryUpgradePlot(state);
else if (choice === "skill") tryUpgrade(state);
else if (choice === "truck") tryUpgradeTruck(state);
}
/**
* When auto mode is on, apply one bot-style upgrade decision for the player (plot, conveyor, or truck).
* @param {import("./types.js").GameState} state
* @param {number} nowUnix
* @returns {void}
*/
export function tickPlayerAutoMode(state, nowUnix) {
if (!state.autoMode) return;
const cfg = GameConfig.Bots || {};
const minInterval = cfg.TickIntervalMinSeconds ?? PLAYER_AUTO_MIN_INTERVAL;
const maxInterval = cfg.TickIntervalMaxSeconds ?? PLAYER_AUTO_MAX_INTERVAL;
const last = state.lastPlayerAutoTickAt ?? 0;
if (nowUnix - last < minInterval) return;
const rng = () => Math.random();
const nextInterval = minInterval + Math.floor(rng() * (maxInterval - minInterval + 1));
if (nowUnix - last < nextInterval) return;
state.lastPlayerAutoTickAt = nowUnix;
const profileId = getEffectiveProfileId(state);
const params = getProfileParams(profileId);
playerAutoDoOneUpgrade(state, params, rng);
}