401 lines
13 KiB
Lua
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
|