tarinai/scripts/achievement_audit.js

675 lines
42 KiB
JavaScript
Raw Normal View History

2026-07-15 14:44:29 +09:00
"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"); }),
2026-07-16 22:12:03 +09:00
TARINAI_VERSION: "39.16.65",
2026-07-15 14:44:29 +09:00
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",
2026-07-15 16:27:56 +09:00
"sniper_333_shots", "megalopolis", "fertility_seeker_721_love_births",
2026-07-16 22:12:03 +09:00
"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",
2026-07-15 14:44:29 +09:00
];
(async function run() {
let h = createHarness();
let storage;
2026-07-16 22:12:03 +09:00
assert(h.api.definitions.length === 64, "definition count must be 64");
2026-07-15 14:44:29 +09:00
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");
2026-07-15 16:27:56 +09:00
assert(titles.sniper_333_shots === "狙撃手", "sniper title mismatch");
assert(titles.megalopolis === "メガロポリス", "megalopolis title mismatch");
assert(titles.fertility_seeker_721_love_births === "豊穣ヲ希求スル者", "fertility title mismatch");
2026-07-16 22:12:03 +09:00
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");
2026-07-15 14:44:29 +09:00
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");
2026-07-15 16:27:56 +09:00
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");
2026-07-16 22:12:03 +09:00
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");
2026-07-15 14:44:29 +09:00
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");
2026-07-16 22:12:03 +09:00
const lowFpsCard = cards.find(node => node.dataset.achievementId === "low_fps_single_digit");
const sleepCard = cards.find(node => node.dataset.achievementId === "continuous_play_24_hours");
2026-07-15 14:44:29 +09:00
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");
2026-07-16 22:12:03 +09:00
for (const card of [disclosedSlaveCard, disclosedKingCard, disclosedRevolutionCard, disclosedLaxativeCard, eternalHistoryCard, lowFpsCard, sleepCard]) {
2026-07-15 14:44:29 +09:00
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");
2026-07-16 22:12:03 +09:00
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 その他");
2026-07-15 14:44:29 +09:00
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");
2026-07-16 22:12:03 +09:00
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");
2026-07-15 14:44:29 +09:00
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");
2026-07-16 22:12:03 +09:00
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");
2026-07-15 14:44:29 +09:00
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();
2026-07-16 22:12:03 +09:00
[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");
2026-07-15 14:44:29 +09:00
h = createHarness();
2026-07-16 22:12:03 +09:00
[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");
2026-07-15 14:44:29 +09:00
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");
2026-07-16 22:12:03 +09:00
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");
2026-07-15 14:44:29 +09:00
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");
2026-07-15 16:27:56 +09:00
assert(recoveryRequests[0].completionistEligible === false, "full recovery sync did not report completionist eligibility");
2026-07-15 14:44:29 +09:00
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 };
2026-07-15 16:27:56 +09:00
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: {} });
2026-07-15 14:44:29 +09:00
h = createHarness(storage);
2026-07-15 16:27:56 +09:00
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");
2026-07-15 14:44:29 +09:00
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");
2026-07-16 22:12:03 +09:00
console.log("[OK] all 64 achievement definitions, categorized UI, inline disclosures, and recovery synchronization passed");
2026-07-15 14:44:29 +09:00
})().catch(error => {
console.error(error);
process.exitCode = 1;
});