Last active
August 11, 2017 12:40
-
-
Save Adamantish/59de8df408bf5b9e27c3 to your computer and use it in GitHub Desktop.
RTanque bot brain with some supporting classes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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