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,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
|
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
|
data/test/coach_test.rb
ADDED
@@ -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
|