Skip to content

Instantly share code, notes, and snippets.

@chrisuehlinger
Created January 19, 2024 21:47
Show Gist options
  • Save chrisuehlinger/c38fff88b7e429c81c2582430a2c3ab9 to your computer and use it in GitHub Desktop.
Save chrisuehlinger/c38fff88b7e429c81c2582430a2c3ab9 to your computer and use it in GitHub Desktop.
Factorio self-expanding factory algorithm
-- Input Signal Key:
-- - red[some-item] - The number of that item in the logistic network,
-- negative if there is unsatisfied demand
-- - green[ore-type] - The amount of ore appearing on the resource scanner
-- Output Signal Key:
-- - out['red/signal-X'] - The X coordinate to survey
-- - out['red/signal-Y'] - The Y coordinate to survey
-- - out['red/signal-W'] - The width of the survey
-- - out['red/signal-H'] - The height of the survey
-- - out['construction-robot'] - The index of the blueprint to deploy
-- - out['signal-X'] - The X coordinate to deploy the next blueprint
-- - out['signal-Y'] - The Y coordinate to deploy the next blueprint
-- - out['signal-check'] - 1 if now is a good time for construction and research,
-- 0 otherwise
-- (Note: The red wire goes to the resource scanner, the green wire goes to the
-- blueprint deployer and also feeds back to the input)
local MAX_MEGATILES = 484
local MAX_RESEARCH_TILES = 25
local PIN_OFFSET_X = -47
local PIN_OFFSET_Y = 10
local newSignal = 0
local tagSignal = nil
-- Convert the input signals to local variables for readability
local currently_constructed_megatiles = red['signal-info']
local currently_constructed_research_tiles = red['signal-dot']
local available_logistic_bots = red['signal-A']
local total_logistic_bots = red['signal-B']
local available_construction_bots = red['signal-C']
local total_construction_bots = red['signal-D']
local accumulator_charge = red['signal-E']
local lastSignal = green['signal-P']
local busy_construction_bots = total_construction_bots -
available_construction_bots
-- We wait less the larger the factory is,
-- since overproduction in the late game is less catastrophic.
local waiting_on_construction_bots = busy_construction_bots >
(currently_constructed_megatiles / 25) +
1
local currently_in_logistic_shock = available_logistic_bots <
(total_logistic_bots / 10)
-- This will only happen in biter runs
local currently_in_construction_shock = total_construction_bots < 300
-- This combined logic ensures that we keep building power megatiles even if
-- the power situation has temporarily recovered
var.currently_in_power_shock = accumulator_charge < 40 or
(var.currently_in_power_shock and
accumulator_charge < 50)
var.need_power = (var.currently_in_power_shock or var.need_power)
local can_build_tile = not waiting_on_construction_bots and
not currently_in_logistic_shock and
not currently_in_construction_shock
out = {}
-- Each item is in the first tier that does not contain any of its ingredients
local ITEM_TIERS = {
{
{name = 'iron-plate', outSignal = 3},
{name = 'copper-plate', outSignal = 4},
{name = 'stone-brick', outSignal = 5}
}, {
{name = 'iron-gear-wheel', outSignal = 7},
{name = 'copper-cable', outSignal = 10},
{name = 'steel-plate', outSignal = 6},
{name = 'iron-stick', outSignal = 11},
{name = 'pipe', outSignal = 22},
{name = 'grenade', outSignal = 17},
{name = 'stone-wall', outSignal = 19},
{name = 'firearm-magazine', outSignal = 16},
{name = 'stone-furnace', outSignal = 111}
}, {
{name = 'empty-barrel', outSignal = 85},
{name = 'storage-tank', outSignal = 90},
{name = 'heat-pipe', outSignal = 100},
{name = 'rail', outSignal = 27},
{name = 'electronic-circuit', outSignal = 12},
{name = 'transport-belt', outSignal = 13},
{name = 'pipe-to-ground', outSignal = 73},
{name = 'engine-unit', outSignal = 24},
{name = 'heat-exchanger', outSignal = 101},
{name = 'steam-turbine', outSignal = 102},
{name = 'steel-chest', outSignal = 67},
{name = 'automation-science-pack', outSignal = 8},
{name = 'piercing-rounds-magazine', outSignal = 18},
{name = 'boiler', outSignal = 112}
}, {
{name = 'water-barrel', outSignal = 86},
{name = 'electric-mining-drill', outSignal = 91},
{name = 'repair-pack', outSignal = 93},
{name = 'oil-refinery', outSignal = 84},
{name = 'assembling-machine-1', outSignal = 58},
{name = 'small-lamp', outSignal = 74},
{name = 'constant-combinator', outSignal = 75},
{name = 'decider-combinator', outSignal = 76},
{name = 'arithmetic-combinator', outSignal = 107},
{name = 'radar', outSignal = 45},
{name = 'inserter', outSignal = 14},
{name = 'lab', outSignal = 77},
{name = 'chemical-plant', outSignal = 78},
{name = 'pump', outSignal = 89},
{name = 'solar-panel', outSignal = 46},
{name = 'logistic-science-pack', outSignal = 15},
{name = 'military-science-pack', outSignal = 20}
}, {
{name = 'concrete', outSignal = 79},
{name = 'fast-inserter', outSignal = 55},
{name = 'assembling-machine-2', outSignal = 59},
{name = 'plastic-bar', outSignal = 21},
{name = 'lubricant-barrel', outSignal = 34},
{name = 'solid-fuel', outSignal = 42},
{name = 'sulfur', outSignal = 25}
}, {
{name = 'hazard-concrete', outSignal = 80},
{name = 'refined-concrete', outSignal = 81},
{name = 'advanced-circuit', outSignal = 23},
{name = 'explosives', outSignal = 96},
{name = 'sulfuric-acid-barrel', outSignal = 32},
{name = 'chemical-science-pack', outSignal = 26},
{name = 'low-density-structure', outSignal = 31},
{name = 'electric-engine-unit', outSignal = 35},
{name = 'rocket-fuel', outSignal = 43}
}, {
{name = 'logistic-chest-active-provider', outSignal = 68},
{name = 'logistic-chest-passive-provider', outSignal = 69},
{name = 'logistic-chest-storage', outSignal = 70},
{name = 'logistic-chest-buffer', outSignal = 71},
{name = 'logistic-chest-requester', outSignal = 72},
{name = 'refined-hazard-concrete', outSignal = 82},
{name = 'nuclear-reactor', outSignal = 99},
{name = 'centrifuge', outSignal = 103},
{name = 'substation', outSignal = 57},
{name = 'roboport', outSignal = 66},
{name = 'stack-inserter', outSignal = 56},
{name = 'electric-furnace', outSignal = 29},
{name = 'explosive-cannon-shell', outSignal = 97},
{name = 'battery', outSignal = 33},
{name = 'processing-unit', outSignal = 36},
{name = 'productivity-module', outSignal = 28},
{name = 'speed-module', outSignal = 39},
{name = 'effectivity-module', outSignal = 61},
{name = 'artillery-turret', outSignal = 113}
}, {
{name = 'laser-turret', outSignal = 92},
{name = 'artillery-shell', outSignal = 98},
{name = 'assembling-machine-3', outSignal = 60},
{name = 'productivity-module-2', outSignal = 64},
{name = 'speed-module-2', outSignal = 62},
{name = 'rocket-silo', outSignal = 83},
{name = 'accumulator', outSignal = 40},
{name = 'rocket-control-unit', outSignal = 41},
{name = 'production-science-pack', outSignal = 30},
{name = 'flying-robot-frame', outSignal = 37}
}, {
{name = 'productivity-module-3', outSignal = 65},
{name = 'speed-module-3', outSignal = 63},
{name = 'logistic-robot', outSignal = 94},
{name = 'construction-robot', outSignal = 95},
{name = 'satellite', outSignal = 44},
{name = 'utility-science-pack', outSignal = 38}
}, {{name = 'space-science-pack', outSignal = 47}}
}
function choose_item_from_tiers()
for i = 1, #ITEM_TIERS do
local current_tier = ITEM_TIERS[i]
local most_needed_item = nil
local check_again = false
for j = 1, #current_tier do
local currrent_item = current_tier[j]
-- Check if this items demand is higher than the other ones
-- in the tier that we've seen
if red[currrent_item.name] < 0 and
(most_needed_item == nil or red[currrent_item.name] <
red[most_needed_item.name]) then
-- If we just built this tile, don't choose it, but set
-- check_again to true in case nothing else in the tier
-- is in demand
if currrent_item.outSignal == lastSignal then
check_again = true
else
most_needed_item = currrent_item
end
end
end
-- If the only item in this tier that is in demand is the thing we
-- built last tile, then fine, we'll build another.
-- We do not want to start checking higher tiers, since they might
-- depend on this item.
if most_needed_item == nil and check_again then
for j = 1, #current_tier do
local currrent_item = current_tier[j]
if red[currrent_item.name] < 0 and
(most_needed_item == nil or red[currrent_item.name] <
red[most_needed_item.name]) then
most_needed_item = currrent_item
end
end
end
if most_needed_item ~= nil then
out['signal-L'] = i
return most_needed_item
end
end
return nil
end
if not var.doneInit and red['blueprint-deployer'] > 0 then
var.doneInit = true
var.last_nuclear_megatile = 0
var.researchDeadline = math.huge
var.need_artillery = false
-- We have to track tilesBuilt as a variable because we can't trust the
-- input from the outside world. If part of a tile has been placed down,
-- but not the combinator that reports its existence, we could end up
-- deploying two different blueprints on the same tile space.
-- Instead we wait for the numbers from the circuit network to match
-- the expected number from the variable before proceeeding.
var.tilesBuilt = red['blueprint-deployer']
game.print('Initialized with ' .. var.tilesBuilt .. ' tiles built')
for i = 1, #ITEM_TIERS do
local current_tier = ITEM_TIERS[i]
local tier_text = 'Tier ' .. i .. ': '
for j = 1, #current_tier do
local currrent_item = current_tier[j]
tier_text = tier_text .. ' [img=item.' .. currrent_item.name .. ']'
end
game.print(tier_text)
end
end
if can_build_tile and currently_constructed_megatiles < MAX_MEGATILES then
-- Megatile stuff
if var.tilesBuilt % 8 == 0 and var.tilesBuilt / 8 >=
currently_constructed_megatiles then
if not var.is_surveying then
var.is_surveying = true
local n = currently_constructed_megatiles
local x = -1
local y = 0
local steps = 0
local max_steps = 1
local turns_taken = 0
for i = 2, n, 1 do
steps = steps + 1
if steps == max_steps then
steps = 0
turns_taken = turns_taken + 1
end
if steps == 0 and turns_taken % 2 == 0 then
max_steps = max_steps + 1
end
if turns_taken % 4 == 0 then
x = x - 1
elseif turns_taken % 4 == 1 then
y = y - 1
elseif turns_taken % 4 == 2 then
x = x + 1
elseif turns_taken % 4 == 3 then
y = y + 1
end
end
var.megablock_x = x * 48 + 2
var.megablock_y = y * 48
out['red/signal-X'] = var.megablock_x + 2
out['red/signal-Y'] = var.megablock_y
out['red/signal-W'] = 30
out['red/signal-H'] = 30
out['green/construction-robot'] = 109
out['green/signal-X'] = var.megablock_x
out['green/signal-Y'] = var.megablock_y
out['green/signal-W'] = 50
out['green/signal-H'] = 50
game.print('Surveying megatile ' ..
(currently_constructed_megatiles + 1))
delay = 60
else
var.is_surveying = false
-- If we're currently building something, keep building it.
-- Unless it's blueprint 109, that's the tree-clearing blueprint
-- applied during surveying
if green['construction-robot'] > 0 and green['construction-robot'] ~=
109 then
newSignal = green['construction-robot']
-- The first Megatile should be nuclear
elseif currently_constructed_megatiles == 1 then
newSignal = 106
var.tilesBuilt = var.tilesBuilt + 8
-- If the megatile has uranium, or has abundant resources,
-- it must be mined (we'll use a blank patch and let the
-- smaller surveys divy it up)
elseif green['uranium-ore'] > 100000 or green['uranium-ore'] +
green['iron-ore'] + green['copper-ore'] + green['stone'] +
green['coal'] > 1000000 then
game.print((currently_constructed_megatiles + 1) ..
'[img=item.laser-turret] [img=item.roboport]')
newSignal = 1
-- Only build power megatiles if we need power
elseif var.need_power then
-- - Don't build more than one nuclear plant in the first 9 megatiles
-- - Don't build nuclear if we're low on fuel cells
-- - Don't build nuclear if we don't have at least 3 reactors
-- in the logistic network (unless we're using infinichests,
-- then go ahead, whatever)
-- - Don't build nuclear if we built it in the last 2 megatiles
if currently_constructed_megatiles > 10 and
red['uranium-fuel-cell'] > 90 and
(red['nuclear-reactor'] > 3 or
(red['nuclear-reactor'] > -10000 and
green['nuclear-reactor'] == 0)) and
currently_constructed_megatiles >= var.last_nuclear_megatile +
1 then
game.print((currently_constructed_megatiles + 1) ..
'[img=item.nuclear-reactor] [img=item.steam-turbine]')
newSignal = 106
var.last_nuclear_megatile =
currently_constructed_megatiles + 1
else
game.print((currently_constructed_megatiles + 1) ..
'[img=item.solar-panel] [img=item.accumulator]')
newSignal = 2
end
var.need_power = false
var.tilesBuilt = var.tilesBuilt + 8
elseif currently_constructed_megatiles < 9 then
newSignal = 2
var.tilesBuilt = var.tilesBuilt + 8
-- If we're good on power, but light on oil products,
-- build an oil processing megatile
elseif lastSignal ~= 110 and
(red['petroleum-gas-barrel'] < 0 or red['light-oil-barrel'] < 0 or
red['heavy-oil-barrel'] < 0) then
game.print((currently_constructed_megatiles + 1) ..
'[img=item.oil-refinery] [img=fluid.petroleum-gas] [img=fluid.light-oil] [img=fluid.heavy-oil]')
newSignal = 110
var.tilesBuilt = var.tilesBuilt + 8
-- If we're good on power and oil, build a blank megatile
else
game.print((currently_constructed_megatiles + 1) ..
'[img=item.laser-turret] [img=item.roboport]')
newSignal = 1
end
var.is_paving = true
out['construction-robot'] = newSignal
lastSignal = newSignal
local n = currently_constructed_megatiles
local x = -1
local y = 0
local steps = 0
local max_steps = 1
local turns_taken = 0
for i = 2, n, 1 do
steps = steps + 1
if steps == max_steps then
steps = 0
turns_taken = turns_taken + 1
end
if steps == 0 and turns_taken % 2 == 0 then
max_steps = max_steps + 1
end
if turns_taken % 4 == 0 then
x = x - 1
elseif turns_taken % 4 == 1 then
y = y - 1
elseif turns_taken % 4 == 2 then
x = x + 1
elseif turns_taken % 4 == 3 then
y = y + 1
end
end
-- Once the factory starts getting big, we want to build an
-- artillery turret after each time we round a corner.
var.need_artillery = var.need_artillery or
(currently_constructed_megatiles > 36 and steps == max_steps - 1)
var.megablock_x = x * 48 + 2
var.megablock_y = y * 48
out['signal-X'] = var.megablock_x
out['signal-Y'] = var.megablock_y
delay = 60
end
-- If this isn't a megatile, check the tile and see if there are
-- resources before building anything.
elseif not var.is_surveying then
var.is_surveying = true
local n = (var.tilesBuilt % 8) + 1
local x = -1
local y = 0
local steps = 0
local max_steps = 1
local turns_taken = 0
for i = 2, n, 1 do
steps = steps + 1
if steps == max_steps then
steps = 0
turns_taken = turns_taken + 1
end
if steps == 0 and turns_taken % 2 == 0 then
max_steps = max_steps + 1
end
if turns_taken % 4 == 0 then
x = x - 1
elseif turns_taken % 4 == 1 then
y = y - 1
elseif turns_taken % 4 == 2 then
x = x + 1
elseif turns_taken % 4 == 3 then
y = y + 1
end
end
out['red/signal-X'] = var.megablock_x + x * 16 + 2
out['red/signal-Y'] = var.megablock_y + y * 16
out['red/signal-W'] = 10
out['red/signal-H'] = 10
game.print('Surveying tile ' .. (var.tilesBuilt + 1))
delay = 60
else
if green['construction-robot'] > 0 then
game.print('Redoing this for some reason...');
newSignal = green['construction-robot']
else
if (green['uranium-ore'] > 100000 and red['uranium-ore'] < 100000) then
tagSignal = {type = "item", name = "uranium-ore"}
game.print(currently_constructed_megatiles .. ' * 8 + ' ..
(var.tilesBuilt % 8 + 1) .. ' = ' ..
(var.tilesBuilt + 1) .. '[img=item.uranium-ore]')
newSignal = 54
elseif (green['iron-ore'] > 100000 and red['iron-ore'] < 250000) or
(green['copper-ore'] > 100000 and red['copper-ore'] < 250000) or
(green['stone'] > 100000 and red['stone'] < 200000) or
(green['coal'] > 100000 and red['coal'] < 250000) then
local oreType = nil
local maxAvailable = math.max(green['iron-ore'],
green['copper-ore'],
green['stone'], green['coal'])
if green['iron-ore'] == maxAvailable then
oreType = 'iron-ore'
elseif green['copper-ore'] == maxAvailable then
oreType = 'copper-ore'
elseif green['stone'] == maxAvailable then
oreType = 'stone'
elseif green['coal'] == maxAvailable then
oreType = 'coal'
end
tagSignal = {type = "item", name = oreType}
game.print(currently_constructed_megatiles .. ' * 8 + ' ..
(var.tilesBuilt % 8 + 1) .. ' = ' ..
(var.tilesBuilt + 1) .. '[img=item.' .. oreType ..
']')
newSignal = 53
elseif var.need_artillery then
var.need_artillery = false
tagSignal = {type = "item", name = 'artillery-targeting-remote'}
newSignal = 114
game.print(currently_constructed_megatiles .. ' * 8 + ' ..
(var.tilesBuilt % 8 + 1) .. ' = ' ..
(var.tilesBuilt + 1) .. '[img=item.artillery-targeting-remote]')
else
local most_needed_item = choose_item_from_tiers()
if most_needed_item ~= nil then
tagSignal = {type = "item", name = most_needed_item.name}
newSignal = most_needed_item.outSignal
game.print(currently_constructed_megatiles .. ' * 8 + ' ..
(var.tilesBuilt % 8 + 1) .. ' = ' ..
(var.tilesBuilt + 1) .. '[img=item.' ..
most_needed_item.name .. ']')
elseif currently_constructed_research_tiles < MAX_RESEARCH_TILES and
game.tick > var.researchDeadline then
tagSignal = {type = "virtual", name = "signal-dot"}
game.print(currently_constructed_megatiles .. ' * 8 + ' ..
(var.tilesBuilt % 8 + 1) .. ' = ' ..
(var.tilesBuilt + 1) .. ' Research!')
newSignal = 9
var.researchDeadline = math.huge
-- This is to prevent situations where we're waiting to see
-- if we can build more research but we're having a power crisis
elseif var.currently_in_power_shock then
game.print(currently_constructed_megatiles .. ' * 8 + ' ..
(var.tilesBuilt % 8 + 1) .. ' = ' ..
(var.tilesBuilt + 1) ..
' Small Power Station')
newSignal = 108
-- If there's truly nothing we can build, start a timer and
-- if that's still the case when it's done, we'll build research.
-- This is to prevent new research tiles from sneaking in
-- during a really short pre-demand-shock period.
elseif currently_constructed_research_tiles < MAX_RESEARCH_TILES and
var.researchDeadline == math.huge then
game.print('Setting a research deadline in ' .. (5 * 60) ..
' seconds...')
var.researchDeadline = game.tick + (5 * 60 * 60)
end
end
end
out['construction-robot'] = newSignal
lastSignal = newSignal
local n = (var.tilesBuilt % 8) + 1
local x = -1
local y = 0
local steps = 0
local max_steps = 1
local turns_taken = 0
for i = 2, n, 1 do
steps = steps + 1
if steps == max_steps then
steps = 0
turns_taken = turns_taken + 1
end
if steps == 0 and turns_taken % 2 == 0 then
max_steps = max_steps + 1
end
if turns_taken % 4 == 0 then
x = x - 1
elseif turns_taken % 4 == 1 then
y = y - 1
elseif turns_taken % 4 == 2 then
x = x + 1
elseif turns_taken % 4 == 3 then
y = y + 1
end
end
local tileX = var.megablock_x + x * 16
local tileY = var.megablock_y + y * 16
out['signal-X'] = tileX
out['signal-Y'] = tileY
if tagSignal ~= nil then
_api.game.get_player(1).force.add_chart_tag(_api.game.surfaces
.nauvis, {
position = {x = tileX + PIN_OFFSET_X, y = tileY + PIN_OFFSET_Y},
icon = tagSignal
})
end
if green['construction-robot'] == 0 and newSignal ~= 0 then
var.tilesBuilt = var.tilesBuilt + 1
var.is_surveying = false
if var.researchDeadline ~= math.huge then
game.print('Cancelling research deadline, ' ..
math.ceil((var.researchDeadline - game.tick) / 60) ..
' seconds were remaining')
var.researchDeadline = math.huge
end
end
delay = 120
end
else
var.is_surveying = false
end
out['signal-P'] = lastSignal
out['signal-check'] = (not currently_in_logistic_shock) and 1 or 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment