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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +18 -0
  3. data/README.md +85 -0
  4. data/bin/rubygoal +15 -0
  5. data/lib/rubygoal.rb +1 -0
  6. data/lib/rubygoal/ball.rb +60 -0
  7. data/lib/rubygoal/coach.rb +81 -0
  8. data/lib/rubygoal/coach_definition.rb +54 -0
  9. data/lib/rubygoal/coach_loader.rb +55 -0
  10. data/lib/rubygoal/coaches/coach_definition_away.rb +72 -0
  11. data/lib/rubygoal/coaches/coach_definition_home.rb +76 -0
  12. data/lib/rubygoal/configuration.rb +49 -0
  13. data/lib/rubygoal/coordinate.rb +33 -0
  14. data/lib/rubygoal/field.rb +134 -0
  15. data/lib/rubygoal/formation.rb +73 -0
  16. data/lib/rubygoal/formation/formation_dsl.rb +67 -0
  17. data/lib/rubygoal/game.rb +162 -0
  18. data/lib/rubygoal/goal.rb +26 -0
  19. data/lib/rubygoal/match_data.rb +130 -0
  20. data/lib/rubygoal/moveable.rb +67 -0
  21. data/lib/rubygoal/player.rb +87 -0
  22. data/lib/rubygoal/players/average.rb +16 -0
  23. data/lib/rubygoal/players/captain.rb +15 -0
  24. data/lib/rubygoal/players/fast.rb +16 -0
  25. data/lib/rubygoal/players/goalkeeper.rb +26 -0
  26. data/lib/rubygoal/players/player_movement.rb +98 -0
  27. data/lib/rubygoal/recorder.rb +56 -0
  28. data/lib/rubygoal/simulator.rb +33 -0
  29. data/lib/rubygoal/team.rb +198 -0
  30. data/lib/rubygoal/teams/away.rb +15 -0
  31. data/lib/rubygoal/teams/home.rb +15 -0
  32. data/lib/rubygoal/util.rb +36 -0
  33. data/lib/rubygoal/version.rb +3 -0
  34. data/test/ball_test.rb +56 -0
  35. data/test/coach_test.rb +49 -0
  36. data/test/field_test.rb +103 -0
  37. data/test/fixtures/four_fast_players_coach_definition.rb +27 -0
  38. data/test/fixtures/less_players_coach_definition.rb +25 -0
  39. data/test/fixtures/mirror_strategy_coach_definition.rb +42 -0
  40. data/test/fixtures/more_players_coach_definition.rb +27 -0
  41. data/test/fixtures/test_away_coach_definition.rb +34 -0
  42. data/test/fixtures/test_home_coach_definition.rb +34 -0
  43. data/test/fixtures/two_captains_coach_definition.rb +27 -0
  44. data/test/fixtures/valid_coach_definition.rb +26 -0
  45. data/test/formation_test.rb +81 -0
  46. data/test/game_test.rb +84 -0
  47. data/test/match_data_test.rb +149 -0
  48. data/test/player_test.rb +125 -0
  49. data/test/players/goalkeeper_test.rb +49 -0
  50. data/test/players/player_movement_test.rb +76 -0
  51. data/test/recorder_test.rb +118 -0
  52. data/test/team_test.rb +97 -0
  53. data/test/test_helper.rb +21 -0
  54. 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