401 lines
13 KiB
Lua

local mpp_util = require("mpp.mpp_util")
local common = {}
local floor, ceil = math.floor, math.ceil
local min, max = math.min, math.max
local sqrt, log = math.sqrt, math.log
---@alias HeuristicMinerPlacement fun(tile: GridTile): boolean
---@param miner MinerStruct
---@return HeuristicMinerPlacement
function common.simple_miner_placement(miner)
local size, area = miner.size, miner.area
local neighbor_cap = (size / 2) ^ 2
return function(tile)
return tile.neighbors_inner > neighbor_cap
-- return tile.neighbor_count > neighbor_cap or tile.far_neighbor_count > leech
end
end
---@param miner MinerStruct
---@return HeuristicMinerPlacement
function common.overfill_miner_placement(miner)
local size, area = miner.size, miner.area
local neighbor_cap = (size/ 2) ^ 2 - 1
local leech = (area * 0.5) ^ 2 - 1
return function(tile)
return tile.neighbors_inner > 0 or tile.neighbors_outer > leech
end
end
---@param heuristic HeuristicsBlock
function common.simple_layout_heuristic(heuristic)
local lane_mult = 1 + ceil(heuristic.lane_count / 2) * 0.05
local unconsumed = 1 + log(max(1, heuristic.unconsumed), 10)
local value =
(heuristic.inner_density + heuristic.empty_space / heuristic.drill_count)
--* heuristic.centricity
* lane_mult
* unconsumed
return value
end
---@param heuristic HeuristicsBlock
function common.overfill_layout_heuristic(heuristic)
local lane_mult = 1 + ceil(heuristic.lane_count / 2) * 0.05
local unconsumed = 1 + log(max(1, heuristic.unconsumed), 10)
local value =
heuristic.outer_density
--* heuristic.centricity
* lane_mult
* unconsumed
return value
end
---@class HeuristicsBlock
--- Values counted per miner placement
---@field inner_neighbor_sum number Sum of resource tiles drills physically cover
---@field outer_neighbor_sum number Sum of all resource tiles drills physically cover
---@field empty_space number Sum of empty tiles drills physically cover
---@field leech_sum number Sum of resources only in the outer reach
---@field postponed_count number Number of postponed drills
---
--- Values calculated after completed layout placement
---@field drill_count number Total number of mining drills
---@field lane_count number Number of lanes
---@field inner_density number Density of tiles physically covered by drills
---@field outer_density number Pseudo (because of overlap) density of all tiles reached by drills
---@field centricity number How centered is the layout in respect to patch bounds, 1 to inf
---@field unconsumed number Unreachable resources count (usually insufficient drill reach)
---@return HeuristicsBlock
function common.init_heuristic_values()
return {
inner_neighbor_sum = 0,
outer_neighbor_sum = 0,
empty_space = 0,
leech_sum = 0,
postponed_count = 0,
drill_count = 1,
lane_count = 1,
inner_density = 1,
outer_density = 1,
centricity = -(0/0),
unconsumed = 0,
}
end
---@param H HeuristicsBlock
---@param M MinerStruct
---@param tile GridTile
---@param postponed boolean?
function common.add_heuristic_values(H, M, tile, postponed)
H.inner_neighbor_sum = H.inner_neighbor_sum + tile.neighbors_inner
H.outer_neighbor_sum = H.outer_neighbor_sum + tile.neighbors_outer
H.empty_space = H.empty_space + (M.size_sq - tile.neighbors_inner)
H.leech_sum = H.leech_sum + max(0, tile.neighbors_outer - tile.neighbors_inner)
if postponed then H.postponed_count = H.postponed_count + 1 end
end
---@param attempt PlacementAttempt
---@param block HeuristicsBlock
---@param coords Coords
function common.finalize_heuristic_values(attempt, block, coords)
block.drill_count = #attempt.miners
block.lane_count = #attempt.lane_layout
block.inner_density = block.inner_neighbor_sum / block.drill_count
block.outer_density = block.outer_neighbor_sum / block.drill_count
local function centricity(m1, m2, size)
local center = size / 2
local drill = m1 + (m2-m1)/2
return center - drill
end
local x = centricity(attempt.sx-1, attempt.bx, coords.w)
local y = centricity(attempt.sy-1, attempt.by, coords.h)
block.centricity = 1 + (x * x + y * y) ^ 0.5
block.unconsumed = attempt.unconsumed
end
---Utility to fill in postponed miners on unconsumed resources
---@param state SimpleState
---@param attempt PlacementAttempt
---@param miners MinerPlacement[]
---@param postponed MinerPlacement[]
function common.process_postponed(state, attempt, miners, postponed)
local grid = state.grid
local M = state.miner
local bx, by = attempt.sx + M.size - 1, attempt.sy + M.size - 1
for _, miner in ipairs(miners) do
grid:consume(miner.x+M.extent_negative, miner.y+M.extent_negative, M.area)
bx, by = max(bx, miner.x + M.size -1), max(by, miner.y + M.size -1)
end
for _, miner in ipairs(postponed) do
miner.unconsumed = grid:get_unconsumed(miner.x+M.extent_negative, miner.y+M.extent_negative, M.area)
bx, by = max(bx, miner.x + M.size -1), max(by, miner.y + M.size -1)
end
table.sort(postponed, function(a, b)
if a.unconsumed == b.unconsumed then
local atile, btile = a.tile, b.tile
if atile.neighbors_outer == btile.neighbors_outer then
return atile.neighbors_inner > btile.neighbors_inner
end
return atile.neighbors_outer > btile.neighbors_outer
end
return a.unconsumed > b.unconsumed
end)
for _, miner in ipairs(postponed) do
local tile = miner.tile
local unconsumed_count = grid:get_unconsumed(miner.x+M.extent_negative, miner.y+M.extent_negative, M.area)
if unconsumed_count > 0 then
common.add_heuristic_values(attempt.heuristics, M, tile, true)
grid:consume(tile.x+M.extent_negative, tile.y+M.extent_negative, M.area)
miners[#miners+1] = miner
miner.postponed = true
bx, by = max(bx, miner.x + M.size - 1), max(by, miner.y + M.size - 1)
end
end
local unconsumed_sum = 0
for _, tile in ipairs(state.resource_tiles) do
if not tile.consumed then unconsumed_sum = unconsumed_sum + 1 end
end
attempt.unconsumed = unconsumed_sum
attempt.bx, attempt.by = bx, by
grid:clear_consumed(state.resource_tiles)
end
local seed
local function get_map_seed()
if seed then return seed end
local game_exchange_string = game.get_map_exchange_string()
local map_data = game.parse_map_exchange_string(game_exchange_string)
local seed_number = map_data.map_gen_settings.seed
seed = string.format("%x", seed_number)
return seed
end
---Dump state to json for inspection
---@param state SimpleState
function common.save_state_to_file(state, type_)
local c = state.coords
local gx, gy = floor(c.gx), floor(c.gy)
local dir = state.direction_choice
local coverage = state.coverage_choice and "t" or "f"
local filename = string.format("layout_%s_%i;%i_%s_%i_%s_%s_%x.%s", get_map_seed(), gx, gy, state.miner_choice, #state.resources, dir, coverage, game.tick, type_)
if type_ == "json" then
game.print(string.format("Dumped data to %s ", filename))
game.write_file("mpp-inspect/"..filename, game.table_to_json(state), false, state.player.index)
elseif type_ == "lua" then
game.print(string.format("Dumped data to %s ", filename))
game.write_file("mpp-inspect/"..filename, serpent.dump(state, {}), false, state.player.index)
end
end
function common.calculate_patch_slack(state)
end
---@param miner MinerStruct
---@param restrictions Restrictions
---@return boolean
function common.is_miner_restricted(miner, restrictions)
return false
or miner.size < restrictions.miner_size[1]
or restrictions.miner_size[2] < miner.size
or miner.radius < restrictions.miner_radius[1]
or restrictions.miner_radius[2] < miner.radius
end
---@param belt BeltStruct
---@param restrictions Restrictions
function common.is_belt_restricted(belt, restrictions)
return false
or (restrictions.uses_underground_belts and not belt.related_underground_belt)
end
---@param pole PoleStruct
---@param restrictions Restrictions
function common.is_pole_restricted(pole, restrictions)
return false
or pole.size < restrictions.pole_width[1]
or restrictions.pole_width[2] < pole.size
or pole.supply_area_distance < restrictions.pole_supply_area[1]
or restrictions.pole_supply_area[2] < pole.supply_area_distance
or pole.wire < restrictions.pole_length[1]
or restrictions.pole_length[2] < pole.wire
end
local triangles = {
west={
{{target={-.6, 0}}, {target={.6, -0.6}}, {target={.6, 0.6}}},
{{target={-.4, 0}}, {target={.5, -0.45}}, {target={.5, 0.45}}},
},
east={
{{target={.6, 0}}, {target={-.6, -0.6}}, {target={-.6, 0.6}}},
{{target={.4, 0}}, {target={-.5, -0.45}}, {target={-.5, 0.45}}},
},
north={
{{target={0, -.6}}, {target={-.6, .6}}, {target={.6, .6}}},
{{target={0, -.4}}, {target={-.45, .5}}, {target={.45, .5}}},
},
south={
{{target={0, .6}}, {target={-.6, -.6}}, {target={.6, -.6}}},
{{target={0, .4}}, {target={-.45, -.5}}, {target={.45, -.5}}},
},
}
local alignment = {
west={"center", "center"},
east={"center", "center"},
north={"left", "right"},
south={"right", "left"},
}
local bound_alignment = {
west="right",
east="left",
north="center",
south="center",
}
---Draws a belt lane overlay
---@param state State
---@param belt BeltSpecification
function common.draw_belt_lane(state, belt)
local r = state._render_objects
local c, ttl, player = state.coords, 0, {state.player}
local x1, y1, x2, y2 = belt.x1, belt.y, math.max(belt.x1+2, belt.x2), belt.y
local function l2w(x, y) -- local to world
return mpp_util.revert(c.gx, c.gy, state.direction_choice, x, y, c.tw, c.th)
end
local c1, c2, c3 = {.9, .9, .9}, {0, 0, 0}, {.4, .4, .4}
local w1, w2 = 4, 10
if not belt.lane1 and not belt.lane2 then c1 = c3 end
r[#r+1] = rendering.draw_line{ -- background main line
surface=state.surface, players=player, only_in_alt_mode=true,
width=w2, color=c2, time_to_live=ttl or 1,
from=l2w(x1, y1), to=l2w(x2+.5, y1),
}
r[#r+1] = rendering.draw_line{ -- background vertical cap
surface=state.surface, players=player, only_in_alt_mode=true,
width=w2, color=c2, time_to_live=ttl or 1,
from=l2w(x2+.5, y1-.6), to=l2w(x2+.5, y2+.6),
}
r[#r+1] = rendering.draw_polygon{ -- background arrow
surface=state.surface, players=player, only_in_alt_mode=true,
width=w2, color=c2, time_to_live=ttl or 1,
target=l2w(x1, y1),
vertices=triangles[state.direction_choice][1],
}
r[#r+1] = rendering.draw_line{ -- main line
surface=state.surface, players=player, only_in_alt_mode=true,
width=w1, color=c1, time_to_live=ttl or 1,
from=l2w(x1-.2, y1), to=l2w(x2+.5, y1),
}
r[#r+1] = rendering.draw_line{ -- vertical cap
surface=state.surface, players=player, only_in_alt_mode=true,
width=w1, color=c1, time_to_live=ttl or 1,
from=l2w(x2+.5, y1-.5), to=l2w(x2+.5, y2+.5),
}
r[#r+1] = rendering.draw_polygon{ -- arrow
surface=state.surface, players=player, only_in_alt_mode=true,
width=0, color=c1, time_to_live=ttl or 1,
target=l2w(x1, y1),
vertices=triangles[state.direction_choice][2],
}
end
---Draws a belt lane overlay
---@param state State
---@param belt BeltSpecification
function common.draw_belt_stats(state, belt, belt_speed, speed1, speed2)
local r = state._render_objects
local c, ttl, player = state.coords, 0, {state.player}
local x1, y1, x2, y2 = belt.x1, belt.y, belt.x2, belt.y
local function l2w(x, y) -- local to world
return mpp_util.revert(c.gx, c.gy, state.direction_choice, x, y, c.tw, c.th)
end
local c1, c2, c3, c4 = {.9, .9, .9}, {0, 0, 0}, {.9, 0, 0}, {.4, .4, .4}
local ratio1 = speed1 / belt_speed
local ratio2 = speed2 / belt_speed
local function get_color(ratio)
return ratio > 1.01 and c3 or ratio == 0 and c4 or c1
end
r[#r+1] = rendering.draw_text{
surface=state.surface, players=player, only_in_alt_mode=true,
color=get_color(ratio1), time_to_live=ttl or 1,
alignment=alignment[state.direction_choice][1], vertical_alignment="middle",
target=l2w(x1-2, y1-.6), scale=1.6,
text=string.format("%.2fx", ratio1),
}
r[#r+1] = rendering.draw_text{
surface=state.surface, players=player, only_in_alt_mode=true,
color=get_color(ratio2), time_to_live=ttl or 1,
alignment=alignment[state.direction_choice][2], vertical_alignment="middle",
target=l2w(x1-2, y1+.6), scale=1.6,
text=string.format("%.2fx", ratio2),
}
end
---Draws a belt lane overlay
---@param state State
---@param pos_x number
---@param pos_y number
---@param speed1 number
---@param speed2 number
function common.draw_belt_total(state, pos_x, pos_y, speed1, speed2)
local r = state._render_objects
local c, ttl, player = state.coords, 0, {state.player}
local function l2w(x, y, b) -- local to world
if ({south=true, north=true})[state.direction_choice] then
x = x + (b and -.5 or .5)
y = y + (b and -.5 or .5)
end
return mpp_util.revert(c.gx, c.gy, state.direction_choice, x, y, c.tw, c.th)
end
local c1 = {0.7, 0.7, 1.0}
local lower_bound = math.min(speed1, speed2)
local upper_bound = math.max(speed1, speed2)
r[#r+1] = rendering.draw_text{
surface=state.surface, players=player, only_in_alt_mode=true,
color=c1, time_to_live=ttl or 1,
alignment=bound_alignment[state.direction_choice], vertical_alignment="middle",
target=l2w(pos_x-4, pos_y-.6, false), scale=2,
text={"mpp.msg_print_info_lane_saturation_belts", string.format("%.2fx", upper_bound), string.format("%.2fx", (lower_bound+upper_bound)/2)},
}
r[#r+1] = rendering.draw_text{
surface=state.surface, players=player, only_in_alt_mode=true,
color=c1, time_to_live=ttl or 1,
alignment=bound_alignment[state.direction_choice], vertical_alignment="middle",
target=l2w(pos_x-4, pos_y+.6, true), scale=2,
text={"mpp.msg_print_info_lane_saturation_bounds", string.format("%.2fx", lower_bound), string.format("%.2fx", upper_bound)},
}
end
return common