diff --git a/NEWS b/NEWS index 074df2bf8..0b7e290d1 100644 --- a/NEWS +++ b/NEWS @@ -78,6 +78,8 @@ DFHack future replaces autoSyndrome reaction-trigger-transition.lua a tool for converting mods from autoSyndrome to reaction-trigger + random-trigger.lua + triggers random scripts that you register skill-change.lua for incrementing and setting skills spawn-flow.lua diff --git a/scripts/modtools/random-trigger.lua b/scripts/modtools/random-trigger.lua new file mode 100644 index 000000000..50d32b930 --- /dev/null +++ b/scripts/modtools/random-trigger.lua @@ -0,0 +1,171 @@ +--scripts/modtools/random-trigger.lua +--triggers random scripts +--register a few scripts, then tell it to "go" and it will pick a random one based on the probability weights you specified. outcomes are mutually exclusive. To make independent random events, call the script multiple times. + +local utils = require 'utils' +local eventful = require 'plugins.eventful' + +outcomeLists = outcomeLists or {} +randomGen = randomGen or dfhack.random.new() + +eventful.enableEvent(eventful.eventType.UNLOAD, 1) +eventful.onUnload.randomTrigger = function() + outcomeLists = {} +end + +validArgs = validArgs or utils.invert({ + 'help', + 'command', + 'outcomeListName', + 'weight', + 'seed', + 'trigger', + 'preserveList', + 'withProbability', + 'listOutcomes', + 'clear', +}) + +local function triggerEvent(outcomeListName) + local outcomeList = outcomeLists[outcomeListName] + local r = randomGen:random(outcomeList.total) + local sum = 0 + --print ('r = ' .. r) + for i,outcome in ipairs(outcomeList.outcomes) do + sum = sum + outcome.weight + if sum > r then + local temp = outcome.command + --print('triggering outcome ' .. i .. ': "' .. table.concat(temp, ' ') .. '"') + --dfhack.run_command(table.unpack(temp)) + dfhack.run_script(table.unpack(temp)) + break + else + --print ('sum = ' .. sum .. ' <= r = ' .. r) + end + end + --print('Done.') + --dfhack.print('\n') +end + +local args = utils.processArgs({...}, validArgs) + +if args.help then + print([[scripts/modtools/random-trigger.lua +Allows mutually-exclusive random events. Register a list of scripts along with positive integer relative weights, then tell the script to select one of them with the specified probabilities and run it. +The weights must be positive integers, but they do NOT have to sum to 100 or any other particular number. +The outcomes are mutually exclusive: only one will be triggered. +If you want multiple independent random events, call the script multiple times. +99% of the time, you won't need to worry about this, but just in case, you can specify a name of a list of outcomes to prevent interference from other scripts that call this one. +That also permits situations where you don't know until runtime what outcomes you want. +For example, you could make a reaction-trigger that registers the worker as a mayor candidate, then run this script to choose a random mayor from the list of units that did the mayor reaction. + +arguments: + -help + print this help message + -outcomeListName name + specify the name of this list of outcomes to prevent interference if two scripts are registering outcomes at the same time + if none is specified, the default outcome list is selected automatically + -command [ commandStrs ] + specify the command to be run if this outcome is selected + must be specified unless the -trigger argument is given + -weight n + the relative probability weight of this outcome + n must be a non-negative integer + if not specified, n=1 is used by default + -trigger + selects a random script based on the specified outcomeList (or the default one if none is specified) + -preserveList + when combined with trigger, preserves the list of outcomes so you don't have to register them again + it is extremely highly recommended that you always specify the outcome list name when you give this command to prevent almost certain interference + if you want to trigger one of 5 outcomes three times, you might want this option even without -outcomeListName + most of the time, you won't want this + will NOT be preserved after the user saves/loads (ask expwnent if you want this: it's not that hard but if nobody wants it I won't bother) + performance will be slightly faster if you preserve the outcome lists when possible and trigger them multiple times instead of reregistering each time, but the effect should be small + -withProbability p + p is a real number between 0 and 1 inclusive + triggers the command immediately with this probability + -seed s + sets the random seed (guarantees the same sequence of random numbers will be produced internally) + use for debugging purposes + -listOutcomes + lists the currently registered list of outcomes of the outcomeList along with their probability weights + use for debugging purposes + -clear + unregister everything +]]) + return +end + +if args.clear then + outcomeLists = {} +end + +if args.weight and not tonumber(args.weight) then + error ('Invalid weight: ' .. args.weight) +end +args.weight = (args.weight and tonumber(args.weight)) or 1 +if args.weight ~= math.floor(args.weight) then + error 'Noninteger weight.' +end +if args.weight < 0 then + error 'invalid weight: must be non-negative' +end + +if args.seed then + randomGen:init(tonumber(args.seed), 37) --37 is probably excessive and definitely arbitrary +end + +args.outcomeListName = args.outcomeListName or '' +args.outcomeListName = 'outcomeList ' .. args.outcomeListName + +if args.withProbability then + args.withProbability = tonumber(args.withProbability) + if not args.withProbability or args.withProbability < 0 or args.withProbability > 1 then + error('Invalid withProbability: ' .. (args.withProbability or 'nil')) + end + if randomGen:drandom() < args.withProbability then + dfhack.run_command(table.unpack(args.command)) + end +end + +if args.trigger then + triggerEvent(args.outcomeListName) + if not args.preserveList then + outcomeLists[args.outcomeListName] = nil + end + return +end + +if args.listOutcomes then + local outcomeList = outcomeLists[args.outcomeListName] + if not outcomeList then + print ('No outcomes registered.') + return + end + print ('Total weight: ' .. outcomeList.total) + for _,outcome in ipairs(outcomeList.outcomes) do + print(' outcome weight ' .. outcome.weight .. ': ' .. table.concat(outcome.command, ' ')) + end + print('\n') + return +end + +if not args.command then + return +end + +--actually register +local outcomeList = outcomeLists[args.outcomeListName] +if not outcomeList then + outcomeLists[args.outcomeListName] = {} + outcomeList = outcomeLists[args.outcomeListName] +end + +outcomeList.total = args.weight + (outcomeList.total or 0) +local outcome = {} +outcome.weight = args.weight +outcome.command = args.command +outcomeList.outcomes = outcomeList.outcomes or {} +table.insert(outcomeList.outcomes, outcome) + +