Skip to content

Instantly share code, notes, and snippets.

@Adamantish
Last active August 11, 2017 12:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Adamantish/59de8df408bf5b9e27c3 to your computer and use it in GitHub Desktop.
Save Adamantish/59de8df408bf5b9e27c3 to your computer and use it in GitHub Desktop.
RTanque bot brain with some supporting classes
class Trig
def self.triangulate_position(my_position, target)
meridian_factor = target.heading < Math::PI ? 0.5000 : 1.500000
meridian = Math::PI * meridian_factor
angle = target.heading.radians - meridian
y_delta = Math::sin(angle.abs) * target.distance
x_delta = Math::cos(angle.abs) * target.distance
# get deltas pointing the right way
x_delta *= target.heading < Math::PI ? 1 : -1
y_delta *= target.heading.to_f.between?( 0.5000001 * Math::PI, 1.5 * Math::PI ) ? -1 : 1
{x: my_position.x + x_delta, y: my_position.y + y_delta}
end
end
class Prediction
include RTanque::Bot::BrainHelper
PREDICTION_WINDOW = 1
def initialize(brain)
@brain = brain
@history = []
end
def time_till_impact
# Rough
bullet_speed = MAX_FIRE_POWER * 4 # I think 4 is the factor. At least for max power
@latest_item[:reflection].distance / bullet_speed
end
def record(reflection)
@history << {reflection: reflection, my_position: @brain.sensors.position}
@history.shift if @history.length >= PREDICTION_WINDOW * 3
if @history.length > PREDICTION_WINDOW
seed
end
end
def seed
@penultimate_item = @history[ -1 - PREDICTION_WINDOW ]
@latest_item = @history[-1]
@penultimate_position = Trig.triangulate_position( @penultimate_item[:my_position], @penultimate_item[:reflection] )
@latest_position = Trig.triangulate_position( @latest_item[:my_position], @latest_item[:reflection] )
@xy_delta = [@latest_position[:x] - @penultimate_position[:x] ,
@latest_position[:y] - @penultimate_position[:y] ]
end
def extrapolate( fire_power )
return if !@xy_delta
factor = time_till_impact / PREDICTION_WINDOW
out = {}
out[:x] = (@xy_delta[0] * factor)
out[:y] = (@xy_delta[1] * factor)
out
end
def heading( fire_power )
predict_delta_coords = extrapolate ( fire_power )
x = predict_delta_coords[:x] + @latest_position[:x]
y = predict_delta_coords[:y] + @latest_position[:y]
fire_point = RTanque::Point.new(x, y)
latest_point = RTanque::Point.new(@latest_position[:x], @latest_position[:y])
RTanque::Heading.new_between_points( @brain.sensors.position, fire_point )
end
def ready
!!@xy_delta
end
end
class Swerve < RTanque::Bot::Brain
NAME = 'Swerve'
include RTanque::Bot::BrainHelper
WALL_MARGIN = 100
ACCEL_TICKS = 62
CHANGE_DIRECTION_PROBABILITY = 0.002
RAND_PRECISION = 2000000
RADAR_PRECISION_RANGE = 0.1
MIN_SHOT_DIST_PER_TICK = 5
HALF_FIRE_POWER = MIN_FIRE_POWER + (MAX_FIRE_POWER - MIN_FIRE_POWER) / 2
FIRE_SWITCH_MAX_DISTANCE = 300
FIRE_SWITCH_MIN_DISTANCE = 40
MIDFIELD = 600
LOCK_PRECISION = 0.04
SWERVE_BEFORE_HIT = 28 # a magic number a bit less than the time taken to make a quarter turn
def tick!
@previous_bot_speed = sensors.speed
@radar_direction ||= -1.0000000000
@turn_direction ||= 1
@i ||= 0
Directives.add("speed", MAX_BOT_SPEED)
command.turret_heading = sensors.turret_heading + MAX_RADAR_ROTATION
command.radar_heading = sensors.radar_heading + MAX_RADAR_ROTATION
duck_n_dive if backs_against_the_wall
engage_target
Directives.run_all(self)
command.fire(@fire_power || HALF_FIRE_POWER) unless @dont_shoot || !turret_locked
@previous_health = sensors.health
end
def engage_target
if target_bot
previous_target = @target
previous_target_name = previous_target.name if previous_target
@target = target_bot
if previous_target_name != @target.name
@prediction = Prediction.new(self)
end
@prediction.record(@target)
if @target.distance.between?( FIRE_SWITCH_MIN_DISTANCE, FIRE_SWITCH_MAX_DISTANCE )
@fire_power = HALF_FIRE_POWER
elsif @target.distance < FIRE_SWITCH_MIN_DISTANCE
@fire_power = MIN_FIRE_POWER
else
@fire_power = MAX_FIRE_POWER
end
Directives.add( "turret_heading", aim_heading )
command.radar_heading = @target.heading
end
end
def aim_heading
predict_delta_coords = @prediction.extrapolate( @fire_power )
if @prediction.ready
@prediction.heading( @fire_power )
else
@target.heading
end
end
def target_bot
radar = sensors.radar.select{ |reflect| !reflect.name.match /Swerve/ }
actual_nearest = sensors.radar.min { |a,b| a.distance <=> b.distance }
@dont_shoot = false
if actual_nearest
if actual_nearest.name.match(/Swerve/) && (actual_nearest.heading == 0 || actual_nearest.heading == Math::PI)
@dont_shoot = true
end
end
radar.min { |a,b| a.distance <=> b.distance }
end
def turret_locked
command.turret_heading.between?(sensors.turret_heading * (1 - LOCK_PRECISION), sensors.turret_heading * (1 + LOCK_PRECISION))
end
def backs_against_the_wall
unless near_wall? || @initial_walled
period = 40
if @i == period
@turn_direction *= -1
end
@i = @i == period ? 1 : @i + 1
command.speed = MAX_BOT_SPEED
scarper_to = sensors.position.x < MIDFIELD ? RTanque::Heading::WEST : RTanque::Heading::EAST
command.heading = scarper_to + (RTanque::Heading::ONE_DEGREE * 40 * @turn_direction )
false
else
@initial_walled = true
true
end
end
def duck_n_dive
compass_directions = { 0 => RTanque::Heading::EAST ,
1 => RTanque::Heading::SOUTH ,
2 => RTanque::Heading::WEST ,
3 => RTanque::Heading::NORTH
}
## turn back if at bounds
possible_on_walls = [ sensors.position.on_top_wall?, sensors.position.on_right_wall?, sensors.position.on_bottom_wall?, sensors.position.on_left_wall? ]
on_walls = possible_on_walls.each_index.select{ |i| possible_on_walls[i] == true }
cornered = on_walls.length == 2
# Below line is mechanism for deciding how to handle walls and corners.
# It's an attempt to avoid clumsy conditional logic and reflect the functional structure of the situation.
# Example: if on the North wall we want to head East. Simple
# However, if we're on North *and* East wall that means we're in the top right corner. Going East would be wrong.
# So the East wall should trump North and we should behave as if we're not on the North Wall at all.
# The compass_directions hash gives numbers to these walls in clockwise order.
# If in a corner we can get the corresponding numbers for the two walls and average them.
# So in this case We have Top Wall = 0 and Right Wall = 1. That averages to 0.5, ceilings to 1 so Right Wall wins.
# We use this number 1 to look up the compass_direction: SOUTH.
direction_selector = (on_walls.inject{|acc, val| acc + val } || 0 / 2).ceil
Directives.add("heading", compass_directions[ direction_selector ], false )
preempt_hit
end
def me_hit ; sensors.health < @previous_health if @previous_health ; end
def preempt_hit
if me_hit
@hit_period = @hit_timer
@hit_timer = 0
else
@hit_timer += 1 if @hit_timer
end
if @hit_period || 0 > 0
Directives.add("heading", sensors.heading, false)
turn_time = (0.5 * Math::PI) / MAX_RADAR_ROTATION
if @hit_timer > (@hit_period - SWERVE_BEFORE_HIT )
# Turn left to evade
Directives.add("heading", sensors.heading - (0.5 * Math::PI))
end
end
end
def near_top_wall? ; sensors.position.y > (sensors.position.arena.height - WALL_MARGIN) ; end
def near_bottom_wall? ; sensors.position.y < WALL_MARGIN ; end
def near_right_wall? ; sensors.position.x > (sensors.position.arena.width - WALL_MARGIN) ; end
def near_left_wall? ; sensors.position.x < WALL_MARGIN ; end
def near_wall?
near_top_wall? || near_right_wall? || near_bottom_wall? || near_left_wall?
end
end
class Directives
@@running = {}
class << self
def run_all(brain)
@@running.each do | command, val |
brain.command.method("#{command}=".to_sym).(val)
if brain.sensors.send(command) == val
@@running.delete(command)
end
end
end
def add( command, val, override = true )
if !@@running[command] || override
@@running[command] = val
end
end
def running
@@running
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment