rubygoal-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +18 -0
- data/README.md +85 -0
- data/bin/rubygoal +15 -0
- data/lib/rubygoal.rb +1 -0
- data/lib/rubygoal/ball.rb +60 -0
- data/lib/rubygoal/coach.rb +81 -0
- data/lib/rubygoal/coach_definition.rb +54 -0
- data/lib/rubygoal/coach_loader.rb +55 -0
- data/lib/rubygoal/coaches/coach_definition_away.rb +72 -0
- data/lib/rubygoal/coaches/coach_definition_home.rb +76 -0
- data/lib/rubygoal/configuration.rb +49 -0
- data/lib/rubygoal/coordinate.rb +33 -0
- data/lib/rubygoal/field.rb +134 -0
- data/lib/rubygoal/formation.rb +73 -0
- data/lib/rubygoal/formation/formation_dsl.rb +67 -0
- data/lib/rubygoal/game.rb +162 -0
- data/lib/rubygoal/goal.rb +26 -0
- data/lib/rubygoal/match_data.rb +130 -0
- data/lib/rubygoal/moveable.rb +67 -0
- data/lib/rubygoal/player.rb +87 -0
- data/lib/rubygoal/players/average.rb +16 -0
- data/lib/rubygoal/players/captain.rb +15 -0
- data/lib/rubygoal/players/fast.rb +16 -0
- data/lib/rubygoal/players/goalkeeper.rb +26 -0
- data/lib/rubygoal/players/player_movement.rb +98 -0
- data/lib/rubygoal/recorder.rb +56 -0
- data/lib/rubygoal/simulator.rb +33 -0
- data/lib/rubygoal/team.rb +198 -0
- data/lib/rubygoal/teams/away.rb +15 -0
- data/lib/rubygoal/teams/home.rb +15 -0
- data/lib/rubygoal/util.rb +36 -0
- data/lib/rubygoal/version.rb +3 -0
- data/test/ball_test.rb +56 -0
- data/test/coach_test.rb +49 -0
- data/test/field_test.rb +103 -0
- data/test/fixtures/four_fast_players_coach_definition.rb +27 -0
- data/test/fixtures/less_players_coach_definition.rb +25 -0
- data/test/fixtures/mirror_strategy_coach_definition.rb +42 -0
- data/test/fixtures/more_players_coach_definition.rb +27 -0
- data/test/fixtures/test_away_coach_definition.rb +34 -0
- data/test/fixtures/test_home_coach_definition.rb +34 -0
- data/test/fixtures/two_captains_coach_definition.rb +27 -0
- data/test/fixtures/valid_coach_definition.rb +26 -0
- data/test/formation_test.rb +81 -0
- data/test/game_test.rb +84 -0
- data/test/match_data_test.rb +149 -0
- data/test/player_test.rb +125 -0
- data/test/players/goalkeeper_test.rb +49 -0
- data/test/players/player_movement_test.rb +76 -0
- data/test/recorder_test.rb +118 -0
- data/test/team_test.rb +97 -0
- data/test/test_helper.rb +21 -0
- metadata +158 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygoal/configuration'
|
2
|
+
|
3
|
+
module Rubygoal
|
4
|
+
class Goal
|
5
|
+
def initialize
|
6
|
+
@celebration_time = 0
|
7
|
+
end
|
8
|
+
|
9
|
+
def celebrating?
|
10
|
+
celebration_time > 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def start_celebration
|
14
|
+
self.celebration_time = 3
|
15
|
+
end
|
16
|
+
|
17
|
+
def update(elapsed_time)
|
18
|
+
start_celebration unless celebrating?
|
19
|
+
self.celebration_time -= elapsed_time
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
attr_accessor :celebration_time
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Rubygoal
|
2
|
+
class MatchData
|
3
|
+
class Factory
|
4
|
+
extend Forwardable
|
5
|
+
def_delegators :game, :ball_position, :score_home, :score_away, :time,
|
6
|
+
:home_players_positions, :away_players_positions
|
7
|
+
|
8
|
+
def initialize(game, side)
|
9
|
+
@game = game
|
10
|
+
@side = side
|
11
|
+
end
|
12
|
+
|
13
|
+
def create
|
14
|
+
MatchData.new(
|
15
|
+
my_score,
|
16
|
+
other_score,
|
17
|
+
ball_match_position,
|
18
|
+
my_positions,
|
19
|
+
other_positions,
|
20
|
+
time
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :game, :side
|
27
|
+
|
28
|
+
def remove_goalkeeper_position(positions)
|
29
|
+
positions.tap do |ps|
|
30
|
+
ps.delete(:goalkeeper)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def home_players_positions
|
35
|
+
remove_goalkeeper_position(game.home_players_positions)
|
36
|
+
end
|
37
|
+
|
38
|
+
def away_players_positions
|
39
|
+
remove_goalkeeper_position(game.away_players_positions)
|
40
|
+
end
|
41
|
+
|
42
|
+
def other_side
|
43
|
+
side == :home ? :away : :home
|
44
|
+
end
|
45
|
+
|
46
|
+
def my_score
|
47
|
+
send("score_#{side}")
|
48
|
+
end
|
49
|
+
|
50
|
+
def other_score
|
51
|
+
send("score_#{other_side}")
|
52
|
+
end
|
53
|
+
|
54
|
+
def my_positions
|
55
|
+
send("#{side}_players_positions")
|
56
|
+
end
|
57
|
+
|
58
|
+
def other_positions
|
59
|
+
send("#{other_side}_players_positions")
|
60
|
+
end
|
61
|
+
|
62
|
+
def ball_field_position
|
63
|
+
Field.field_position(ball_position, side)
|
64
|
+
end
|
65
|
+
|
66
|
+
def ball_match_position
|
67
|
+
Field.position_to_percentages(ball_field_position)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Team
|
72
|
+
attr_reader :score, :result, :positions
|
73
|
+
|
74
|
+
def initialize(score, result, positions = nil)
|
75
|
+
@score = score
|
76
|
+
@result = result
|
77
|
+
@positions = positions
|
78
|
+
|
79
|
+
convert_positions_to_percentages
|
80
|
+
end
|
81
|
+
|
82
|
+
def draw?
|
83
|
+
result == :draw
|
84
|
+
end
|
85
|
+
|
86
|
+
def winning?
|
87
|
+
result == :win
|
88
|
+
end
|
89
|
+
|
90
|
+
def losing?
|
91
|
+
result == :lose
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def convert_positions_to_percentages
|
97
|
+
@positions = positions.each_with_object({}) do |(name, pos), hash|
|
98
|
+
hash[name] = Field.position_to_percentages(pos)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
attr_reader :me, :other, :time, :ball
|
104
|
+
|
105
|
+
def initialize(my_score, other_score, ball_position, my_positions, other_positions, time)
|
106
|
+
@me = MatchData::Team.new(
|
107
|
+
my_score,
|
108
|
+
result(my_score, other_score),
|
109
|
+
my_positions
|
110
|
+
)
|
111
|
+
@other = MatchData::Team.new(
|
112
|
+
other_score,
|
113
|
+
result(other_score, my_score),
|
114
|
+
other_positions
|
115
|
+
)
|
116
|
+
@time = time
|
117
|
+
@ball = ball_position
|
118
|
+
end
|
119
|
+
|
120
|
+
def result(my_score, other_score)
|
121
|
+
if my_score > other_score
|
122
|
+
:win
|
123
|
+
elsif my_score < other_score
|
124
|
+
:lose
|
125
|
+
else
|
126
|
+
:draw
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rubygoal/util'
|
2
|
+
require 'rubygoal/coordinate'
|
3
|
+
|
4
|
+
module Rubygoal
|
5
|
+
module Moveable
|
6
|
+
MIN_DISTANCE = 10
|
7
|
+
|
8
|
+
attr_accessor :position, :velocity, :rotation
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@position = Position.new(0, 0)
|
12
|
+
@velocity = Velocity.new(0, 0)
|
13
|
+
@speed = 0
|
14
|
+
@destination = nil
|
15
|
+
@rotation = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def moving?
|
19
|
+
velocity.nonzero?
|
20
|
+
end
|
21
|
+
|
22
|
+
def distance(position)
|
23
|
+
Util.distance(self.position.x, self.position.y, position.x, position.y)
|
24
|
+
end
|
25
|
+
|
26
|
+
def move_to(destination)
|
27
|
+
self.destination = destination
|
28
|
+
|
29
|
+
self.rotation = Util.angle(position.x, position.y, destination.x, destination.y)
|
30
|
+
velocity.x = Util.offset_x(rotation, speed)
|
31
|
+
velocity.y = Util.offset_y(rotation, speed)
|
32
|
+
end
|
33
|
+
|
34
|
+
def update(elapsed_time)
|
35
|
+
return unless moving?
|
36
|
+
|
37
|
+
if destination && distance(destination) < MIN_DISTANCE
|
38
|
+
stop
|
39
|
+
reset_rotation
|
40
|
+
else
|
41
|
+
self.position = position_after_update(elapsed_time)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def stop
|
46
|
+
self.destination = nil
|
47
|
+
self.velocity = Velocity.new(0, 0)
|
48
|
+
end
|
49
|
+
|
50
|
+
def position_after_update(elapsed_time)
|
51
|
+
custom_frame_rate = 1 / 60.0
|
52
|
+
coef = elapsed_time / custom_frame_rate
|
53
|
+
movement = velocity.mult(coef)
|
54
|
+
|
55
|
+
position.add(movement)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
attr_reader :speed
|
61
|
+
attr_accessor :destination
|
62
|
+
|
63
|
+
def reset_rotation
|
64
|
+
self.rotation = 0
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'rubygoal/coordinate'
|
2
|
+
require 'rubygoal/moveable'
|
3
|
+
require 'rubygoal/configuration'
|
4
|
+
require 'rubygoal/util'
|
5
|
+
|
6
|
+
require 'rubygoal/players/player_movement'
|
7
|
+
|
8
|
+
module Rubygoal
|
9
|
+
class Player
|
10
|
+
STRAIGHT_ANGLE = 180
|
11
|
+
|
12
|
+
include Moveable
|
13
|
+
|
14
|
+
attr_reader :side, :type
|
15
|
+
attr_accessor :coach_defined_position
|
16
|
+
|
17
|
+
def initialize(game, side)
|
18
|
+
super()
|
19
|
+
|
20
|
+
@time_to_kick_again = 0
|
21
|
+
@side = side
|
22
|
+
@player_movement = PlayerMovement.new(game, self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def can_kick?(ball)
|
26
|
+
!waiting_to_kick_again? && control_ball?(ball)
|
27
|
+
end
|
28
|
+
|
29
|
+
def kick(ball, target)
|
30
|
+
direction = random_direction(target)
|
31
|
+
strength = random_strength
|
32
|
+
|
33
|
+
ball.move(direction, strength)
|
34
|
+
reset_waiting_to_kick!
|
35
|
+
end
|
36
|
+
|
37
|
+
def move_to_coach_position
|
38
|
+
move_to(coach_defined_position)
|
39
|
+
end
|
40
|
+
|
41
|
+
def update(elapsed_time)
|
42
|
+
update_waiting_to_kick(elapsed_time)
|
43
|
+
player_movement.update(elapsed_time) if moving?
|
44
|
+
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
attr_accessor :time_to_kick_again, :player_movement
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :error
|
55
|
+
|
56
|
+
def waiting_to_kick_again?
|
57
|
+
time_to_kick_again > 0
|
58
|
+
end
|
59
|
+
|
60
|
+
def reset_waiting_to_kick!
|
61
|
+
self.time_to_kick_again = Rubygoal.configuration.kick_again_delay
|
62
|
+
end
|
63
|
+
|
64
|
+
def update_waiting_to_kick(time_elapsed)
|
65
|
+
self.time_to_kick_again -= time_elapsed if waiting_to_kick_again?
|
66
|
+
end
|
67
|
+
|
68
|
+
def control_ball?(ball)
|
69
|
+
distance(ball.position) < Rubygoal.configuration.distance_control_ball
|
70
|
+
end
|
71
|
+
|
72
|
+
def random_strength
|
73
|
+
error_range = (1 - error)..(1 + error)
|
74
|
+
error_coef = Random.rand(error_range)
|
75
|
+
Rubygoal.configuration.kick_strength * error_coef
|
76
|
+
end
|
77
|
+
|
78
|
+
def random_direction(target)
|
79
|
+
direction = Util.angle(position.x, position.y, target.x, target.y)
|
80
|
+
|
81
|
+
max_angle_error = STRAIGHT_ANGLE * error
|
82
|
+
angle_error_range = -max_angle_error..max_angle_error
|
83
|
+
|
84
|
+
direction += Random.rand(angle_error_range)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygoal/player'
|
2
|
+
|
3
|
+
module Rubygoal
|
4
|
+
class AveragePlayer < Player
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
config = Rubygoal.configuration
|
8
|
+
error_range = config.average_lower_error..config.average_upper_error
|
9
|
+
|
10
|
+
@error = Random.rand(error_range)
|
11
|
+
@speed = config.average_speed
|
12
|
+
|
13
|
+
@type = :average
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygoal/player'
|
2
|
+
|
3
|
+
module Rubygoal
|
4
|
+
class CaptainPlayer < Player
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
config = Rubygoal.configuration
|
8
|
+
|
9
|
+
@error = config.captain_error
|
10
|
+
@speed = config.captain_speed
|
11
|
+
|
12
|
+
@type = :captain
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygoal/player'
|
2
|
+
|
3
|
+
module Rubygoal
|
4
|
+
class FastPlayer < Player
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
config = Rubygoal.configuration
|
8
|
+
error_range = config.fast_lower_error..config.fast_upper_error
|
9
|
+
|
10
|
+
@error = Random.rand(error_range)
|
11
|
+
@speed = config.fast_speed
|
12
|
+
|
13
|
+
@type = :fast
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygoal/players/average'
|
2
|
+
require 'rubygoal/field'
|
3
|
+
require 'rubygoal/util'
|
4
|
+
|
5
|
+
module Rubygoal
|
6
|
+
class GoalKeeperPlayer < AveragePlayer
|
7
|
+
def move_to_cover_goal(ball)
|
8
|
+
move_without_rotation_to(position_to_cover_goal(ball))
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def position_to_cover_goal(ball)
|
14
|
+
Util.y_intercept_with_line(
|
15
|
+
coach_defined_position.x,
|
16
|
+
Field.goal_position(side),
|
17
|
+
ball.position
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def move_without_rotation_to(pos)
|
22
|
+
move_to(pos)
|
23
|
+
reset_rotation
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'rubygoal/player'
|
2
|
+
|
3
|
+
module Rubygoal
|
4
|
+
class PlayerMovement
|
5
|
+
PLAYERS_MIN_DISTANCE = 50
|
6
|
+
CLOSE_TO_DESTINATION = 60
|
7
|
+
PLAYERS_CLOSE_DISTANCE = 70
|
8
|
+
|
9
|
+
extend Forwardable
|
10
|
+
def_delegators :player, :position, :velocity, :destination
|
11
|
+
|
12
|
+
def initialize(game, player)
|
13
|
+
@game = game
|
14
|
+
@player = player
|
15
|
+
end
|
16
|
+
|
17
|
+
def update(elapsed_time)
|
18
|
+
self.elapsed_time = elapsed_time
|
19
|
+
|
20
|
+
if blocking_player
|
21
|
+
if close_to_destination? || any_moving_and_very_close_player?
|
22
|
+
player.stop
|
23
|
+
elsif blocking_player_very_close?
|
24
|
+
adapt_velocity_when_very_close
|
25
|
+
elsif blocking_player_close?
|
26
|
+
adapt_velocity_when_close
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :game, :player
|
34
|
+
attr_accessor :elapsed_time
|
35
|
+
|
36
|
+
def game_players_except_me
|
37
|
+
game.players - [player]
|
38
|
+
end
|
39
|
+
|
40
|
+
def game_players_closer_to_destination
|
41
|
+
game_players_except_me.select do |p|
|
42
|
+
destination.distance(position_after_update) >
|
43
|
+
destination.distance(p.position)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def blocking_player
|
48
|
+
game_players_closer_to_destination.min_by do |p|
|
49
|
+
position_after_update.distance(p.position)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def close_to_destination?
|
54
|
+
position_after_update.distance(destination) < CLOSE_TO_DESTINATION
|
55
|
+
end
|
56
|
+
|
57
|
+
def blocking_player_very_close?
|
58
|
+
blocking_player.distance(position) < PLAYERS_MIN_DISTANCE
|
59
|
+
end
|
60
|
+
|
61
|
+
def blocking_player_close?
|
62
|
+
blocking_player.distance(position) < PLAYERS_CLOSE_DISTANCE
|
63
|
+
end
|
64
|
+
|
65
|
+
def any_moving_and_very_close_player?
|
66
|
+
game_players_closer_to_destination.any? do |p|
|
67
|
+
p.moving? && p.distance(position_after_update) < PLAYERS_MIN_DISTANCE
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def adapt_velocity_when_very_close
|
72
|
+
vel_angle = Util.angle(0, 0, velocity.x, velocity.y) - 45
|
73
|
+
vel_magnitude = Util.distance(0, 0, velocity.x, velocity.y)
|
74
|
+
|
75
|
+
player.velocity = Velocity.new(
|
76
|
+
Util.offset_x(vel_angle, vel_magnitude),
|
77
|
+
Util.offset_y(vel_angle, vel_magnitude),
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def adapt_velocity_when_close
|
82
|
+
distance_to_run =
|
83
|
+
blocking_player.distance(position_after_update) - PLAYERS_MIN_DISTANCE
|
84
|
+
|
85
|
+
close_range_distance = (PLAYERS_CLOSE_DISTANCE - PLAYERS_MIN_DISTANCE).to_f
|
86
|
+
|
87
|
+
# We want to decelerate when close, but we do not want to
|
88
|
+
# have velocity = 0, so we add 0.5 to still be in movement
|
89
|
+
deceleration_coef = (distance_to_run * 0.5) / close_range_distance + 0.5
|
90
|
+
|
91
|
+
player.velocity = velocity.mult(deceleration_coef)
|
92
|
+
end
|
93
|
+
|
94
|
+
def position_after_update
|
95
|
+
player.position_after_update(elapsed_time)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|