rubygoal-core 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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