tarinai/scripts/achievement_audit.js
2026-07-16 22:12:03 +09:00

675 lines
42 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.65",
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",
"low_fps_single_digit", "continuous_play_24_hours", "sandbox_five_toilets",
"park_ground_changed", "ground_change_4_in_1_second",
"ant_nest_without_tarinai", "sticky_bomb_15_passes",
"daily_play_7_days", "wire_shock_7_tarinai", "information_industry",
];
(async function run() {
let h = createHarness();
let storage;
assert(h.api.definitions.length === 64, "definition count must be 64");
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");
assert(titles.low_fps_single_digit === "スペックがたりない", "low-fps title mismatch");
assert(titles.continuous_play_24_hours === "寝ろ", "24-hour title mismatch");
assert(titles.sandbox_five_toilets === "砂場", "sandbox title mismatch");
assert(titles.park_ground_changed === "そこ公共空間だよ?", "park-ground title mismatch");
assert(titles.ground_change_4_in_1_second === "破格の工事費用", "ground-spam title mismatch");
assert(titles.ant_nest_without_tarinai === "アリ観察", "ant observation title mismatch");
assert(titles.sticky_bomb_15_passes === "命のバトン", "sticky-bomb relay title mismatch");
assert(titles.daily_play_7_days === "毎日たりない観察", "daily observation title mismatch");
assert(titles.wire_shock_7_tarinai === "抵抗器じゃない", "wire shock 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(descriptions.idle_observer_5_minutes === "5分間画面を見るだけ", "observer description mismatch");
assert(descriptions.safe_colony_25_5_minutes.includes("寿命以外で"), "safe-colony lifespan exclusion description mismatch");
assert(descriptions.pause_spam_4_in_1_second.includes("何もない部分"), "empty-click description mismatch");
assert(descriptions.low_fps_single_digit === "fpsを1桁にする。", "low-fps description mismatch");
assert(descriptions.park_ground_changed === "公園の地面を勝手に変更する。", "park-ground description mismatch");
assert(descriptions.ground_change_4_in_1_second === "地面を変えすぎて破産する。", "ground-spam description mismatch");
assert(descriptions.sauna_cold_plunge === "暑すぎる状態になったたりないを、15秒以内に寒すぎる状態にする。", "sauna description mismatch");
assert(descriptions.ant_nest_without_tarinai === "アリの巣があるが、たりないがいない。", "ant observation description mismatch");
assert(descriptions.sticky_bomb_15_passes === "粘着ボムを爆発までに15回以上パスする。", "sticky-bomb relay 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 lowFpsCard = cards.find(node => node.dataset.achievementId === "low_fps_single_digit");
const sleepCard = cards.find(node => node.dataset.achievementId === "continuous_play_24_hours");
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, lowFpsCard, sleepCard]) {
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");
const operationIds = h.api.categories.find(category => category.id === "operation").ids;
assert(JSON.stringify(operationIds.slice(-2)) === JSON.stringify(["continuous_play_1_hour", "true_tarinai_observer"]), "observation achievements are not at the bottom of その他");
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 lifespanSafeAlive = Array.from({ length: 25 }, () => ({ dead: false }));
const lifespanSafeWorld = { time: 100, tarinai: lifespanSafeAlive, achievementNoDeathStartAt: 0 };
h.api.recordDeath({ world: lifespanSafeWorld, reason: "天寿を全うした", tarinai: { deathReason: "天寿を全うした" } });
assert(lifespanSafeWorld.achievementNoDeathStartAt === 0, "lifespan death reset safe-colony timer");
h.api.recordDeath({ world: lifespanSafeWorld, reason: "事故", tarinai: { deathReason: "事故" } });
assert(lifespanSafeWorld.achievementNoDeathStartAt === 100, "non-lifespan death did not reset safe-colony timer");
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();
const antObservationWorld = { tarinai: [], ants: [], items: [] };
h.api.evaluateWorld(antObservationWorld, {});
assert(!unlocked(h, "ant_nest_without_tarinai"), "ant observation unlocked without an ant nest");
antObservationWorld.items.push({ type: "ant_nest", dead: false });
antObservationWorld.tarinai.push({ dead: false });
h.api.evaluateWorld(antObservationWorld, {});
assert(!unlocked(h, "ant_nest_without_tarinai"), "ant observation unlocked while a tarinai was alive");
antObservationWorld.tarinai[0].dead = true;
h.api.evaluateWorld(antObservationWorld, {});
assert(unlocked(h, "ant_nest_without_tarinai"), "ant observation did not unlock with a nest and zero living tarinai");
h = createHarness();
const stickyBomb = { type: "sticky_bomb", dead: false };
for (let i = 0; i < 14; i += 1) h.api.recordStickyBombPass(stickyBomb, {});
assert(!unlocked(h, "sticky_bomb_15_passes"), "sticky-bomb relay unlocked before 15 passes");
h.api.recordStickyBombPass(stickyBomb, {});
assert(unlocked(h, "sticky_bomb_15_passes"), "sticky-bomb relay did not unlock at 15 passes");
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.recordEmptyClick({ now }));
assert(!unlocked(h, "pause_spam_4_in_1_second"), "empty-click achievement unlocked before four clicks");
h.api.recordEmptyClick({ now: 999 });
assert(unlocked(h, "pause_spam_4_in_1_second"), "empty-click achievement did not unlock within one second");
h = createHarness();
[0, 400, 800, 1201].forEach(now => h.api.recordEmptyClick({ now }));
assert(!unlocked(h, "pause_spam_4_in_1_second"), "empty-click achievement 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");
assert(!unlocked(h, "continuous_play_24_hours"), "24-hour play unlocked at one hour");
h.api.recordContinuousPlay({ elapsedMs: 86399999 });
assert(!unlocked(h, "continuous_play_24_hours"), "24-hour play unlocked early");
h.api.recordContinuousPlay({ elapsedMs: 86400000 });
assert(unlocked(h, "continuous_play_24_hours"), "24-hour play did not unlock");
h = createHarness();
h.api.recordLowFps({ fps: 10 });
assert(!unlocked(h, "low_fps_single_digit"), "low-fps achievement unlocked at 10 fps");
h.api.recordLowFps({ fps: 9 });
assert(unlocked(h, "low_fps_single_digit"), "low-fps achievement did not unlock at 9 fps");
h = createHarness();
const sandboxWorld = { items: Array.from({ length: 4 }, () => ({ type: "toilet", dead: false })) };
h.api.evaluateSandbox(sandboxWorld);
assert(!unlocked(h, "sandbox_five_toilets"), "sandbox unlocked before five toilet sands");
sandboxWorld.items.push({ type: "toilet", dead: false });
h.api.evaluateSandbox(sandboxWorld);
assert(unlocked(h, "sandbox_five_toilets"), "sandbox did not unlock at five toilet sands");
h = createHarness();
h.api.recordGroundChange({ world: { fieldType: "garden" }, previous: "soil", next: "ice", now: 0 });
assert(!unlocked(h, "park_ground_changed"), "park-ground achievement unlocked outside park");
h.api.recordGroundChange({ world: { fieldType: "park" }, previous: "soil", next: "ice", now: 100 });
assert(unlocked(h, "park_ground_changed"), "park-ground achievement did not unlock in park");
h = createHarness();
const groundWorld = { fieldType: "garden" };
[0, 250, 500].forEach((now, index) => h.api.recordGroundChange({ world: groundWorld, previous: `g${index}`, next: `g${index + 1}`, now }));
assert(!unlocked(h, "ground_change_4_in_1_second"), "ground-spam achievement unlocked before four changes");
h.api.recordGroundChange({ world: groundWorld, previous: "g3", next: "g4", now: 999 });
assert(unlocked(h, "ground_change_4_in_1_second"), "ground-spam achievement did not unlock within one second");
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 64 achievement definitions, categorized UI, inline disclosures, and recovery synchronization passed");
})().catch(error => {
console.error(error);
process.exitCode = 1;
});