"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; });