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,56 @@
1
+ require 'rubygoal/game'
2
+
3
+ module Rubygoal
4
+ class Recorder
5
+ def initialize(game)
6
+ @game = game
7
+ @frames = []
8
+ end
9
+
10
+ def update
11
+ @frames << frame_info
12
+ end
13
+
14
+ def to_hash
15
+ {
16
+ teams: {
17
+ home: @game.coach_home.name,
18
+ away: @game.coach_away.name
19
+ },
20
+ score: {
21
+ home: @game.score_home,
22
+ away: @game.score_away
23
+ },
24
+ frames: @frames
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :game, :frames
31
+
32
+ def frame_info
33
+ {
34
+ time: @game.time.round(0),
35
+ score: { home: @game.score_home, away: @game.score_away },
36
+ ball: {
37
+ x: @game.ball.position.x.round(0),
38
+ y: @game.ball.position.y.round(0)
39
+ },
40
+ home_players: team_info(@game.team_home),
41
+ away_players: team_info(@game.team_away)
42
+ }
43
+ end
44
+
45
+ def team_info(team)
46
+ team.players.map do |_, player|
47
+ {
48
+ x: player.position.x.round(0),
49
+ y: player.position.y.round(0),
50
+ angle: player.rotation.round(0),
51
+ type: player.type
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,33 @@
1
+ require 'timecop'
2
+ require 'json'
3
+ require 'rubygoal/game'
4
+
5
+ module Rubygoal
6
+ class Simulator
7
+ extend Forwardable
8
+ def_delegators :game, :recorded_game
9
+
10
+ def initialize
11
+ Rubygoal.configuration.record_game = true
12
+ @game = Rubygoal::Game.new(load_coach(:home), load_coach(:away))
13
+ end
14
+
15
+ def simulate
16
+ time = Time.now
17
+
18
+ while !game.ended? do
19
+ game.update
20
+ time += 1.0 / 60.0
21
+ Timecop.travel(time)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :game
28
+
29
+ def load_coach(side)
30
+ CoachLoader.new(side).coach
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,198 @@
1
+ require 'forwardable'
2
+
3
+ require 'rubygoal/formation'
4
+ require 'rubygoal/field'
5
+ require 'rubygoal/players/average'
6
+ require 'rubygoal/players/fast'
7
+ require 'rubygoal/players/captain'
8
+ require 'rubygoal/players/goalkeeper'
9
+ require 'rubygoal/match_data'
10
+
11
+ module Rubygoal
12
+ class Team
13
+ attr_reader :players, :side, :opponent_side, :coach, :formation
14
+ attr_accessor :goalkeeper
15
+
16
+ INFINITE = 100_000
17
+
18
+ extend Forwardable
19
+ def_delegators :coach, :name
20
+ def_delegators :game, :ball
21
+
22
+ def initialize(game, coach)
23
+ @game = game
24
+ @players = {}
25
+ @coach = coach
26
+
27
+ @match_data_factory = MatchData::Factory.new(game, side)
28
+
29
+ initialize_lineup_values
30
+ initialize_players
31
+ initialize_formation
32
+ end
33
+
34
+ def players_to_initial_position
35
+ match_data = match_data_factory.create
36
+ formation = coach.formation(match_data)
37
+ restart_player_positions_in_own_field(formation)
38
+ end
39
+
40
+ def update(elapsed_time)
41
+ match_data = match_data_factory.create
42
+ self.formation = coach.formation(match_data)
43
+
44
+ unless formation.valid?
45
+ puts formation.errors
46
+ raise "Invalid formation: #{coach.name}"
47
+ end
48
+
49
+ update_coach_defined_positions(formation)
50
+
51
+ player_to_move = nil
52
+ min_distance_to_ball = INFINITE
53
+ players_list.each do |player|
54
+ pass_or_shoot(player) if player.can_kick?(ball)
55
+
56
+ distance_to_ball = player.distance(ball.position)
57
+ if min_distance_to_ball > distance_to_ball
58
+ min_distance_to_ball = distance_to_ball
59
+ player_to_move = player
60
+ end
61
+ end
62
+
63
+ player_to_move.move_to(ball.position)
64
+
65
+ players.each do |name, player|
66
+ if name == :goalkeeper
67
+ if player != player_to_move
68
+ player.move_to_cover_goal(ball)
69
+ player.update(elapsed_time)
70
+ next
71
+ end
72
+ end
73
+
74
+ player.move_to_coach_position unless player == player_to_move
75
+ player.update(elapsed_time)
76
+ end
77
+ end
78
+
79
+ def players_list
80
+ players.values
81
+ end
82
+
83
+ def players_position
84
+ players.each_with_object({}) do |(name, player), hash|
85
+ hash[name] = Field.field_position(player.position, side)
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ attr_reader :game, :match_data_factory
92
+ attr_writer :formation
93
+
94
+ def initialize_lineup_values
95
+ @average_players_count = 6
96
+ @fast_players_count = 3
97
+ end
98
+
99
+ def initialize_formation
100
+ @formation = @coach.initial_formation
101
+ end
102
+
103
+ def initialize_players
104
+ @players = { goalkeeper: GoalKeeperPlayer.new(game, side) }
105
+
106
+ unless @coach.valid?
107
+ puts @coach.errors
108
+ raise "Invalid team definition: #{@coach.name}"
109
+ end
110
+
111
+ @players[@coach.captain_player.name] = CaptainPlayer.new(game, side)
112
+
113
+ @coach.players_by_type(:fast).each do |player_def|
114
+ @players[player_def.name] = FastPlayer.new(game, side)
115
+ end
116
+
117
+ @coach.players_by_type(:average).each do |player_def|
118
+ @players[player_def.name] = AveragePlayer.new(game, side)
119
+ end
120
+
121
+ initialize_player_positions
122
+ end
123
+
124
+ def pass_or_shoot(player)
125
+ # Kick straight to the goal whether the distance is short (200)
126
+ # or we don't have a better option
127
+ target = shoot_target
128
+
129
+ unless Field.close_to_goal?(player.position, opponent_side)
130
+ if teammate = nearest_forward_teammate(player)
131
+ target = teammate.position
132
+ end
133
+ end
134
+
135
+ player.kick(ball, target)
136
+ end
137
+
138
+ def nearest_forward_teammate(player)
139
+ min_dist = INFINITE
140
+ nearest_teammate = nil
141
+
142
+ (players.values - [player]).each do |teammate|
143
+ if teammate_is_on_front?(player, teammate)
144
+ dist = player.distance(teammate.position)
145
+ if min_dist > dist
146
+ nearest_teammate = teammate
147
+ min_dist = dist
148
+ end
149
+ end
150
+ end
151
+
152
+ nearest_teammate
153
+ end
154
+
155
+ def shoot_target
156
+ # Do not kick always to the center, look for the sides of the goal
157
+ limit = Field::GOAL_HEIGHT / 2
158
+ offset = Random.rand(-limit..limit)
159
+
160
+ target = Field.goal_position(opponent_side)
161
+ target.y += offset
162
+ target
163
+ end
164
+
165
+ def initialize_player_positions
166
+ Field.default_player_field_positions.each_with_index do |pos, index|
167
+ players.values[index].position = lineup_to_position(pos)
168
+ players.values[index].coach_defined_position = lineup_to_position(pos)
169
+ end
170
+ end
171
+
172
+ def update_coach_defined_positions(formation)
173
+ formation.players_position.each do |player_name, pos|
174
+ players[player_name].coach_defined_position = lineup_to_position(pos)
175
+ end
176
+ end
177
+
178
+ def restart_player_positions_in_own_field(formation)
179
+ formation.players_position.each do |player_name, pos|
180
+ pos.x *= 0.5
181
+ pos = lineup_to_position(pos)
182
+
183
+ player = players[player_name]
184
+
185
+ player.coach_defined_position = pos
186
+ player.position = pos
187
+ end
188
+ end
189
+
190
+ def lineup_to_position(field_position)
191
+ Field.absolute_position(field_position, side)
192
+ end
193
+
194
+ def goalkeeper
195
+ players[:goalkeeper]
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,15 @@
1
+ require 'rubygoal/team'
2
+
3
+ module Rubygoal
4
+ class AwayTeam < Team
5
+ def initialize(*args)
6
+ @side = :away
7
+ @opponent_side = :home
8
+ super
9
+ end
10
+
11
+ def teammate_is_on_front?(player, teammate)
12
+ teammate.position.x < player.position.x - 40
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ require 'rubygoal/team'
2
+
3
+ module Rubygoal
4
+ class HomeTeam < Team
5
+ def initialize(*args)
6
+ @side = :home
7
+ @opponent_side = :away
8
+ super
9
+ end
10
+
11
+ def teammate_is_on_front?(player, teammate)
12
+ teammate.position.x > player.position.x + 40
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ module Rubygoal
2
+ module Util
3
+ class << self
4
+ def offset_x(angle, distance)
5
+ distance * Math.cos(angle * Math::PI / 180.0)
6
+ end
7
+
8
+ def offset_y(angle, distance)
9
+ distance * Math.sin(angle * Math::PI / 180.0)
10
+ end
11
+
12
+ def distance(x1, y1, x2, y2)
13
+ Math.hypot(x2 - x1, y2 - y1)
14
+ end
15
+
16
+ def angle(x1, y1, x2, y2)
17
+ Math.atan2(y2 - y1, x2 - x1) / Math::PI * 180.0
18
+ end
19
+
20
+ def positive_angle(x1, y1, x2, y2)
21
+ angle = self.angle(x1, y1, x2, y2)
22
+ if angle < 0
23
+ 360 + angle
24
+ else
25
+ angle
26
+ end
27
+ end
28
+
29
+ def y_intercept_with_line(x, pos1, pos2)
30
+ slope = (pos2.y - pos1.y) / (pos2.x - pos1.x)
31
+
32
+ Position.new(x, slope * (x - pos1.x) + pos1.y)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module Rubygoal
2
+ VERSION = Gem::Version.new '1.0.0'
3
+ end
data/test/ball_test.rb ADDED
@@ -0,0 +1,56 @@
1
+ require 'test_helper'
2
+
3
+ require 'rubygoal/ball'
4
+ require 'rubygoal/field'
5
+
6
+ module Rubygoal
7
+ class BallTest < Minitest::Test
8
+ def setup
9
+ @ball = Ball.new
10
+ end
11
+
12
+ def test_ball_is_in_home_goal_position
13
+ ball.position = Field.goal_position(:home).add(Position.new(-1, 0))
14
+
15
+ assert ball.goal?
16
+ end
17
+
18
+ def test_ball_is_in_away_goal_position
19
+ ball.position = Field.goal_position(:away).add(Position.new(1, 0))
20
+
21
+ assert ball.goal?
22
+ end
23
+
24
+ def test_ball_is_not_in_goal_position
25
+ ball.position = Field.goal_position(:home)
26
+
27
+ assert !ball.goal?
28
+ end
29
+
30
+ def test_ball_velocity_drops_by_095_on_each_update
31
+ ball.velocity = Velocity.new(10, 10)
32
+ ball.update(elapsed_time)
33
+ ball.update(elapsed_time)
34
+
35
+ assert_equal Velocity.new(9.025, 9.025), @ball.velocity
36
+ assert_equal Position.new(978.5, 600.5), @ball.position
37
+ end
38
+
39
+ def test_ball_bounces_on_the_height
40
+ ball.position = Position.new(265, 500)
41
+ ball.velocity = Velocity.new(-10, 10)
42
+ ball.update(elapsed_time)
43
+
44
+ assert_equal Velocity.new(9.5, 9.5), @ball.velocity
45
+ assert_equal Position.new(255, 510), @ball.position
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :ball
51
+
52
+ def elapsed_time
53
+ 1 / 60.0
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,49 @@
1
+ require 'test_helper'
2
+ require 'fixtures/four_fast_players_coach_definition'
3
+ require 'fixtures/less_players_coach_definition'
4
+ require 'fixtures/more_players_coach_definition'
5
+ require 'fixtures/two_captains_coach_definition'
6
+ require 'fixtures/valid_coach_definition'
7
+
8
+ module Rubygoal
9
+ class CoachTest < Minitest::Test
10
+ def test_valid_players
11
+ coach = Coach.new(ValidCoachDefinition.new)
12
+
13
+ assert coach.valid?
14
+ assert_empty coach.errors
15
+ end
16
+
17
+ def test_less_players
18
+ coach = Coach.new(LessPlayersCoachDefnition.new)
19
+ expected_error = ['The number of average players is 5']
20
+
21
+ refute coach.valid?
22
+ assert_equal expected_error, coach.errors
23
+ end
24
+
25
+ def test_more_players
26
+ coach = Coach.new(MorePlayersCoachDefinition.new)
27
+ expected_error = ['The number of average players is 7']
28
+
29
+ refute coach.valid?
30
+ assert_equal expected_error, coach.errors
31
+ end
32
+
33
+ def test_more_than_one_captain
34
+ coach = Coach.new(TwoCaptainsCoachDefinition.new)
35
+ expected_errors = ['The number of captains is 2']
36
+
37
+ refute coach.valid?
38
+ assert_equal expected_errors, coach.errors
39
+ end
40
+
41
+ def test_more_than_three_fast
42
+ coach = Coach.new(FourFastPlayersCoachDefinition.new)
43
+ expected_errors = ['The number of fast players is 4']
44
+
45
+ refute coach.valid?
46
+ assert_equal expected_errors, coach.errors
47
+ end
48
+ end
49
+ end