Skip to content

Instantly share code, notes, and snippets.

@SuperSpaceEye
Last active March 6, 2024 16:49
Show Gist options
  • Save SuperSpaceEye/c33443213605d1bf35f81737c9058dc2 to your computer and use it in GitHub Desktop.
Save SuperSpaceEye/c33443213605d1bf35f81737c9058dc2 to your computer and use it in GitHub Desktop.
Brute-force ballistic calculator
---
--- Original creator of formulas: @sashafiesta on Discord
--- Original creator of python adaptation: @malexy on Discord
--- Optimized and translated python code to lua: SpaceEye. (https://gist.github.com/SuperSpaceEye/c33443213605d1bf35f81737c9058dc2)
--- Some lua optimizations: Autist69420
---
-- Simple micro-optimizations for better performance
local table_insert = table.insert
local rad, sin, cos, log, abs, min, pow = math.rad, math.sin, math.cos, math.log, math.abs, math.min, math.pow
local tu = table.unpack
local function linspace(start, end_, num)
local linspaced = {}
if num == 0 then return linspaced end
if num == 1 then
table_insert(linspaced, start)
return linspaced
end
local delta = (end_ - start) / (num - 1)
for i = 0, num-2 do
table_insert(linspaced, start+delta*i)
end
table_insert(linspaced, end_)
return linspaced
end
local function range(start, stop, step)
step = step or 1
local pos = start
return function ()
if step > 0 then
if pos >= stop then return nil end
else
if pos <= stop then return nil end
end
local lpos = pos
pos = pos + step
return lpos
end
end
local function flinspace(start, stop, num_elements, min, max)
local items = linspace(math.max(start, min), math.min(stop, max), num_elements)
local pos = 0
return function() -- simple iterator
pos = pos + 1
return items[pos]
end
end
local function get_root(d, from_end)
if from_end then
for i = #d-1, 1, -1 do
if d[i][1] > d[i+1][1] then return d[i+1] end
end
return d[1]
else
for i = 2, #d, 1 do
if d[i-1][1] < d[i][1] then return d[i-1] end
end
return d[#d]
end
end
local function time_in_air(y0, y, Vy, gravity, max_steps)
local t = 0
local t_below = 9999999
gravity = gravity or 0.05
max_steps = max_steps or 1000000
if y0 <= y then
local y0p
while t < max_steps do
y0p = y0
y0 = y0 + Vy
Vy = 0.99 * Vy - gravity
t = t + 1
if y0 > y then
t_below = t-1
break
end
if y0 - y0p < 0 then
return -1, -1
end
end
end
while t < max_steps do
y0 = y0 + Vy
Vy = 0.99 * Vy - gravity
t = t + 1
if y0 <= y then return t_below, t end
end
return t_below, -1
end
local function get_min(array)
local min_delta_t = array[1][1]
local pitch_ = 0;
local airtime_ = 0;
for i = 1, #array do
if min_delta_t > array[i][1] then
min_delta_t = array[i][1]
pitch_ = array[i][2]
airtime_ = array[i][3]
end
end
return min_delta_t, pitch_, airtime_
end
local function try_pitch(tried_pitch, initial_speed,
length, distance, cannon, target, gravity, max_steps)
gravity = gravity or 0.05
max_steps = max_steps or 1000000
local tp_rad = rad(tried_pitch)
local Vw = cos(tp_rad) * initial_speed
local Vy = sin(tp_rad) * initial_speed
local x_coord_2d = length * cos(tp_rad)
if Vw == 0 then return nil, false end
local part = 1 - (distance - x_coord_2d) / (100 * Vw)
if part <= 0 then return nil, false end
local horizontal_time_to_target = abs(log(part) / (-0.010050335853501))
local y_coord_of_end_barrel = cannon[2] + sin(tp_rad) * length
local t_below, t_above = time_in_air(y_coord_of_end_barrel, target[2], Vy, gravity, max_steps)
if t_below < 0 then return nil, false end
local delta_t = min(
abs(horizontal_time_to_target - t_below),
abs(horizontal_time_to_target - t_above)
)
return {delta_t, tried_pitch, delta_t + horizontal_time_to_target}, true
end
local function try_pitches(iter, ...)
local delta_times = {}
for pitch in iter do
local items, is_successful = try_pitch(pitch, ...)
if is_successful then table.insert(delta_times, items) end
end
return delta_times
end
-- Required parameters:
-- cannon = table of three numbers: x, y, z of cannon
-- target = same as cannon but for target
-- initial_speed = speed in m/s
-- length = length of a cannon
-- Optional parameters:
-- max_steps = maximum number of steps program will simulate projectile before declaring it unreachable
-- max_delta_t_error = maximum difference between horizontal and vertical times to target before declaring target impossible to hit. Only matters if check_impossible is enabled
-- amin = minimum cannon angle
-- amax = maximum cannon angle
-- gravity = x m/tick
-- num_iterations = number of refining steps after roughly calculating angle
-- num_elements = number of elements to test during refining stage
-- check_impossible = does additional check for targets that are impossible to hit
local function calculate_pitch(cannon, target, initial_speed, length,
optional)
local max_steps, max_delta_t_error, amin, amax, gravity, num_iterations, num_elements, check_impossible
optional = optional or {}
max_steps = optional.max_steps or optional[1] or 100000
max_delta_t_error = optional.max_delta_t_error or optional[2] or 1
amin = optional.amin or optional[3] or -30
amax = optional.amax or optional[4] or 60
gravity = optional.gravity or optional[5] or 0.05
num_iterations = optional.num_iterations or optional[6] or 5
num_elements = optional.num_elements or optional[7] or 20
check_impossible = optional.check_impossible or optional[8] or true
local Dx, Dz = cannon[1] - target[1], cannon[3] - target[3]
local distance = math.sqrt(Dx * Dx + Dz * Dz)
local delta_times = try_pitches(range(amax, amin-1, -1), initial_speed, length, distance, cannon, target, gravity, max_steps)
if #delta_times == 0 then return {-1, -1, -1}, {-1, -1, -1} end
local dT1, p1, at1 = tu(get_root(delta_times, false))
local dT2, p2, at2 = tu(get_root(delta_times, true))
local c1 = true
local c2 = not p1 == p2
local same_res = p1 == p2
local dTs1, dTs2
for i in range(0, num_iterations) do
if c1 then dTs1 = try_pitches(flinspace(p1-pow(10,-i), p1+pow(10,-i), num_elements, amin, amax), initial_speed, length, distance, cannon, target, gravity, max_steps) end
if c2 then dTs2 = try_pitches(flinspace(p2-pow(10,-i), p2+pow(10,-i), num_elements, amin, amax), initial_speed, length, distance, cannon, target, gravity, max_steps) end
if c1 and #dTs1 == 0 then c1=false end
if c2 and #dTs2 == 0 then c2=false end
if not c1 and not c2 then return {-1, -1, -1}, {-1, -1, -1} end
if c1 then dT1, p1, at1 = get_min(dTs1) end
if c2 then dT2, p2, at2 = get_min(dTs2) end
end
if same_res then dT2, p2, at2 = dT1, p1, at1 end
local r1, r2 = {dT1, p1, at1}, {dT2, p2, at2}
if check_impossible and dT1 > max_delta_t_error then r1 = {-1, -1, -1} end
if check_impossible and dT2 > max_delta_t_error then r2 = {-1, -1, -1} end
return r1, r2
end
local function calculate_yaw(Dx, Dz, direction)
local yaw
if Dx ~= 0 then
yaw = math.atan(Dz/Dx) * 180/math.pi
else
yaw = 90
end
if Dx >= 0 then
yaw = yaw + 180
end
local dirs = {90, 180, 270, 0}
return (yaw + dirs[direction]) % 360
end
local function ballistics_to_target(cannon, target, power, direction, R1, R2, length)
local directions = {north=1, west=2, south=3, east=4}
direction = directions[direction]
if direction == nil then error("Invalid direction") end
local Dx, Dz = cannon[1] - target[1], cannon[3] - target[3]
local r1, r2 = calculate_pitch(cannon, target, power, length)
local yaw = calculate_yaw(Dx, Dz, direction)
local rt = {}
rt.yaw = yaw
rt.yaw_time = yaw * 20 / (0.75 * R1)
for k, v in pairs({[1]=r1, [2]=r2}) do
local t = {pitch=-1, pitch_time=-1, airtime=-1, fuze_time=-1}
if v[1] ~= -1 then
t.delta_t = v[1]
t.pitch = v[2]
t.airtime = v[3]
t.pitch_time = t.pitch * 20 / (0.75 * R2)
t.precision = 1 - t.delta_t / t.airtime
end
table.insert(rt, k, t)
end
return rt
end
print("For the cannon coordinates, please input the coordinates of the cannon mount.")
cannonCoord = {}
print("x coord of cannon : ")
table_insert(cannonCoord, tonumber(io.read()))
print("y coord of cannon : ")
table_insert(cannonCoord, tonumber(io.read())+2)
print("z coord of cannon : ")
table_insert(cannonCoord, tonumber(io.read()))
targetCoord = {}
print("x coord of target : ")
table_insert(targetCoord, tonumber(io.read()))
print("y coord of target : ")
table_insert(targetCoord, tonumber(io.read()))
print("z coord of target : ")
table_insert(targetCoord, tonumber(io.read()))
print("Number of powder charges (int) : ")
powderCharges = tonumber(io.read())
print("What is the standart direction of the cannon ? (north, south, east, west)")
directionOfCannon = io.read()
print("What is the RPM of the yaw axis ?")
yawRPM = tonumber(io.read())
print("What is the RPM of the pitch axis ?")
pitchRPM = tonumber(io.read())
print("What is the length of the cannon ? (From the block held by the mount to the tip of the cannon, the held block excluded) ")
cannonLength = tonumber(io.read())
-- local cannonCoord = {323, 73+2, 32}
-- local targetCoord = {350, 84, -113}
-- local powderCharges = 8
-- local directionOfCannon = "north"
-- local yawRPM = 1
-- local pitchRPM = 1
-- local cannonLength = 31
local rt = ballistics_to_target(
cannonCoord,
targetCoord,
powderCharges,
directionOfCannon,
yawRPM,
pitchRPM,
cannonLength
)
print("Yaw is ", rt.yaw)
print("With the yaw axis set at ", yawRPM, " rpm, the cannon must take ", rt.yaw_time, " ticks of turning the yaw axis.")
if rt[1].pitch ~= -1 then
print("\nHigh shot:")
print("Pitch is ", rt[1].pitch)
print("Airtime is", rt[1].airtime, "ticks")
print("With the pitch axis set at ", pitchRPM, " rpm, the cannon must take ", rt[1].pitch_time, " ticks of turning the pitch axis.")
print("Precision: ", rt[1].precision)
else
print("\nHigh shot is impossible")
end
if rt[2].pitch ~= -1 then
print("\nLow shot:")
print("Pitch is ", rt[2].pitch)
print("Airtime is", rt[2].airtime, "ticks")
print("With the pitch axis set at ", pitchRPM, " rpm, the cannon must take ", rt[2].pitch_time, " ticks of turning the pitch axis.")
print("Precision: ", rt[2].precision)
else
print("\nLow shot is impossible")
end
@WOSAJ
Copy link

WOSAJ commented Oct 7, 2023

wait, @WOSAJ it's not t.airtime[1] but just t.airtime

i tried to fix this bug (it was cringe)

@WOSAJ
Copy link

WOSAJ commented Oct 7, 2023

image
this error is more correct

@SuperSpaceEye
Copy link
Author

@WOSAJ again, i do not have this error. So try copying the code again from gist

@SuperSpaceEye
Copy link
Author

wait

@WOSAJ
Copy link

WOSAJ commented Oct 7, 2023

okay, if you tried to execute your code with the parameters I specified and you don't have an error, the problem is on my side

@SuperSpaceEye
Copy link
Author

damn, i see the issue

@SuperSpaceEye
Copy link
Author

@WOSAJ i forgot that lua doesn't do table destructuring. Now it should be correct.

@WOSAJ
Copy link

WOSAJ commented Oct 8, 2023

now it works very bad (the target is close, but for some reason it says that it is impossible, although it is a very simple shot)

@SuperSpaceEye
Copy link
Author

@WOSAJ for very close targets you'll need to disable check_impossible

@SuperSpaceEye
Copy link
Author

Technically you don't even need it, as long as you make logic for processing shots with low precision yourself

@WOSAJ
Copy link

WOSAJ commented Oct 8, 2023

thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment