tarinai/scripts/achievement_audit.js
2026-07-15 16:27:56 +09:00

590 lines
35 KiB
JavaScript
Raw 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.

"use strict";
const fs = require("fs");
const vm = require("vm");
const path = require("path");
const source = fs.readFileSync(path.join(__dirname, "..", "js", "achievements.js"), "utf8");
class ClassList {
constructor() { this.values = new Set(["hidden"]); }
add(...names) { names.forEach(name => this.values.add(name)); }
remove(...names) { names.forEach(name => this.values.delete(name)); }
contains(name) { return this.values.has(name); }
toggle(name, force) {
const next = force === undefined ? !this.values.has(name) : Boolean(force);
if (next) this.values.add(name); else this.values.delete(name);
return next;
}
}
class Element {
constructor(id = "", tagName = "div") {
this.tagName = String(tagName || "div").toUpperCase();
this.open = false;
this.id = id;
this.className = "";
this.classList = new ClassList();
this.dataset = {};
this.styles = new Map();
this.style = {
setProperty: (name, value) => this.styles.set(name, value),
removeProperty: name => this.styles.delete(name),
};
this.children = [];
this.textContent = "";
this.tabIndex = -1;
this.isConnected = true;
}
append(...children) { this.children.push(...children); }
replaceChildren(...children) { this.children = children; }
setAttribute() {}
addEventListener() {}
focus() {}
getBoundingClientRect() { return { left: 0, top: 0, width: 100, height: 30 }; }
get offsetWidth() { return 100; }
}
function createHarness(existingStorage = null, search = "", options = {}) {
const storage = existingStorage || new Map();
const elements = new Map();
const ids = [
"achievementsBtn", "achievementButtonCount", "achievementsDialog", "achievementDialogCount",
"achievementsCloseBtn", "achievementsCloseIconBtn", "achievementRefreshBtn", "achievementResetBtn", "achievementUnlockAllBtn",
"achievementList", "achievementSharedStatus", "achievementToast", "achievementToastTitle", "pauseBtn",
];
ids.forEach(id => elements.set(id, new Element(id)));
elements.get("achievementsDialog").classList.add("hidden");
elements.get("achievementToast").classList.add("hidden");
let timerId = 0;
const context = {
console, Date, Intl, Math, JSON, Object, Array, Map, Set, WeakMap, Promise,
Number, String, Boolean, Error, AbortController, encodeURIComponent, URLSearchParams,
location: { protocol: options.protocol || "file:", search },
crypto: { randomUUID: () => "11111111-1111-4111-8111-111111111111" },
localStorage: {
getItem(key) { return storage.has(key) ? storage.get(key) : null; },
setItem(key, value) { storage.set(key, String(value)); },
removeItem(key) { storage.delete(key); },
},
document: {
hidden: false,
getElementById(id) { return elements.get(id) || null; },
createElement(tagName) { return new Element("", tagName); },
addEventListener() {},
},
TarinaiEvents: { emit() {} },
showToast() {},
audio: { uiClick() {} },
setTimeout() { timerId += 1; return timerId; },
clearTimeout() {},
setInterval() { timerId += 1; return timerId; },
clearInterval() {},
fetch: options.fetch || (async () => { throw new Error("offline"); }),
TARINAI_VERSION: "39.16.60",
TarinaiGameDialogs: { confirm: async () => true },
};
context.window = context;
context.globalThis = context;
vm.createContext(context);
vm.runInContext(source, context, { filename: "achievements.js" });
return { context, api: context.TarinaiAchievements, storage, elements };
}
function assert(condition, message) { if (!condition) throw new Error(message); }
function unlocked(h, id) { return h.api.isUnlocked(id); }
const expectedIds = [
"first_birth", "natural_zunchi_slave", "natural_tarinai_king", "self_zunchi_death",
"natural_zunchi_slave_5_generations", "natural_tarinai_king_3_generations",
"death_50_in_10_seconds", "birth_50_in_60_seconds", "great_mother_1000_births", "lifespan_completed",
"soccer_ball_death", "fight_pair_danger_kill", "all_non_sleep_diseased_25", "colony_happy", "direct_feed_33",
"ignite_during_birth_ritual", "laxative_starvation", "idle_observer_5_minutes",
"placed_objects_100", "mechanized_industry", "safe_colony_25_5_minutes",
"ants_alive_25", "ants_killed_100", "secret_collection_9_slots",
"pause_spam_4_in_1_second", "continuous_play_1_hour", "below_absolute_zero_item",
"undo_mass_revival", "robot_cleaner_100", "held_30_seconds", "minimalist_happy",
"overprotective", "self_sufficient", "unplanned_city_30", "sauna_cold_plunge",
"rain_shelter_all", "medicine_ledger_all", "mercury_lifespan", "enemy_enemy_friend",
"revolution", "fuel_to_fire", "undo_20", "redo_20",
"eternal_history_generation_10", "king_full_satisfaction", "slave_zero_satisfaction", "town_doctor_50", "poke_plushie_fling",
"chaos_seeker_666_fights", "clean_freak_robot_only", "true_tarinai_observer",
"sniper_333_shots", "megalopolis", "fertility_seeker_721_love_births",
];
(async function run() {
let h = createHarness();
let storage;
assert(h.api.definitions.length === 54, "definition count must be 54");
assert(JSON.stringify(h.api.definitions.map(d => d.id)) === JSON.stringify(expectedIds), "definition IDs mismatch");
const categorizedIds = h.api.categories.flatMap(category => category.ids);
assert(categorizedIds.length === expectedIds.length, "achievement category count mismatch");
assert(new Set(categorizedIds).size === expectedIds.length, "achievement category contains duplicates");
assert(expectedIds.every(id => categorizedIds.includes(id)), "achievement category misses an ID");
h.api.open();
const titles = Object.fromEntries(h.api.definitions.map(def => [def.id, def.title]));
assert(titles.natural_zunchi_slave === "ずんちどれい", "slave title mismatch");
assert(titles.natural_tarinai_king === "たりない王", "king title mismatch");
assert(titles.self_zunchi_death === "あぁ^~ たまらねぇぜ", "self-zunchi title mismatch");
assert(titles.natural_zunchi_slave_5_generations === "末代までの恥", "five-generation title mismatch");
assert(titles.natural_tarinai_king_3_generations === "華麗なる王族", "three-generation title mismatch");
assert(titles.death_50_in_10_seconds === "6番目の大量絶滅", "mass-extinction title mismatch");
assert(titles.birth_50_in_60_seconds === "ベビーブーム", "baby-boom title mismatch");
assert(titles.idle_observer_5_minutes === "監視カメラ", "observer title mismatch");
assert(titles.laxative_starvation === "内臓全部出た", "laxative title mismatch");
assert(titles.colony_happy === "管理された楽園", "happy colony title mismatch");
assert(titles.soccer_ball_death === "豆野、殺ッカーやろうぜ!", "soccer title mismatch");
assert(titles.ants_alive_25 === "アリの惑星", "ant planet title mismatch");
assert(titles.all_non_sleep_diseased_25 === "緊急事態宣言", "emergency title mismatch");
assert(titles.secret_collection_9_slots === "秘密のコレクション", "save-slot title mismatch");
assert(titles.pause_spam_4_in_1_second === "何やってるの?", "pause-spam title mismatch");
assert(titles.continuous_play_1_hour === "めっちゃたりない観察", "continuous-play title mismatch");
assert(titles.below_absolute_zero_item === "物理法則を下側に超越", "absolute-zero title mismatch");
assert(titles.ignite_during_birth_ritual === "燃え上がる恋", "ritual ignition title mismatch");
assert(titles.safe_colony_25_5_minutes === "安全第一", "safe colony title mismatch");
assert(titles.great_mother_1000_births === "大いなる母", "great mother title mismatch");
assert(titles.fight_pair_danger_kill === "喧嘩両成敗", "fight-pair title mismatch");
assert(titles.direct_feed_33 === "餌やりじいさん", "feeding title mismatch");
assert(titles.eternal_history_generation_10 === "悠久の歴史", "generation title mismatch");
assert(titles.king_full_satisfaction === "五体満足", "king effect title mismatch");
assert(titles.slave_zero_satisfaction === "零体満足", "slave effect title mismatch");
assert(titles.chaos_seeker_666_fights === "混沌ヲ希求スル者", "chaos seeker title mismatch");
assert(titles.clean_freak_robot_only === "潔癖症", "clean freak title mismatch");
assert(titles.true_tarinai_observer === "真・たりない観察", "completionist title mismatch");
assert(titles.sniper_333_shots === "狙撃手", "sniper title mismatch");
assert(titles.megalopolis === "メガロポリス", "megalopolis title mismatch");
assert(titles.fertility_seeker_721_love_births === "豊穣ヲ希求スル者", "fertility title mismatch");
const descriptions = Object.fromEntries(h.api.definitions.map(def => [def.id, def.description]));
assert(descriptions.natural_zunchi_slave === "ずんちどれいが出現する。", "slave description mismatch");
assert(descriptions.natural_tarinai_king === "たりない王が出現する。", "king description mismatch");
assert(descriptions.self_zunchi_death === "おしり鋲を抜いた時、自分のずんちで自爆する。", "self-zunchi description mismatch");
assert(descriptions.natural_zunchi_slave_5_generations === "5代連続でずんちどれい鎖なし", "five-generation description mismatch");
assert(descriptions.natural_tarinai_king_3_generations === "3代連続でたりない王王冠なし", "three-generation description mismatch");
assert(descriptions.great_mother_1000_births.includes("3333体"), "great mother description threshold mismatch");
assert(descriptions.robot_cleaner_100.includes("200個"), "cleaner description threshold mismatch");
assert(descriptions.minimalist_happy.includes("6日目以降"), "minimalist start-day description mismatch");
assert(descriptions.chaos_seeker_666_fights.includes("けんか餅") && descriptions.chaos_seeker_666_fights.includes("666回"), "chaos seeker description mismatch");
assert(descriptions.sniper_333_shots.includes("333発"), "sniper description mismatch");
assert(descriptions.megalopolis.includes("10個") && descriptions.megalopolis.includes("15個") && descriptions.megalopolis.includes("100体"), "megalopolis description mismatch");
assert(descriptions.fertility_seeker_721_love_births.includes("へこ餅") && descriptions.fertility_seeker_721_love_births.includes("721回"), "fertility description mismatch");
assert(!source.includes("TarinaiTooltips"), "achievement tooltips were not removed");
assert(source.includes("PRE_UNLOCK_DESCRIPTION_IDS"), "pre-unlock disclosure allowlist is missing");
function walk(node, out = []) { for (const child of node?.children || []) { out.push(child); walk(child, out); } return out; }
const cards = walk(h.elements.get("achievementList")).filter(node => node.dataset?.achievementId);
const firstBirthCard = cards.find(node => node.dataset.achievementId === "first_birth");
const disclosedSlaveCard = cards.find(node => node.dataset.achievementId === "natural_zunchi_slave");
const disclosedKingCard = cards.find(node => node.dataset.achievementId === "natural_tarinai_king");
const disclosedRevolutionCard = cards.find(node => node.dataset.achievementId === "revolution");
const disclosedLaxativeCard = cards.find(node => node.dataset.achievementId === "laxative_starvation");
const eternalHistoryCard = cards.find(node => node.dataset.achievementId === "eternal_history_generation_10");
const hiddenCard = cards.find(node => node.dataset.achievementId === "self_zunchi_death");
assert(firstBirthCard.children[1].children[0].children[1].textContent === descriptions.first_birth, "disclosed locked condition is not shown in the UI");
for (const card of [disclosedSlaveCard, disclosedKingCard, disclosedRevolutionCard, disclosedLaxativeCard, eternalHistoryCard]) {
assert(card.children[1].children[0].children[1].textContent !== "", "requested pre-unlock condition is hidden");
}
assert(hiddenCard.children[1].children[0].children[1].textContent === "", "undisclosed locked condition is not hidden as question marks");
const groups = h.elements.get("achievementList").children;
assert(groups.length === 4, "achievement UI must contain exactly four category groups");
assert(JSON.stringify(groups.map(group => group.dataset.achievementGroup)) === JSON.stringify(["ecology", "experiment", "construction", "operation"]), "achievement category order mismatch");
assert(groups.every(group => group.tagName === "DETAILS"), "achievement categories are not collapsible details");
const categoryTitles = h.api.categories.map(category => category.title);
assert(JSON.stringify(categoryTitles) === JSON.stringify(["生態", "実験", "建築", "その他"]), "achievement category titles mismatch");
h.api.unlock("natural_zunchi_slave");
const rerenderedGroups = h.elements.get("achievementList").children;
const ecologyCards = walk(rerenderedGroups[0]).filter(node => node.dataset?.achievementId);
const allRenderedCards = rerenderedGroups.flatMap(group => walk(group).filter(node => node.dataset?.achievementId));
assert(ecologyCards[0]?.dataset.achievementId === "natural_zunchi_slave", "unlocked achievement was not moved to the top of its category");
assert(allRenderedCards.filter(card => card.dataset.achievementId === "natural_zunchi_slave").length === 1, "unlocked achievement is duplicated across categories");
assert(await h.api.reset(), "in-game achievement reset confirmation was not accepted");
assert(!h.api.isUnlocked("natural_zunchi_slave"), "achievement reset did not clear unlocked state");
assert(source.includes('card.style.setProperty("--achievement-rate"'), "achievement percentage bar is not wired");
h = createHarness();
h.api.recordBirth({ world: { time: 0 } });
assert(unlocked(h, "first_birth"), "first birth did not unlock");
h = createHarness();
const birthWorld = { time: 0 };
for (let i = 0; i < 29; i += 1) { birthWorld.time = i; h.api.recordBirth({ world: birthWorld }); }
assert(!unlocked(h, "birth_50_in_60_seconds"), "rapid birth unlocked before 30");
birthWorld.time = 59;
h.api.recordBirth({ world: birthWorld });
assert(unlocked(h, "birth_50_in_60_seconds"), "rapid birth did not unlock at 30");
storage = new Map();
h = createHarness(storage);
for (let i = 0; i < 2000; i += 1) h.api.recordBirth({ world: { time: i % 61 } });
assert(!unlocked(h, "great_mother_1000_births"), "great mother unlocked before 3333");
h = createHarness(storage);
for (let i = 0; i < 1332; i += 1) h.api.recordBirth({ world: { time: i % 61 } });
assert(!unlocked(h, "great_mother_1000_births"), "great mother unlocked at 3332");
h.api.recordBirth({ world: { time: 1 } });
assert(unlocked(h, "great_mother_1000_births"), "great mother did not persist to 3333");
h = createHarness();
const deathWorld = { time: 0 };
for (let i = 0; i < 49; i += 1) { deathWorld.time = i * 0.19; h.api.recordDeath({ world: deathWorld, reason: "accident" }); }
assert(!unlocked(h, "death_50_in_10_seconds"), "rapid death unlocked before 50");
deathWorld.time = 9.5;
h.api.recordDeath({ world: deathWorld, reason: "accident" });
assert(unlocked(h, "death_50_in_10_seconds"), "rapid death did not unlock at 50");
h = createHarness();
h.api.recordDeath({ world: { time: 1 }, tarinai: {}, reason: "天寿を全うした" });
assert(unlocked(h, "lifespan_completed"), "lifespan did not unlock");
h = createHarness();
h.api.recordDeath({ world: { time: 1 }, tarinai: { _achievementLaxativeActiveThisFrame: true }, reason: "飢餓" });
assert(unlocked(h, "laxative_starvation"), "laxative starvation did not unlock");
h = createHarness();
const slaveWorld = {};
[1, 2, 4, 5, 6].forEach(generation => h.api.recordNaturalZunchiSlave({ world: slaveWorld, tarinai: { generation } }));
assert(!unlocked(h, "natural_zunchi_slave_5_generations"), "slave run ignored generation gap");
h.api.recordNaturalZunchiSlave({ world: slaveWorld, tarinai: { generation: 3 } });
assert(unlocked(h, "natural_zunchi_slave_5_generations"), "slave five-generation run did not unlock");
h = createHarness();
const kingWorld = {};
[4, 5, 6].forEach(generation => h.api.recordNaturalTarinaiKing({ world: kingWorld, tarinai: { generation } }));
assert(unlocked(h, "natural_tarinai_king_3_generations"), "king three-generation run did not unlock");
h = createHarness();
const feedWorldA = { time: 0, achievementDirectFeedCount: 0 };
for (let i = 0; i < 32; i += 1) h.api.recordDirectFeed({ world: feedWorldA });
assert(!unlocked(h, "direct_feed_33"), "direct feed unlocked before 33");
const feedWorldB = { time: 0, achievementDirectFeedCount: 0 };
h.api.recordDirectFeed({ world: feedWorldB });
assert(!unlocked(h, "direct_feed_33"), "direct-feed count survived a field reset");
h.api.recordDirectFeed({ world: feedWorldA });
assert(unlocked(h, "direct_feed_33"), "direct feed did not unlock at 33 in one field");
h = createHarness();
const diseased = Array.from({ length: 25 }, () => ({ fightDisease: true, dead: false }));
h.api.evaluateWorld({ tarinai: diseased, ants: [], time: 0 }, { id: "relaxed" });
assert(unlocked(h, "all_non_sleep_diseased_25"), "disease colony did not unlock");
h = createHarness();
const happyWorld = { tarinai: [{ dead: false }], ants: [], items: [], time: 600, day: 6 };
h.api.evaluateWorld(happyWorld, { id: "happy" });
happyWorld.time = 719.99;
h.api.evaluateWorld(happyWorld, { id: "happy" });
assert(!unlocked(h, "colony_happy"), "happy colony unlocked before one day");
happyWorld.time = 720;
h.api.evaluateWorld(happyWorld, { id: "happy" });
assert(unlocked(h, "colony_happy"), "happy colony did not unlock after one day from day 6");
h = createHarness();
const interruptedHappyWorld = { tarinai: [{ dead: false }], ants: [], items: [], time: 600, day: 6 };
h.api.evaluateWorld(interruptedHappyWorld, { id: "happy" });
interruptedHappyWorld.time = 650;
h.api.evaluateWorld(interruptedHappyWorld, { id: "normal" });
interruptedHappyWorld.time = 700;
h.api.evaluateWorld(interruptedHappyWorld, { id: "happy" });
interruptedHappyWorld.time = 819.99;
h.api.evaluateWorld(interruptedHappyWorld, { id: "happy" });
assert(!unlocked(h, "colony_happy"), "interrupted happiness did not restart the one-day timer");
interruptedHappyWorld.time = 820;
h.api.evaluateWorld(interruptedHappyWorld, { id: "happy" });
assert(unlocked(h, "colony_happy"), "happy colony did not unlock one full day after restart");
h = createHarness();
h.api.recordIgnition({});
h.api.recordSelfZunchiDeath({});
h.api.recordSoccerBallDeath({});
assert(unlocked(h, "ignite_during_birth_ritual"), "ritual ignition failed");
assert(unlocked(h, "self_zunchi_death"), "self zunchi failed");
assert(unlocked(h, "soccer_ball_death"), "soccer death failed");
h = createHarness();
const dangerItem = { id: "danger-1", type: "firecracker", roles: { danger: true } };
const fighterA = { id: "a", dead: true, fightTimer: 2, fightTargetId: "b", fightTargetIds: ["b"] };
const fighterB = { id: "b", dead: false, fightTimer: 2, fightTargetId: "a", fightTargetIds: ["a"] };
const fightWorld = {
time: 5,
tarinai: [fighterA, fighterB],
fightPairKey(a, b) { return [a.id, b.id].sort().join(":"); },
isAlreadyFightingPair() { return true; },
};
fighterA.world = fightWorld; fighterB.world = fightWorld;
h.api.recordDamageSource(fighterA, dangerItem, { world: fightWorld });
h.api.recordDeath({ world: fightWorld, tarinai: fighterA, reason: "爆竹", fromDamage: true });
assert(!unlocked(h, "fight_pair_danger_kill"), "fight pair unlocked after only one death");
fightWorld.time = 6;
fighterB.dead = true; fighterB.fightTimer = 0; fighterB.fightTargetId = null; fighterB.fightTargetIds = [];
h.api.recordDamageSource(fighterB, dangerItem, { world: fightWorld });
h.api.recordDeath({ world: fightWorld, tarinai: fighterB, reason: "爆竹", fromDamage: true });
assert(unlocked(h, "fight_pair_danger_kill"), "fight pair danger kill did not unlock");
h = createHarness();
const falsePositiveA = { id: "fa", dead: true, fightTimer: 2, fightTargetId: "fb", fightTargetIds: ["fb"] };
const falsePositiveB = { id: "fb", dead: false, fightTimer: 2, fightTargetId: "fa", fightTargetIds: ["fa"] };
const falsePositiveWorld = {
time: 3,
tarinai: [falsePositiveA, falsePositiveB],
fightPairKey(a, b) { return [a.id, b.id].sort().join(":"); },
isAlreadyFightingPair() { return true; },
};
falsePositiveA.world = falsePositiveWorld; falsePositiveB.world = falsePositiveWorld;
h.api.recordDamageSource(falsePositiveA, dangerItem, { world: falsePositiveWorld });
h.api.recordDeath({ world: falsePositiveWorld, tarinai: falsePositiveA, reason: "飢餓", fromDamage: false });
falsePositiveWorld.time = 4;
falsePositiveB.dead = true;
h.api.recordDamageSource(falsePositiveB, dangerItem, { world: falsePositiveWorld });
h.api.recordDeath({ world: falsePositiveWorld, tarinai: falsePositiveB, reason: "爆竹", fromDamage: true });
assert(!unlocked(h, "fight_pair_danger_kill"), "non-lethal danger damage was misclassified as a danger kill");
h = createHarness();
const generationWorld = { time: 0, maxGeneration: 9 };
h.api.recordBirth({ world: generationWorld, child: { generation: 9, world: generationWorld } });
assert(!unlocked(h, "eternal_history_generation_10"), "generation history unlocked before generation 10");
generationWorld.maxGeneration = 10;
h.api.recordBirth({ world: generationWorld, child: { generation: 10, world: generationWorld } });
assert(unlocked(h, "eternal_history_generation_10"), "generation history did not unlock at generation 10");
h = createHarness();
const king = { dead: false, isTarinaiChampion: true, isZunchiSlave: false, powerItemMode: "protein", sizeItemMode: "", world: {} };
h.api.recordConsumableUse({ tarinai: king, world: king.world, type: "protein" });
assert(!unlocked(h, "king_full_satisfaction"), "king satisfaction unlocked without giant drug");
king.sizeItemMode = "giant_drug";
h.api.recordConsumableUse({ tarinai: king, world: king.world, type: "giant_drug" });
assert(unlocked(h, "king_full_satisfaction"), "king satisfaction did not unlock with both effects");
h = createHarness();
const slave = { dead: false, isTarinaiChampion: false, isZunchiSlave: true, powerItemMode: "niteropu", sizeItemMode: "", world: {} };
h.api.recordConsumableUse({ tarinai: slave, world: slave.world, type: "niteropu" });
assert(!unlocked(h, "slave_zero_satisfaction"), "slave satisfaction unlocked without dwarf drug");
slave.sizeItemMode = "dwarf_drug";
h.api.recordConsumableUse({ tarinai: slave, world: slave.world, type: "dwarf_drug" });
assert(unlocked(h, "slave_zero_satisfaction"), "slave satisfaction did not unlock with both effects");
h = createHarness();
const allExceptCompletionist = expectedIds.filter(id => id !== "true_tarinai_observer");
for (let i = 0; i < allExceptCompletionist.length - 1; i += 1) h.api.unlock(allExceptCompletionist[i]);
assert(!unlocked(h, "true_tarinai_observer"), "completionist unlocked before every other achievement");
h.api.unlock(allExceptCompletionist[allExceptCompletionist.length - 1]);
assert(unlocked(h, "true_tarinai_observer"), "completionist did not unlock with every other achievement");
h = createHarness();
const idleWorld = { time: 300, tarinai: [], ants: [], achievementLastInterventionAt: 0, achievementNoDeathStartAt: -1 };
h.api.evaluateWorld(idleWorld, { id: "relaxed" });
assert(unlocked(h, "idle_observer_5_minutes"), "idle observer did not unlock");
storage = new Map();
h = createHarness(storage);
for (let i = 0; i < 60; i += 1) h.api.recordPlayerPlacement({ world: { time: 1 }, item: { type: "grass" } });
assert(!unlocked(h, "placed_objects_100"), "placement unlocked before 100");
h = createHarness(storage);
for (let i = 0; i < 39; i += 1) h.api.recordPlayerPlacement({ world: { time: 1 }, item: { type: "grass" } });
assert(!unlocked(h, "placed_objects_100"), "placement unlocked at 99");
h.api.recordPlayerPlacement({ world: { time: 1 }, item: { type: "grass" } });
assert(unlocked(h, "placed_objects_100"), "placement did not persist across field reset/reload to 100");
storage = new Map();
h = createHarness(storage);
h.api.recordLinkPlaced("wire", { world: { time: 1 }, item: { type: "wire" } });
h.api.recordLinkPlaced("insulated_wire", { world: { time: 2 }, item: { type: "insulated_wire" } });
const wireProgress = JSON.parse(storage.get("tarinai_achievements_v2")).progress;
assert(wireProgress.placementCount === 2, "wire placements were not counted toward placed objects");
assert(wireProgress.linkTypes.length === 0, "wire placements incorrectly advanced mechanized link types");
h = createHarness();
h.api.recordLinkPlaced("rope", { world: { time: 1 } });
h.api.recordLinkPlaced("rod", { world: { time: 1 } });
h.api.recordLinkPlaced("spring", { world: { time: 1 } });
assert(!unlocked(h, "mechanized_industry"), "mechanized industry unlocked without signal");
h.api.recordSignalActivation({ world: {}, target: { type: "gate_fence" } });
assert(unlocked(h, "mechanized_industry"), "mechanized industry did not unlock after logical AND");
assert(!unlocked(h, "signal_automation_first") && !unlocked(h, "link_craftsman"), "removed achievements remain accessible");
h = createHarness();
const safeAlive = Array.from({ length: 25 }, () => ({ dead: false }));
const safeWorld = { time: 0, tarinai: safeAlive, ants: [], achievementLastInterventionAt: 0, achievementNoDeathStartAt: -1 };
h.api.evaluateWorld(safeWorld, { id: "relaxed" });
safeWorld.time = 300;
h.api.evaluateWorld(safeWorld, { id: "relaxed" });
assert(unlocked(h, "safe_colony_25_5_minutes"), "safe colony did not unlock");
h = createHarness();
const antWorld = { time: 1, tarinai: [], ants: Array.from({ length: 25 }, () => ({ dead: false })), achievementLastInterventionAt: 0, achievementNoDeathStartAt: -1 };
h.api.evaluateWorld(antWorld, { id: "relaxed" });
assert(unlocked(h, "ants_alive_25"), "ant herd did not unlock at 25");
storage = new Map();
h = createHarness(storage);
for (let i = 0; i < 65; i += 1) h.api.recordAntKilled({ world: {} });
assert(!unlocked(h, "ants_killed_100"), "ant extermination unlocked before 100");
h = createHarness(storage);
for (let i = 0; i < 34; i += 1) h.api.recordAntKilled({ world: {} });
assert(!unlocked(h, "ants_killed_100"), "ant extermination unlocked at 99");
h.api.recordAntKilled({ world: {} });
assert(unlocked(h, "ants_killed_100"), "ant extermination did not persist to 100");
h = createHarness();
h.api.recordSaveSlotsFilled({ filled: 8 });
assert(!unlocked(h, "secret_collection_9_slots"), "save-slot collection unlocked before 9");
h.api.recordSaveSlotsFilled({ filled: 9 });
assert(unlocked(h, "secret_collection_9_slots"), "save-slot collection did not unlock at 9");
h = createHarness();
[0, 250, 500].forEach(now => h.api.recordPauseClick({ now }));
assert(!unlocked(h, "pause_spam_4_in_1_second"), "pause-spam unlocked before four clicks");
h.api.recordPauseClick({ now: 999 });
assert(unlocked(h, "pause_spam_4_in_1_second"), "pause-spam did not unlock within one second");
h = createHarness();
[0, 400, 800, 1201].forEach(now => h.api.recordPauseClick({ now }));
assert(!unlocked(h, "pause_spam_4_in_1_second"), "pause-spam unlocked outside one-second window");
h = createHarness();
h.api.recordContinuousPlay({ elapsedMs: 3599999 });
assert(!unlocked(h, "continuous_play_1_hour"), "continuous play unlocked before one hour");
h.api.recordContinuousPlay({ elapsedMs: 3600000 });
assert(unlocked(h, "continuous_play_1_hour"), "continuous play did not unlock at one hour");
h = createHarness();
const thermalWorld = {
time: 0, tarinai: [], ants: [], items: [{ id: "cold", x: 10, y: 20, dead: false }],
temperatureAt() { return -273.15; }, achievementLastInterventionAt: 0, achievementNoDeathStartAt: -1,
};
h.api.evaluateWorld(thermalWorld, { id: "relaxed" });
assert(!unlocked(h, "below_absolute_zero_item"), "absolute-zero achievement unlocked at exactly absolute zero");
thermalWorld.temperatureAt = () => -273.16;
h.api.evaluateWorld(thermalWorld, { id: "relaxed" });
assert(unlocked(h, "below_absolute_zero_item"), "absolute-zero achievement did not unlock below absolute zero");
storage = new Map();
storage.set("tarinai_achievements_v2", JSON.stringify({
unlocked: { signal_automation_first: 1000, link_craftsman: 2000 },
pending: ["signal_automation_first", "link_craftsman"],
progress: { linkTypes: ["rope", "rod", "spring"] },
}));
h = createHarness(storage);
assert(unlocked(h, "mechanized_industry"), "legacy AND achievements were not migrated");
storage = new Map();
storage.set("tarinai_achievements_v2", JSON.stringify({
unlocked: { link_craftsman: 2000 },
pending: [],
progress: {},
}));
h = createHarness(storage);
h.api.recordSignalActivation({ world: {}, target: { type: "gate_fence" } });
assert(unlocked(h, "mechanized_industry"), "legacy link-craftsman progress was not reconstructed before a later signal");
storage = new Map();
h = createHarness(storage, "?debug=1");
assert(h.elements.get("achievementUnlockAllBtn").hidden === false, "debug all-achievements button was not shown");
h.api.debugUnlockAll();
assert(h.api.definitions.every(def => unlocked(h, def.id)), "debug all-achievements button did not unlock all");
const debugState = JSON.parse(storage.get("tarinai_achievements_v2"));
assert(debugState.pending.length === 0, "debug unlocks were queued for shared statistics");
assert(debugState.debugUnlocked.length === h.api.definitions.length, "debug unlocks were not marked as local-only");
h = createHarness(storage, "");
assert(h.api.definitions.every(def => !unlocked(h, def.id)), "debug-only unlocks leaked into normal play");
storage = new Map();
storage.set("tarinai_achievements_v2", JSON.stringify({
unlocked: { first_birth: 1700000000123, lifespan_completed: 1700000000456 },
pending: [],
progress: {},
}));
const recoveryRequests = [];
h = createHarness(storage, "", {
protocol: "https:",
fetch: async (_url, options = {}) => {
recoveryRequests.push(JSON.parse(options.body || "{}"));
return { ok: true, async json() { return { ok: true, totalPlayers: 1, achievements: {} }; } };
},
});
await h.api.refresh();
assert(recoveryRequests.length === 1, "recovery sync did not issue exactly one request");
assert(recoveryRequests[0].action === "sync", "recovery request did not use bulk sync");
assert(recoveryRequests[0].completionistEligible === false, "full recovery sync did not report completionist eligibility");
assert(recoveryRequests[0].unlocked.first_birth === 1700000000123, "recovery sync omitted first birth timestamp");
assert(recoveryRequests[0].unlocked.lifespan_completed === 1700000000456, "recovery sync omitted lifespan timestamp");
h = createHarness();
for (let i = 0; i < 49; i += 1) h.api.recordFirstAidRecovery({ recovered: 10 });
assert(!unlocked(h, "town_doctor_50"), "doctor unlocked before 50 recoveries");
h.api.recordFirstAidRecovery({ recovered: 0 });
assert(!unlocked(h, "town_doctor_50"), "zero recovery counted for doctor");
h.api.recordFirstAidRecovery({ recovered: 1 });
assert(unlocked(h, "town_doctor_50"), "doctor did not unlock at 50 recoveries");
h = createHarness();
h.api.recordPlushiePokeFling({});
assert(unlocked(h, "poke_plushie_fling"), "plushie poke achievement did not unlock");
storage = new Map();
h = createHarness(storage);
const chaosWorld = { time: 0 };
for (let i = 0; i < 700; i += 1) h.api.recordFightStarted({ world: chaosWorld, a: {}, b: {} });
assert(!unlocked(h, "chaos_seeker_666_fights"), "ordinary fights counted toward chaos seeker");
const fightMochiTarinai = { fightMochiTimer: 10 };
for (let i = 0; i < 400; i += 1) h.api.recordFightStarted({ world: chaosWorld, a: fightMochiTarinai, b: {} });
h = createHarness(storage);
for (let i = 0; i < 265; i += 1) h.api.recordFightStarted({ world: chaosWorld, a: fightMochiTarinai, b: {} });
assert(!unlocked(h, "chaos_seeker_666_fights"), "chaos seeker unlocked at 665 influenced fights");
h.api.recordFightStarted({ world: chaosWorld, a: fightMochiTarinai, b: {} });
assert(unlocked(h, "chaos_seeker_666_fights"), "chaos seeker did not persist to 666 influenced fights");
storage = new Map();
h = createHarness(storage);
for (let i = 0; i < 332; i += 1) h.api.recordShotFired({});
assert(!unlocked(h, "sniper_333_shots"), "sniper unlocked before 333 shots");
h = createHarness(storage);
h.api.recordShotFired({});
assert(unlocked(h, "sniper_333_shots"), "sniper did not persist to 333 shots");
storage = new Map();
h = createHarness(storage);
for (let i = 0; i < 800; i += 1) h.api.recordLoveMochiBirth({ loveMochiInfluenced: false });
assert(!unlocked(h, "fertility_seeker_721_love_births"), "ordinary reproduction counted toward fertility seeker");
for (let i = 0; i < 720; i += 1) h.api.recordLoveMochiBirth({ loveMochiInfluenced: true });
assert(!unlocked(h, "fertility_seeker_721_love_births"), "fertility seeker unlocked before 721 influenced reproductions");
h = createHarness(storage);
h.api.recordLoveMochiBirth({ loveMochiInfluenced: true });
assert(unlocked(h, "fertility_seeker_721_love_births"), "fertility seeker did not persist to 721 influenced reproductions");
h = createHarness();
const metroWorld = {
tarinai: Array.from({ length: 99 }, () => ({ dead: false })),
items: [
...Array.from({ length: 10 }, () => ({ type: "duplicator", dead: false })),
...Array.from({ length: 15 }, (_v, i) => ({ type: i % 3 === 0 ? "grass_bed" : (i % 3 === 1 ? "nest_box" : "pipe"), dead: false })),
],
};
h.api.evaluateMegalopolis(metroWorld);
assert(!unlocked(h, "megalopolis"), "megalopolis ignored the 100-tarinai threshold");
metroWorld.tarinai.push({ dead: false });
metroWorld.items[0].dead = true;
h.api.evaluateMegalopolis(metroWorld);
assert(!unlocked(h, "megalopolis"), "megalopolis ignored the 10-duplicator threshold");
metroWorld.items[0].dead = false;
metroWorld.items[10].dead = true;
h.api.evaluateMegalopolis(metroWorld);
assert(!unlocked(h, "megalopolis"), "megalopolis ignored the 15-shelter threshold");
metroWorld.items[10].dead = false;
h.api.evaluateMegalopolis(metroWorld);
assert(unlocked(h, "megalopolis"), "megalopolis did not unlock at exact thresholds");
h = createHarness();
const cleaner = { type: "robot_cleaner", dead: false, robotCleanerMask: 0x3f, robotCleanerHighSpeed: true, _signalDriven: true, _signalOn: false };
const cleanWorld = { items: [cleaner], tarinai: [], ants: [], time: 0 };
h.api.evaluateCleanFreak(cleanWorld);
assert(!unlocked(h, "clean_freak_robot_only"), "clean freak unlocked while signal-powered cleaner was off");
cleaner._signalOn = true;
cleanWorld.tarinai.push({ dead: false });
h.api.evaluateCleanFreak(cleanWorld);
assert(!unlocked(h, "clean_freak_robot_only"), "clean freak ignored a living tarinai");
cleanWorld.tarinai.length = 0;
cleanWorld.items.push({ type: "grass", dead: false });
h.api.evaluateCleanFreak(cleanWorld);
assert(!unlocked(h, "clean_freak_robot_only"), "clean freak ignored another field item");
cleanWorld.items.pop();
h.api.evaluateCleanFreak(cleanWorld);
assert(unlocked(h, "clean_freak_robot_only"), "clean freak did not unlock with all settings and robot-only field");
console.log("[OK] all 54 achievement definitions, categorized UI, inline disclosures, and recovery synchronization passed");
})().catch(error => {
console.error(error);
process.exitCode = 1;
});