ballistics-ng 0.1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +12 -0
- data/Rakefile +51 -0
- data/examples/table.rb +10 -0
- data/ext/ballistics/ext.c +136 -0
- data/ext/ballistics/extconf.rb +3 -0
- data/ext/ballistics/gnu_ballistics.h +577 -0
- data/lib/ballistics.rb +77 -0
- data/lib/ballistics/atmosphere.rb +110 -0
- data/lib/ballistics/cartridge.rb +177 -0
- data/lib/ballistics/cartridges/300_blk.yaml +159 -0
- data/lib/ballistics/gun.rb +123 -0
- data/lib/ballistics/guns/pistols.yaml +6 -0
- data/lib/ballistics/guns/rifles.yaml +34 -0
- data/lib/ballistics/guns/shotguns.yaml +6 -0
- data/lib/ballistics/problem.rb +102 -0
- data/lib/ballistics/projectile.rb +165 -0
- data/lib/ballistics/projectiles/300_blk.yaml +321 -0
- data/lib/ballistics/util.rb +67 -0
- data/lib/ballistics/yaml.rb +57 -0
- data/test/atmosphere.rb +26 -0
- data/test/ballistics.rb +31 -0
- data/test/cartridge.rb +121 -0
- data/test/gun.rb +85 -0
- data/test/problem.rb +137 -0
- data/test/projectile.rb +163 -0
- data/test/util.rb +28 -0
- metadata +102 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'bigdecimal/util'
|
3
|
+
|
4
|
+
module Ballistics
|
5
|
+
# weight in grains
|
6
|
+
# velocity in ft / s
|
7
|
+
# acceleration in ft / s^2
|
8
|
+
# caliber in inches
|
9
|
+
|
10
|
+
GRAINS_PER_LB = 7000.to_d
|
11
|
+
G = 32.175.to_d # gravitational acceleration
|
12
|
+
|
13
|
+
def self.lbs(grains)
|
14
|
+
grains.to_d / GRAINS_PER_LB
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.grains(lbs)
|
18
|
+
lbs.to_d * GRAINS_PER_LB
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.mass(weight)
|
22
|
+
weight.to_d / G
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sectional density, according to the SpeerReloading Manual No. 13,
|
26
|
+
# is defined as: "A bullet's weight in pounds divided by the square of
|
27
|
+
# its diameter in inches." Note that SD is independent of a bullet's
|
28
|
+
# shape. All bullets of the same caliber and weight will have the same
|
29
|
+
# SD, regardless of their shape or composition.
|
30
|
+
#
|
31
|
+
def self.sectional_density(weight, caliber)
|
32
|
+
self.lbs(weight) / caliber.to_d ** 2
|
33
|
+
end
|
34
|
+
|
35
|
+
# 1/2 m v^2
|
36
|
+
#
|
37
|
+
def self.kinetic_energy(velocity, weight)
|
38
|
+
self.mass(self.lbs(weight)) * velocity.to_d ** 2 / 2
|
39
|
+
end
|
40
|
+
|
41
|
+
# http://www.chuckhawks.com/taylor_KO_factor.htm
|
42
|
+
# tl;dr don't use this
|
43
|
+
#
|
44
|
+
def self.taylor_ko(velocity, weight, caliber)
|
45
|
+
self.lbs(weight) * velocity.to_d * caliber.to_d
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.recoil_impulse(proj_weight, charge_weight, proj_v, charge_v=4000)
|
49
|
+
self.mass(self.lbs(proj_weight.to_d * proj_v.to_d +
|
50
|
+
charge_weight.to_d * charge_v.to_d))
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.free_recoil(proj_weight, charge_weight,
|
54
|
+
proj_v, gun_lbs, charge_v=4000)
|
55
|
+
self.recoil_impulse(proj_weight, charge_weight, proj_v, charge_v) /
|
56
|
+
self.mass(gun_lbs)
|
57
|
+
end
|
58
|
+
|
59
|
+
# 1/2 m(gun) * fr^2
|
60
|
+
#
|
61
|
+
def self.recoil_energy(proj_weight, charge_weight,
|
62
|
+
proj_v, gun_lbs, charge_v=4000)
|
63
|
+
fr = self.free_recoil(proj_weight, charge_weight,
|
64
|
+
proj_v, gun_lbs, charge_v)
|
65
|
+
self.mass(gun_lbs) * fr ** 2 / 2
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# We don't depend on the Ballistics module, so it may not have been loaded yet
|
4
|
+
module Ballistics; end
|
5
|
+
|
6
|
+
module Ballistics::YAML
|
7
|
+
class UnknownType < RuntimeError; end
|
8
|
+
class TypeMismatch < RuntimeError; end
|
9
|
+
class LoadError < RuntimeError; end
|
10
|
+
class MandatoryFieldError; end
|
11
|
+
|
12
|
+
# Return a hash keyed by subdir with array values
|
13
|
+
# Array contains short names for the subdir's yaml files
|
14
|
+
# e.g. { 'cartridges' => ['300_blk'] }
|
15
|
+
#
|
16
|
+
BUILT_IN = {}
|
17
|
+
Dir[File.join(__dir__, '*')].each { |fn|
|
18
|
+
if File.directory? fn
|
19
|
+
yaml_files = Dir[File.join(fn, '*.yaml')]
|
20
|
+
if !yaml_files.empty?
|
21
|
+
BUILT_IN[File.basename fn] =
|
22
|
+
yaml_files.map { |y| File.basename(y, '.yaml') }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
}
|
26
|
+
|
27
|
+
def self.load_built_in(dir, short_name)
|
28
|
+
files = BUILT_IN[dir] or raise(LoadError, "unknown dir: #{dir}")
|
29
|
+
filename = [short_name, 'yaml'].join('.')
|
30
|
+
if files.include?(short_name)
|
31
|
+
::YAML.load_file(File.join(__dir__, dir, filename))
|
32
|
+
else
|
33
|
+
raise(LoadError, "unknown short name: #{short_name}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.check_type?(val, type)
|
38
|
+
case type
|
39
|
+
when :string, :reference
|
40
|
+
val.is_a?(String)
|
41
|
+
when :float
|
42
|
+
val.is_a?(Numeric)
|
43
|
+
when :percent
|
44
|
+
val.is_a?(Numeric) and val >= 0 and val <= 1
|
45
|
+
when :count
|
46
|
+
val.is_a?(1.class) and val >= 0
|
47
|
+
when :int
|
48
|
+
val.is_a?(1.class)
|
49
|
+
else
|
50
|
+
raise UnknownType, type
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.check_type!(val, type)
|
55
|
+
self.check_type?(val, type) or raise(TypeMismatch, [val, type].join(' '))
|
56
|
+
end
|
57
|
+
end
|
data/test/atmosphere.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'ballistics/atmosphere'
|
3
|
+
|
4
|
+
include Ballistics
|
5
|
+
|
6
|
+
describe Atmosphere do
|
7
|
+
before do
|
8
|
+
@altitude = 5430
|
9
|
+
@humidity = 0.48
|
10
|
+
@pressure = 29.93
|
11
|
+
@temp = 40
|
12
|
+
end
|
13
|
+
|
14
|
+
it "translates a ballistic coefficient" do
|
15
|
+
Atmosphere.translate(0.338,
|
16
|
+
altitude: @altitude,
|
17
|
+
humidity: @humidity,
|
18
|
+
pressure: @pressure,
|
19
|
+
temp: @temp).round(3).must_equal 0.392
|
20
|
+
|
21
|
+
Atmosphere.new("altitude" => @altitude,
|
22
|
+
"humidity" => @humidity,
|
23
|
+
"pressure" => @pressure,
|
24
|
+
"temp" => @temp).translate(0.338).round(3).must_equal 0.392
|
25
|
+
end
|
26
|
+
end
|
data/test/ballistics.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'ballistics'
|
3
|
+
|
4
|
+
describe Ballistics do
|
5
|
+
it "calculates the expected zero angle" do
|
6
|
+
opts = {
|
7
|
+
drag_number: 1,
|
8
|
+
ballistic_coefficient: 0.5,
|
9
|
+
velocity: 1200,
|
10
|
+
sight_height: 1.6,
|
11
|
+
zero_range: 100,
|
12
|
+
}
|
13
|
+
Ballistics.zero_angle(opts).round(6).must_equal 0.227188
|
14
|
+
end
|
15
|
+
|
16
|
+
it "calculates the expected trajectory" do
|
17
|
+
opts = {
|
18
|
+
drag_number: 1,
|
19
|
+
ballistic_coefficient: 0.5,
|
20
|
+
velocity: 2250,
|
21
|
+
sight_height: 2.6,
|
22
|
+
zero_range: 50,
|
23
|
+
wind_speed: 10,
|
24
|
+
wind_angle: 90,
|
25
|
+
max_range: 1000,
|
26
|
+
interval: 25,
|
27
|
+
shooting_angle: 0,
|
28
|
+
}
|
29
|
+
Ballistics.trajectory(opts).must_be_kind_of Array
|
30
|
+
end
|
31
|
+
end
|
data/test/cartridge.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'ballistics/cartridge'
|
3
|
+
|
4
|
+
include Ballistics
|
5
|
+
|
6
|
+
describe Cartridge do
|
7
|
+
before do
|
8
|
+
@test_data = {
|
9
|
+
"name" => "Test Cartridge",
|
10
|
+
"case" => "test case",
|
11
|
+
"projectile" => "test_projectile",
|
12
|
+
"20_inch_fps" => 2000,
|
13
|
+
"desc" => "Test Cartridge for test purposes",
|
14
|
+
}
|
15
|
+
@extra_data = {
|
16
|
+
"foo" => "bar",
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "BARREL_LENGTH_REGEX" do
|
21
|
+
it "must match and extract barrel lengths" do
|
22
|
+
["16_inch_fps", "1_inch_fps", "21_inch_fps"].each { |valid|
|
23
|
+
rgx = Cartridge::BARREL_LENGTH_REGEX
|
24
|
+
matches = rgx.match valid
|
25
|
+
matches.wont_be_nil
|
26
|
+
matches.captures.wont_be_empty
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "new instance" do
|
32
|
+
before do
|
33
|
+
@cart = Cartridge.new(@test_data)
|
34
|
+
@cart_ex = Cartridge.new(@test_data.merge(@extra_data))
|
35
|
+
end
|
36
|
+
|
37
|
+
it "must raise with insufficient parameters" do
|
38
|
+
params = {}
|
39
|
+
proc { Cartridge.new params }.must_raise Exception
|
40
|
+
# Accumulate the mandatory fields in params
|
41
|
+
# Note, a field matching Cartridge::BARREL_LENGTH_REGEX is also mandatory
|
42
|
+
Cartridge::MANDATORY.keys.each { |mfield|
|
43
|
+
params[mfield] = @test_data[mfield]
|
44
|
+
proc { Cartridge.new params }.must_raise Exception
|
45
|
+
}
|
46
|
+
mv = { "15_inch_fps" => 15 }
|
47
|
+
proc { Cartridge.new mv }.must_raise Exception
|
48
|
+
Cartridge.new(params.merge(mv)).must_be_kind_of Cartridge
|
49
|
+
end
|
50
|
+
|
51
|
+
it "must have a name" do
|
52
|
+
@cart.name.wont_be_nil
|
53
|
+
@cart.name.must_equal @test_data["name"]
|
54
|
+
end
|
55
|
+
|
56
|
+
it "must have a case" do
|
57
|
+
@cart.case.wont_be_nil
|
58
|
+
@cart.case.must_equal @test_data["case"]
|
59
|
+
end
|
60
|
+
|
61
|
+
it "must have a projectile" do
|
62
|
+
@cart.projectile.wont_be_nil
|
63
|
+
@cart.projectile.must_equal @test_data["projectile"]
|
64
|
+
end
|
65
|
+
|
66
|
+
it "must have a muzzle velocity" do
|
67
|
+
mv = @cart.muzzle_velocity
|
68
|
+
mv.wont_be_nil
|
69
|
+
mv.must_be_kind_of Hash
|
70
|
+
mv.wont_be_empty
|
71
|
+
["16", "20"].each { |barrel_length|
|
72
|
+
test_mv = @test_data[barrel_length]
|
73
|
+
if test_mv
|
74
|
+
mv[barrel_length].must_equal test_mv
|
75
|
+
else
|
76
|
+
mv[barrel_length].must_be_nil
|
77
|
+
end
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
it "must accept optional fields" do
|
82
|
+
Cartridge::OPTIONAL.keys.each { |k|
|
83
|
+
if @test_data.key? k
|
84
|
+
@cart.send(k).must_equal @test_data[k]
|
85
|
+
else
|
86
|
+
@cart.send(k).must_be_nil
|
87
|
+
end
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
it "must retain initializing data" do
|
92
|
+
@cart.yaml_data.must_equal @test_data
|
93
|
+
@cart_ex.yaml_data.must_equal @test_data.merge(@extra_data)
|
94
|
+
end
|
95
|
+
|
96
|
+
it "must retain extra data" do
|
97
|
+
@cart.extra.must_be_empty
|
98
|
+
@cart_ex.extra.wont_be_empty
|
99
|
+
@cart_ex.extra.must_equal @extra_data
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "muzzle velocity" do
|
104
|
+
before do
|
105
|
+
# we need a valid case -- e.g. 300 BLK
|
106
|
+
@cart = Cartridge.new(@test_data.merge("case" => "300 BLK"))
|
107
|
+
end
|
108
|
+
|
109
|
+
it "must estimate an unknown muzzle velocity" do
|
110
|
+
# sanity checks
|
111
|
+
Cartridge::BURN_LENGTH[@cart.case].wont_be_nil
|
112
|
+
@test_data["20_inch_fps"].wont_be_nil
|
113
|
+
@test_data["19_inch_fps"].must_be_nil
|
114
|
+
@test_data["21_inch_fps"].must_be_nil
|
115
|
+
|
116
|
+
@cart.mv(20).must_equal @test_data["20_inch_fps"]
|
117
|
+
@cart.mv(20.9).must_equal @test_data["20_inch_fps"]
|
118
|
+
@cart.mv(19.1).must_equal @test_data["20_inch_fps"]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/test/gun.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'ballistics/gun'
|
3
|
+
|
4
|
+
include Ballistics
|
5
|
+
|
6
|
+
describe Gun do
|
7
|
+
before do
|
8
|
+
@test_data = {
|
9
|
+
"name" => "Test Gun",
|
10
|
+
"chamber" => "test chamber",
|
11
|
+
"barrel_length" => 16,
|
12
|
+
"sight_height" => 1.5,
|
13
|
+
"zero_range" => 100,
|
14
|
+
}
|
15
|
+
|
16
|
+
@extra_data = {
|
17
|
+
"foo" => "bar",
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "new instance" do
|
22
|
+
before do
|
23
|
+
@gun = Gun.new(@test_data)
|
24
|
+
@gun_ex = Gun.new(@test_data.merge(@extra_data))
|
25
|
+
end
|
26
|
+
|
27
|
+
it "must have a name" do
|
28
|
+
@gun.name.wont_be_nil
|
29
|
+
@gun.name.must_equal @test_data["name"]
|
30
|
+
end
|
31
|
+
|
32
|
+
it "must have a chamber" do
|
33
|
+
@gun.chamber.wont_be_nil
|
34
|
+
@gun.chamber.must_equal @test_data["chamber"]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "must have a barrel_length" do
|
38
|
+
@gun.barrel_length.wont_be_nil
|
39
|
+
@gun.barrel_length.must_equal @test_data["barrel_length"]
|
40
|
+
end
|
41
|
+
|
42
|
+
it "must have a sight_height" do
|
43
|
+
@gun.sight_height.wont_be_nil
|
44
|
+
@gun.sight_height.must_equal @test_data["sight_height"]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "must accept optional fields" do
|
48
|
+
Gun::OPTIONAL.keys.each { |k|
|
49
|
+
if @test_data.key?(k)
|
50
|
+
@gun.send(k).must_equal @test_data[k]
|
51
|
+
else
|
52
|
+
@gun.send(k).must_be_nil
|
53
|
+
end
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
it "must retain initializing data" do
|
58
|
+
@gun.yaml_data.must_equal @test_data
|
59
|
+
@gun_ex.yaml_data.must_equal @test_data.merge(@extra_data)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "must retain extra data" do
|
63
|
+
@gun.extra.must_be_empty
|
64
|
+
@gun_ex.extra.wont_be_empty
|
65
|
+
@gun_ex.extra.must_equal @extra_data
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "cartridges" do
|
70
|
+
it "must recognize known cartridges" do
|
71
|
+
["300 BLK"].each { |valid|
|
72
|
+
# sanity check
|
73
|
+
Gun::CHAMBER_CARTRIDGE.key?(valid).must_equal true
|
74
|
+
gun = Gun.new @test_data.merge("chamber" => valid)
|
75
|
+
gun.cartridges.must_be_kind_of Hash
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it "must reject an unknown cartridge" do
|
81
|
+
invalid = { "chamber" => "501 Levis" }
|
82
|
+
gun = Gun.new @test_data.merge(invalid)
|
83
|
+
proc { gun.cartridges }.must_raise Gun::ChamberNotFound
|
84
|
+
end
|
85
|
+
end
|
data/test/problem.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'ballistics/problem'
|
3
|
+
|
4
|
+
include Ballistics
|
5
|
+
|
6
|
+
describe Problem do
|
7
|
+
describe "enrich" do
|
8
|
+
it "tolerates empty input" do
|
9
|
+
hsh = Problem.new.enrich
|
10
|
+
hsh.wont_be_empty
|
11
|
+
hsh.must_equal Problem::DEFAULTS
|
12
|
+
end
|
13
|
+
|
14
|
+
it "infers projectile params" do
|
15
|
+
prj = Projectile.new("name" => "test proj",
|
16
|
+
"cal" => 0.223,
|
17
|
+
"grains" => 100,
|
18
|
+
"g7" => 0.445)
|
19
|
+
hsh = Problem.new(projectile: prj).enrich
|
20
|
+
hsh.wont_be_empty
|
21
|
+
prj.params.each { |sym, val| hsh[sym].must_equal val }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "trajectory" do
|
26
|
+
@problem = Problem.new(atmosphere: Atmosphere.new("altitude" => 5430,
|
27
|
+
"humidity" => 0.48,
|
28
|
+
"pressure" => 29.93,
|
29
|
+
"temp" => 40))
|
30
|
+
@opts = {
|
31
|
+
drag_number: 1,
|
32
|
+
ballistic_coefficient: 0.5,
|
33
|
+
velocity: 2850,
|
34
|
+
sight_height: 1.6,
|
35
|
+
wind_speed: 10,
|
36
|
+
wind_angle: 90,
|
37
|
+
zero_range: 200,
|
38
|
+
max_range: 1000,
|
39
|
+
interval: 25,
|
40
|
+
}
|
41
|
+
|
42
|
+
def self.trajectory
|
43
|
+
@trajectory ||= @problem.trajectory(@opts)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "raises with insufficient params" do
|
47
|
+
proc { Problem.trajectory(drag_function: 'G1',
|
48
|
+
drag_number: 1,
|
49
|
+
ballistic_coefficient: 0.5,
|
50
|
+
velocity: 2850) }.must_raise Exception
|
51
|
+
end
|
52
|
+
|
53
|
+
it "has the expected range" do
|
54
|
+
t = self.class.trajectory
|
55
|
+
Hash[2 => 50,
|
56
|
+
4 => 100,
|
57
|
+
8 => 200,
|
58
|
+
12 => 300,
|
59
|
+
16 => 400,
|
60
|
+
20 => 500,
|
61
|
+
40 => 1000,
|
62
|
+
].each { |i, val|
|
63
|
+
t[i]['range'].must_equal val
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
it "has the expected path" do
|
68
|
+
t = self.class.trajectory
|
69
|
+
Hash[2 => 0.6,
|
70
|
+
4 => 1.6,
|
71
|
+
8 => 0,
|
72
|
+
12 => -7,
|
73
|
+
16 => -20.1,
|
74
|
+
20 => -40.1,
|
75
|
+
40 => -282.5,
|
76
|
+
].each { |i, val|
|
77
|
+
t[i]['path'].round(1).must_equal val
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
it "has the expected velocity" do
|
82
|
+
t = self.class.trajectory
|
83
|
+
Hash[2 => 2769,
|
84
|
+
4 => 2691,
|
85
|
+
8 => 2538,
|
86
|
+
12 => 2390,
|
87
|
+
16 => 2246,
|
88
|
+
20 => 2108,
|
89
|
+
40 => 1500,
|
90
|
+
].each { |i, val|
|
91
|
+
t[i]['velocity'].to_i.must_equal val
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
it "has the expected MOA correction" do
|
96
|
+
t = self.class.trajectory
|
97
|
+
Hash[2 => -1.1,
|
98
|
+
4 => -1.5,
|
99
|
+
8 => 0,
|
100
|
+
12 => 2.2,
|
101
|
+
16 => 4.8,
|
102
|
+
20 => 7.7,
|
103
|
+
40 => 27,
|
104
|
+
].each { |i, val|
|
105
|
+
t[i]['moa_correction'].round(1).must_equal val
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
it "has the expected windage" do
|
110
|
+
t = self.class.trajectory
|
111
|
+
Hash[2 => 0.2,
|
112
|
+
4 => 0.6,
|
113
|
+
8 => 2.3,
|
114
|
+
12 => 5.2,
|
115
|
+
16 => 9.4,
|
116
|
+
20 => 15.2,
|
117
|
+
40 => 71.3,
|
118
|
+
].each { |i, val|
|
119
|
+
t[i]['windage'].round(1).must_equal val
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
it "has the expected MOA correction for windage" do
|
124
|
+
t = self.class.trajectory
|
125
|
+
Hash[2 => 0.3,
|
126
|
+
4 => 0.5,
|
127
|
+
8 => 1.1,
|
128
|
+
12 => 1.6,
|
129
|
+
16 => 2.3,
|
130
|
+
20 => 2.9,
|
131
|
+
40 => 6.8,
|
132
|
+
].each { |i, val|
|
133
|
+
t[i]['moa_windage'].round(1).must_equal val
|
134
|
+
}
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|