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,76 @@
1
+ require 'rubygoal/coach_definition'
2
+ require 'rubygoal/formation'
3
+
4
+ module Rubygoal
5
+ class CoachDefinitionHome < CoachDefinition
6
+
7
+ team do
8
+ name "Uruguay"
9
+
10
+ players do
11
+ captain :godin
12
+
13
+ fast :cavani
14
+ fast :rolan
15
+ fast :suarez
16
+
17
+ average :pereira
18
+ average :gimenez
19
+ average :arevalo
20
+ average :lodeiro
21
+ average :cacerez
22
+ average :rodriguez
23
+ end
24
+ end
25
+
26
+ def formation(match)
27
+ formation = Formation.new
28
+
29
+ if match.me.winning?
30
+ formation.defenders :pereira, :cacerez, :gimenez, :godin, :rodriguez
31
+ formation.midfielders :lodeiro, :none, :rolan, :none, :arevalo
32
+ formation.attackers :none, :cavani, :none, :suarez, :none
33
+ elsif match.time < 20
34
+ formation.defenders :none, :rolan, :cacerez, :gimenez, :none
35
+ formation.midfielders :arevalo, :lodeiro, :godin, :none, :pereira
36
+ formation.attackers :suarez, :none, :none, :cavani, :rodriguez
37
+ elsif match.time < 20 && match.me.winning?
38
+ # Mirror opponent players
39
+
40
+ opponent = match.other.positions
41
+ my_players = players
42
+
43
+ opponent.each_with_index do |(opponent_name, opponent_pos), index|
44
+ formation.lineup do
45
+ custom_position do
46
+ player my_players[index].name
47
+ position opponent_pos.x, 100.0 - opponent_pos.y
48
+ end
49
+ end
50
+ end
51
+ else
52
+ formation.lineup do
53
+ if match.ball.x < 50
54
+ lines do
55
+ defenders 10
56
+ midfielders 30
57
+ attackers 50
58
+ end
59
+ else
60
+ lines do
61
+ defenders 30
62
+ midfielders 50
63
+ attackers 70
64
+ end
65
+ end
66
+
67
+ defenders :pereira, :cacerez, :gimenez, :godin, :rodriguez
68
+ midfielders :lodeiro, :none, :rolan, :none, :arevalo
69
+ attackers :none, :cavani, :none, :suarez, :none
70
+ end
71
+ end
72
+
73
+ formation
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,49 @@
1
+ require 'rubygoal/coordinate'
2
+
3
+ module Rubygoal
4
+ class Configuration
5
+ attr_accessor :average_players_count, :fast_players_count, :captain_players_count,
6
+ :kick_strength, :kick_again_delay, :distance_control_ball,
7
+ :deceleration_coef, :initial_player_positions, :game_time,
8
+ :average_lower_error, :average_upper_error, :average_speed,
9
+ :fast_lower_error, :fast_upper_error, :fast_speed,
10
+ :captain_error, :captain_speed, :debug_output, :record_game
11
+ end
12
+
13
+ class << self
14
+ attr_writer :configuration
15
+
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def configure
21
+ yield(configuration)
22
+ end
23
+ end
24
+
25
+ Rubygoal.configure do |config|
26
+ config.average_players_count = 6
27
+ config.fast_players_count = 3
28
+
29
+ config.kick_strength = 20
30
+ config.kick_again_delay = 1
31
+ config.distance_control_ball = 30
32
+ config.deceleration_coef = 0.95
33
+
34
+ config.average_lower_error = 0.1
35
+ config.average_upper_error = 0.15
36
+ config.average_speed = 3.5
37
+
38
+ config.fast_lower_error = 0.1
39
+ config.fast_upper_error = 0.15
40
+ config.fast_speed = 4
41
+
42
+ config.captain_error = 0.05
43
+ config.captain_speed = 4.5
44
+
45
+ config.game_time = 120
46
+ config.debug_output = true
47
+ config.record_game = false
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ require 'rubygoal/util'
2
+
3
+ module Rubygoal
4
+ class Coordinate < Struct.new(:x, :y)
5
+ def distance(coordinate)
6
+ Util.distance(x, y, coordinate.x, coordinate.y)
7
+ end
8
+
9
+ def add(coordinate)
10
+ self.class.new(x + coordinate.x, y + coordinate.y)
11
+ end
12
+
13
+ def mult(coeficient)
14
+ self.class.new(x * coeficient, y * coeficient)
15
+ end
16
+
17
+ def to_s
18
+ "(#{x}, #{y})"
19
+ end
20
+
21
+ def to_hash
22
+ { x: x, y: y }
23
+ end
24
+ end
25
+
26
+ class Position < Coordinate; end
27
+
28
+ class Velocity < Coordinate
29
+ def nonzero?
30
+ x != 0 || y != 0
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,134 @@
1
+ require 'forwardable'
2
+
3
+ require 'rubygoal/ball'
4
+ require 'rubygoal/coach_loader'
5
+ require 'rubygoal/teams/home'
6
+ require 'rubygoal/teams/away'
7
+ module Rubygoal
8
+ module Field
9
+ WIDTH = 1394.0
10
+ HEIGHT = 938.0
11
+ OFFSET = Position.new(262, 112)
12
+ GOAL_HEIGHT = 275
13
+ CLOSE_GOAL_DISTANCE = 275
14
+
15
+ class << self
16
+ def center_position
17
+ center = Position.new(
18
+ WIDTH / 2,
19
+ HEIGHT / 2
20
+ )
21
+ OFFSET.add(center)
22
+ end
23
+
24
+ def goal_position(side)
25
+ position = center_position
26
+ case side
27
+ when :home
28
+ position.x = OFFSET.x
29
+ when :away
30
+ position.x = OFFSET.x + WIDTH
31
+ end
32
+
33
+ position
34
+ end
35
+
36
+ def absolute_position(field_position, side)
37
+ case side
38
+ when :home
39
+ OFFSET.add(field_position)
40
+ when :away
41
+ OFFSET.add(
42
+ Position.new(
43
+ WIDTH - field_position.x,
44
+ HEIGHT - field_position.y
45
+ )
46
+ )
47
+ end
48
+ end
49
+
50
+ def field_position(absolute_position, side)
51
+ case side
52
+ when :home
53
+ absolute_position.add(
54
+ Position.new(
55
+ - OFFSET.x,
56
+ - OFFSET.y
57
+ )
58
+ )
59
+ when :away
60
+ Position.new(
61
+ WIDTH - (absolute_position.x - OFFSET.x),
62
+ HEIGHT - (absolute_position.y - OFFSET.y)
63
+ )
64
+ end
65
+ end
66
+
67
+ def position_from_percentages(position_in_percentages)
68
+ Position.new(
69
+ position_in_percentages.x / 100.0 * Field::WIDTH,
70
+ position_in_percentages.y / 100.0 * Field::HEIGHT
71
+ )
72
+ end
73
+
74
+ def position_to_percentages(position)
75
+ Position.new(
76
+ position.x / Field::WIDTH * 100,
77
+ position.y / Field::HEIGHT * 100
78
+ )
79
+ end
80
+
81
+ def position_side(position)
82
+ position.x < center_position.x ? :home : :away
83
+ end
84
+
85
+ def out_of_bounds_width?(position)
86
+ lower_limit = OFFSET.x
87
+ upper_limit = OFFSET.x + WIDTH
88
+ !(lower_limit..upper_limit).include?(position.x)
89
+ end
90
+
91
+ def out_of_bounds_height?(position)
92
+ lower_limit = OFFSET.y
93
+ upper_limit = OFFSET.y + HEIGHT
94
+ !(lower_limit..upper_limit).include?(position.y)
95
+ end
96
+
97
+ def goal?(position)
98
+ if out_of_bounds_width?(position)
99
+ lower_limit = center_position.y - GOAL_HEIGHT / 2
100
+ upper_limit = center_position.y + GOAL_HEIGHT / 2
101
+
102
+ (lower_limit..upper_limit).include?(position.y)
103
+ else
104
+ false
105
+ end
106
+ end
107
+
108
+ def close_to_goal?(position, side)
109
+ goal_position = Field.goal_position(side)
110
+ goal_position.distance(position) < CLOSE_GOAL_DISTANCE
111
+ end
112
+
113
+ def default_player_field_positions
114
+ defenders = Field::WIDTH / 6
115
+ midfielders = Field::WIDTH / 2
116
+ attackers = Field::WIDTH * 5 / 6
117
+
118
+ [
119
+ Position.new(50, Field::HEIGHT / 2),
120
+ Position.new(defenders, Field::HEIGHT / 5),
121
+ Position.new(defenders, Field::HEIGHT * 2 / 5),
122
+ Position.new(defenders, Field::HEIGHT * 3 / 5),
123
+ Position.new(defenders, Field::HEIGHT * 4 / 5),
124
+ Position.new(midfielders, Field::HEIGHT / 5),
125
+ Position.new(midfielders, Field::HEIGHT * 2 / 5),
126
+ Position.new(midfielders, Field::HEIGHT * 3 / 5),
127
+ Position.new(midfielders, Field::HEIGHT * 4 / 5),
128
+ Position.new(attackers, Field::HEIGHT / 3),
129
+ Position.new(attackers, Field::HEIGHT * 2 / 3)
130
+ ]
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,73 @@
1
+ require 'rubygoal/formation/formation_dsl'
2
+
3
+ module Rubygoal
4
+ class Formation
5
+ attr_accessor :players_position, :lines_definition
6
+
7
+ def initialize
8
+ @players_position = {}
9
+
10
+ @lines_definition = {
11
+ defenders: Field::WIDTH / 6,
12
+ def_midfielders: Field::WIDTH / 3,
13
+ midfielders: Field::WIDTH / 2,
14
+ att_midfielders: Field::WIDTH / 3 * 2,
15
+ attackers: Field::WIDTH / 6 * 5,
16
+ }
17
+ end
18
+
19
+ def method_missing(method, *args)
20
+ line_name = method.to_s.chomp('=').to_sym
21
+ if lines_definition[line_name]
22
+ set_players_in_custom_line(lines_definition[line_name], args)
23
+ end
24
+ end
25
+
26
+ def lineup(&block)
27
+ instance_eval(&block)
28
+ end
29
+
30
+ def errors
31
+ errors = []
32
+
33
+ # TODO Check if we need to count for the goalkeeper as well
34
+ if players_position.size != 10
35
+ errors << 'Incorrect number of players, are you missing a name?'
36
+ end
37
+
38
+ errors
39
+ end
40
+
41
+ def valid?
42
+ errors.empty?
43
+ end
44
+
45
+ private
46
+
47
+ def lines(&block)
48
+ CustomLines.apply(self, &block)
49
+ end
50
+
51
+ def custom_position(&block)
52
+ CustomPosition.apply(self, &block)
53
+ end
54
+
55
+ def set_players_in_custom_line(position_x, players)
56
+ base_position = Position.new(position_x, 0)
57
+ separation = line_position_separation(players)
58
+
59
+ players.each_with_index do |player, i|
60
+ next if player == :none
61
+
62
+ offset = Position.new(0, separation * (i + 1))
63
+ position = base_position.add(offset)
64
+
65
+ self.players_position[player] = position
66
+ end
67
+ end
68
+
69
+ def line_position_separation(players)
70
+ Field::HEIGHT / (players.size + 1)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,67 @@
1
+ module Rubygoal
2
+ class Formation
3
+ class FormationDSL
4
+ def self.apply(formation, &block)
5
+ dsl = self.new(formation)
6
+ dsl.instance_eval(&block)
7
+ dsl.apply
8
+ end
9
+
10
+ private
11
+
12
+ def field_width
13
+ 100.0
14
+ end
15
+
16
+ def field_height
17
+ 100.0
18
+ end
19
+ end
20
+
21
+ class CustomLines < FormationDSL
22
+ def initialize(formation)
23
+ @formation = formation
24
+ @lines = {}
25
+ end
26
+
27
+ def apply
28
+ formation.lines_definition.merge!(lines)
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :formation, :lines
34
+
35
+ def method_missing(method, *args)
36
+ define_line(method, args.first)
37
+ end
38
+
39
+ def define_line(name, x_position)
40
+ lines[name] = x_position / 100.0 * Field::WIDTH
41
+ end
42
+ end
43
+
44
+ class CustomPosition < FormationDSL
45
+ def initialize(formation)
46
+ @formation = formation
47
+ end
48
+
49
+ def apply
50
+ formation.players_position[player_name] = player_position
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :formation
56
+ attr_accessor :player_name, :player_position
57
+
58
+ def player(name)
59
+ self.player_name = name
60
+ end
61
+
62
+ def position(x, y)
63
+ self.player_position = Field.position_from_percentages(Position.new(x, y))
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,162 @@
1
+ require 'forwardable'
2
+
3
+ require 'rubygoal/coordinate'
4
+ require 'rubygoal/field'
5
+ require 'rubygoal/goal'
6
+ require 'rubygoal/recorder'
7
+
8
+ module Rubygoal
9
+ class Game
10
+ attr_reader :team_home, :team_away, :ball,
11
+ :time, :goal, :recorder,
12
+ :coach_home, :coach_away,
13
+ :score_home, :score_away
14
+
15
+ def initialize(coach_home, coach_away)
16
+ @coach_home = coach_home
17
+ @coach_away = coach_away
18
+
19
+ if debug_output?
20
+ puts "Home coach: #{@coach_home.name}"
21
+ puts "Away coach: #{@coach_away.name}"
22
+ end
23
+
24
+ @recorder = Recorder.new(self) if record_game?
25
+
26
+ @ball = Ball.new
27
+
28
+ @team_home = HomeTeam.new(self, coach_home)
29
+ @team_away = AwayTeam.new(self, coach_away)
30
+
31
+ @goal = Goal.new
32
+
33
+ @state = :playing
34
+
35
+ @time = Rubygoal.configuration.game_time
36
+ @score_home = 0
37
+ @score_away = 0
38
+
39
+ reinitialize_players
40
+ end
41
+
42
+ def update
43
+ return if ended?
44
+
45
+ update_elapsed_time
46
+
47
+ if celebrating_goal?
48
+ update_goal
49
+ else
50
+ update_remaining_time
51
+ team_home.update(elapsed_time)
52
+ team_away.update(elapsed_time)
53
+ update_ball
54
+ end
55
+
56
+ recorder.update if record_game?
57
+
58
+ end_match! if time <= 0
59
+ end
60
+
61
+ def players
62
+ teams.map(&:players_list).flatten
63
+ end
64
+
65
+ def celebrating_goal?
66
+ goal.celebrating?
67
+ end
68
+
69
+ def home_players_positions
70
+ team_home.players_position
71
+ end
72
+
73
+ def away_players_positions
74
+ team_away.players_position
75
+ end
76
+
77
+ def ball_position
78
+ ball.position
79
+ end
80
+
81
+ def recorded_game
82
+ recorder.to_hash if record_game?
83
+ end
84
+
85
+ def ended?
86
+ state == :ended
87
+ end
88
+
89
+ protected
90
+
91
+ attr_writer :time, :score_home, :score_away
92
+ attr_accessor :state, :last_time, :elapsed_time
93
+
94
+ private
95
+
96
+ def update_elapsed_time
97
+ self.last_time ||= Time.now
98
+
99
+ self.elapsed_time = Time.now - last_time
100
+ self.last_time = Time.now
101
+ end
102
+
103
+ def update_remaining_time
104
+ self.time -= elapsed_time
105
+ end
106
+
107
+ def update_ball
108
+ ball.update(elapsed_time)
109
+ if ball.goal?
110
+ update_score
111
+ goal.start_celebration
112
+ end
113
+ end
114
+
115
+ def update_goal
116
+ goal.update(elapsed_time)
117
+ reinitialize_match unless goal.celebrating?
118
+ end
119
+
120
+ def reinitialize_match
121
+ reinitialize_players
122
+ reinitialize_ball
123
+ end
124
+
125
+ def update_score
126
+ if Field.position_side(ball_position) == :home
127
+ self.score_away += 1
128
+ else
129
+ self.score_home += 1
130
+ end
131
+ end
132
+
133
+ def reinitialize_players
134
+ teams.each(&:players_to_initial_position)
135
+ end
136
+
137
+ def reinitialize_ball
138
+ ball.position = Field.center_position
139
+ end
140
+
141
+ def teams
142
+ [team_home, team_away]
143
+ end
144
+
145
+ def end_match!
146
+ self.state = :ended
147
+ puts_score if debug_output?
148
+ end
149
+
150
+ def puts_score
151
+ puts "#{coach_home.name} #{score_home} - #{score_away} #{coach_away.name}"
152
+ end
153
+
154
+ def debug_output?
155
+ Rubygoal.configuration.debug_output
156
+ end
157
+
158
+ def record_game?
159
+ Rubygoal.configuration.record_game
160
+ end
161
+ end
162
+ end