Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ twitter_images/
bots/
example.rb
examples/
replays/

7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ RTanque::Heading.new == 0
=> true
```

## Save & Replay Matches

Maybe you want to capture an interesting match and send it to friends without sharing your bot brains. The `--capture` flag records the match in a bot-independent format which can be replayed later using the `replay` command.

$ bundle exec rtanque start --capture bots/my_deadly_bot sample_bots/keyboard sample_bots/camper:x2
$ bundle exec rtanque replay replays/last-match.yml

## Contributing

1. Fork it
Expand Down
36 changes: 31 additions & 5 deletions bin/rtanque
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ require 'thor'
require 'rtanque'
require 'rtanque/runner'
require 'octokit'
require 'yaml'

class RTanqueCLI < Thor
include Thor::Actions
Expand All @@ -27,17 +28,41 @@ LONGDESC
method_option :gc, :default => true, :type => :boolean, :banner => 'disable GC (EXPERIMENTAL)'
method_option :quiet, :aliases => '-q', :default => false, :type => :boolean, :banner => 'disable chatter'
method_option :seed, :default => Kernel.srand, :type => :numeric, :banner => 'random number seed value'
method_option :capture, :default => false, :type => :boolean, :banner => 'record the match'
def start(*brain_paths)
Kernel.srand(options[:seed])
runner = RTanque::Runner.new(options[:width], options[:height], options[:max_ticks])
brain_paths.each { |brain_path|
if options[:capture]
runner = RTanque::Recorder.create_runner(options.merge(replay_dir: 'replays'))
else
Kernel.srand(options[:seed])
runner = RTanque::Runner.new(options[:width], options[:height], options[:max_ticks])
end

brain_paths.each do |brain_path|
begin
runner.add_brain_path(brain_path)
rescue RTanque::Runner::LoadError => e
say e.message, :red
exit false
end
}
end

self.print_start_banner(runner) unless options[:quiet]
self.set_gc(options[:gc]) { runner.start(options[:gui]) }
self.print_runner_stats(runner) unless options[:quiet]
end

desc "replay <path_to_replay>", "Replays a previous match"
method_option :gui, :default => true, :type => :boolean, :banner => 'false to run headless'
method_option :gc, :default => true, :type => :boolean, :banner => 'disable GC (EXPERIMENTAL)'
method_option :quiet, :aliases => '-q', :default => false, :type => :boolean, :banner => 'disable chatter'
def replay(replay_path)
begin
runner = RTanque::Replayer.create_runner(replay_path)
rescue RTanque::Replayer::LoadError => e
say e.message, :red
exit false
end

self.print_start_banner(runner) unless options[:quiet]
self.set_gc(options[:gc]) { runner.start(options[:gui]) }
self.print_runner_stats(runner) unless options[:quiet]
Expand Down Expand Up @@ -105,6 +130,7 @@ LONGDESC
def print_stats(indent = 2, &block)
self.print_table([].tap(&block), :indent => indent)
end

end

RTanqueCLI.start
RTanqueCLI.start
4 changes: 3 additions & 1 deletion lib/rtanque.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ module RTanque
require 'rtanque/explosion'
require 'rtanque/shell'
require 'rtanque/match'
require 'rtanque/match/tick_group'
require 'rtanque/match/tick_group'
require 'rtanque/replayer'
require 'rtanque/recorder'
18 changes: 17 additions & 1 deletion lib/rtanque/bot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Bot
MAX_GUN_ENERGY = Configuration.bot.gun_energy_max
GUN_ENERGY_FACTOR = Configuration.bot.gun_energy_factor
attr_reader :arena, :brain, :radar, :turret, :ticks, :health, :fire_power, :gun_energy
attr_accessor :gui_window
attr_accessor :gui_window, :recorder
attr_normalized(:speed, Configuration.bot.speed, Configuration.bot.speed_step)
attr_normalized(:heading, Heading::FULL_RANGE, Configuration.bot.turn_step)
attr_normalized(:fire_power, Configuration.bot.fire_power)
Expand Down Expand Up @@ -92,6 +92,8 @@ def tick_brain
end

def execute_command(command)
self.record_command(self.ticks, command)

self.fire_power = self.normalize_fire_power(self.fire_power, command.fire_power)
self.speed = self.normalize_speed(self.speed, command.speed)
self.heading = self.normalize_heading(self.heading, command.heading)
Expand All @@ -113,5 +115,19 @@ def sensors
sensors.gui_window = self.gui_window
end
end

def to_command
RTanque::Bot::Command.new.tap do |empty_command|
empty_command.fire_power = self.fire_power
empty_command.speed = self.speed
empty_command.heading = self.heading
empty_command.radar_heading = self.radar.heading
empty_command.turret_heading = self.turret.heading
end
end

def record_command(ticks, command)
self.recorder.add(self, ticks, command) unless self.recorder.nil?
end
end
end
5 changes: 4 additions & 1 deletion lib/rtanque/match.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module RTanque
class Match
attr_reader :arena, :bots, :shells, :explosions, :ticks, :max_ticks
attr_accessor :recorder

def initialize(arena, max_ticks = nil)
@arena = arena
Expand Down Expand Up @@ -58,10 +59,12 @@ def pre_shell_tick(shell)
end

def tick
recorder.stop if finished? && recorder

self.shells.tick
self.bots.tick
self.explosions.tick
@ticks += 1
end
end
end
end
81 changes: 81 additions & 0 deletions lib/rtanque/recorder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
module RTanque
class Recorder
attr_reader :save_data, :replay_dir

def self.create_runner(options)
recorder = RTanque::Recorder.new(options)

Kernel.srand(options[:seed])
runner = RTanque::Runner.new(options[:width], options[:height], options[:max_ticks])
runner.recorder = recorder
runner.match.recorder = recorder

runner
end

def initialize(options)
@save_data = make_save_data(options[:seed], options[:width], options[:height], options[:max_ticks])
@replay_dir = options[:replay_dir]
end

def add_bots(bots)
bots.each do |bot|
self[bot] = make_bot_data(bot)
bot.recorder = self
end
end

def add(bot, ticks, command)
self[bot][:commands] << make_command_data(ticks, command)
end

def stop
unless @stopped
serialize
@stopped = true
end
end

protected

def [](bot)
save_data[:bots][bot.object_id.to_s]
end

def []=(bot, value)
save_data[:bots][bot.object_id.to_s] = value
end

def serialize
`mkdir -p #{replay_dir}`

File.open("#{replay_dir}/last-match.yml",'w') do |file|
file.puts(save_data.to_yaml)
end
end

def make_save_data(seed, width, height, max_ticks)
{ captured_at: Time.now.to_i,
options: {
seed: seed,
width: width,
height: height,
max_ticks: max_ticks },
bots: {} }
end

def make_bot_data(bot)
{ name: bot.name,
iv: {
position: bot.position,
heading: bot.heading,
radar_heading: bot.radar.heading,
turret_heading: bot.turret.heading },
commands: [] }
end

def make_command_data(ticks, command)
{ticks: ticks, command: command}
end
end
end
72 changes: 72 additions & 0 deletions lib/rtanque/replayer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
module RTanque

class ReplayBot < RTanque::Bot::Brain
attr_accessor :queue

def tick!
return unless has_input?
e = queue.shift[:command]

command.speed = e.speed
command.heading = e.heading
command.radar_heading = e.radar_heading
command.turret_heading = e.turret_heading
command.fire_power = e.fire_power
end

def has_input?
!queue.empty? && queue.first[:ticks] == sensors.ticks
end
end

class Replayer
LoadError = Class.new(::LoadError)

attr_reader :save_data
attr_reader :runner

# @param [String] replay_path
def self.create_runner(replay_path)
replayer = RTanque::Replayer.new

replayer.deserialize(replay_path)

options = replayer.save_data[:options]

Kernel.srand(options[:seed])
runner = RTanque::Runner.new(options[:width], options[:height], options[:max_ticks])
runner.replayer = replayer

a = replayer.save_data[:bots].map { |(_, e)|
replayer.create_bot(runner.match.arena, e[:name], e[:iv], e[:commands])
}

runner.match.add_bots(a)

runner
end

def deserialize(replay_path)
begin
@save_data = YAML.load(File.read(replay_path))
rescue
raise LoadError, $!.message
end
end

def create_bot(arena, name, iv, commands)
bot = RTanque::Bot.new_random_location(arena, RTanque::ReplayBot)

bot.brain.queue = commands

bot.instance_variable_set(:@name, name)
bot.position = iv[:position]
bot.heading = iv[:heading]
bot.radar.heading = iv[:radar_heading]
bot.turret.heading = iv[:turret_heading]

bot
end

end
end
8 changes: 7 additions & 1 deletion lib/rtanque/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module RTanque
class Runner
LoadError = Class.new(::LoadError)
attr_reader :match
attr_accessor :recorder, :replayer

# @param [Integer] width
# @param [Integer] height
Expand All @@ -22,6 +23,7 @@ def initialize(width, height, *match_args)
def add_brain_path(brain_path)
parsed_path = self.parse_brain_path(brain_path)
bots = parsed_path.multiplier.times.map { self.new_bots_from_brain_path(parsed_path.path) }.flatten
self.recorder.add_bots(bots) if recording?
self.match.add_bots(bots)
end

Expand All @@ -39,6 +41,10 @@ def start(gui = true)
end
end

def recording?
!self.recorder.nil?
end

protected

def new_bots_from_brain_path(brain_path)
Expand Down Expand Up @@ -85,4 +91,4 @@ def parse_brain_path(brain_path)
ParsedBrainPath.new(path, multiplier)
end
end
end
end