├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.dev ├── LICENSE ├── README.md ├── Rakefile ├── bin └── rubygoal ├── gui ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin │ └── rubygoal ├── lib │ ├── rubygoal.rb │ └── rubygoal │ │ ├── gui.rb │ │ └── gui │ │ ├── ball.rb │ │ ├── field.rb │ │ ├── game.rb │ │ ├── goal.rb │ │ ├── players.rb │ │ └── version.rb ├── media │ ├── average_away.png │ ├── average_home.png │ ├── background.png │ ├── ball.png │ ├── captain_away.png │ ├── captain_home.png │ ├── fast_away.png │ ├── fast_home.png │ └── goal.png └── rubygoal.gemspec ├── lib ├── rubygoal.rb └── rubygoal │ ├── ball.rb │ ├── coach.rb │ ├── coach_definition.rb │ ├── coach_loader.rb │ ├── coaches │ ├── coach_definition_away.rb │ └── coach_definition_home.rb │ ├── configuration.rb │ ├── coordinate.rb │ ├── field.rb │ ├── formation.rb │ ├── formation │ └── formation_dsl.rb │ ├── game.rb │ ├── goal.rb │ ├── match_data.rb │ ├── moveable.rb │ ├── player.rb │ ├── players │ ├── average.rb │ ├── captain.rb │ ├── fast.rb │ ├── goalkeeper.rb │ └── player_movement.rb │ ├── recorder.rb │ ├── simulator.rb │ ├── team.rb │ ├── teams │ ├── away.rb │ └── home.rb │ ├── util.rb │ └── version.rb ├── rubygoal-core.gemspec ├── test ├── ball_test.rb ├── coach_test.rb ├── field_test.rb ├── fixtures │ ├── four_fast_players_coach_definition.rb │ ├── less_players_coach_definition.rb │ ├── mirror_strategy_coach_definition.rb │ ├── more_players_coach_definition.rb │ ├── test_away_coach_definition.rb │ ├── test_home_coach_definition.rb │ ├── two_captains_coach_definition.rb │ └── valid_coach_definition.rb ├── formation_test.rb ├── game_test.rb ├── match_data_test.rb ├── player_test.rb ├── players │ ├── goalkeeper_test.rb │ └── player_movement_test.rb ├── recorder_test.rb ├── team_test.rb └── test_helper.rb └── test_mirror.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | Gemfile.dev.lock 3 | .ruby-version 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - "2.2.2" 4 | sudo: false 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.dev: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "rubygoal", path: 'gui' 4 | gem "rubygoal-core", path: '.' 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Jorge Bejar 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | ----- 16 | 17 | This does not apply to files under the "media/" folder. For licensing of 18 | those files refer to the Legal section in the README.md file. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/wyeworks/rubygoal.png)](https://travis-ci.org/wyeworks/rubygoal) 2 | [![Code Climate](https://codeclimate.com/github/wyeworks/rubygoal.png)](https://codeclimate.com/github/wyeworks/rubygoal) 3 | [![Inline docs](http://inch-ci.org/github/wyeworks/rubygoal.png?branch=master)](http://inch-ci.org/github/wyeworks/rubygoal) 4 | 5 | #Welcome to RubyGoal! 6 | 7 | ## What is RubyGoal? 8 | 9 | RubyGoal is a game in which you will be coaching your football team. 10 | 11 | You will be coding your strategy in RUBY :D . 12 | 13 | ## Dependencies 14 | 15 | GNU/Linux, Make sure you have all dependencies installed. 16 | 17 | Ubuntu/Debian: 18 | 19 | ```bash 20 | # Gosu's dependencies for both C++ and Ruby 21 | sudo apt-get install build-essential libsdl2-dev libsdl2-ttf-dev 22 | libpango1.0-dev \ 23 | libgl1-mesa-dev libfreeimage-dev libopenal-dev 24 | libsndfile-dev 25 | ``` 26 | 27 | For other distros: https://github.com/jlnr/gosu/wiki/Getting-Started-on-Linux 28 | 29 | ## How do i run it? 30 | 31 | ```bash 32 | gem install rubygoal 33 | ``` 34 | 35 | Run the game with example `CoachDefinition` 36 | ```bash 37 | rubygoal 38 | ``` 39 | 40 | Run the game with your custom `CoachDefinition` implementation 41 | ```bash 42 | rubygoal coach_1.rb 43 | ``` 44 | 45 | Run the game with your home and away `CoachDefinition` implementations 46 | ```bash 47 | rubygoal coach_1.rb coach_2.rb 48 | ``` 49 | 50 | If you want to run the game from the source code, clone the project and 51 | run the following commands: 52 | 53 | ```bash 54 | BUNDLE_GEMFILE=Gemfile.dev bundle install 55 | BUNDLE_GEMFILE=Gemfile.dev bundle exec ruby gui/bin/rubygoal [coach_file] [coach_file] 56 | ``` 57 | 58 | Also, you can simulate a game without the GUI by running 59 | 60 | ```bash 61 | bundle install 62 | bundle exec ruby bin/rubygoal [coach_file] [coach_file] 63 | ``` 64 | 65 | When you simuate a game, a JSON file is created in the same folder. You 66 | could run this using our experiment webcomponent to play Rubygoal in the 67 | web: https://github.com/jmbejar/rubygoal-webplayer 68 | 69 | ## How do i write my own coach class? 70 | 71 | You can find a complete guide explaining how to program a coach in 72 | www.rubygoal.com 73 | 74 | Aditionally, you can take a look to the already defined `CoachDefinition` at 75 | `lib/rubygoal/coach_definition`. 76 | Specially pay attention to the example coaches in `lib/rubygoal/coaches/` 77 | 78 | 79 | ## Legal 80 | All source code, except the files under the `media/` folder, is 81 | licensed under the Apache License 2.0. Please see the `LICENSE` file under 82 | the gem root folder. 83 | 84 | All media files under the `media/` folder are licensed under the Creative 85 | Commons 4.0 Attribution license. Please see https://creativecommons.org/licenses/by/4.0/ 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # 3 | require 'rake/testtask' 4 | 5 | desc 'Run tests' 6 | test_task = Rake::TestTask.new(:test) do |t| 7 | t.libs << 'test' 8 | t.pattern = 'test/**/*_test.rb' 9 | t.verbose = true 10 | end 11 | 12 | task default: :test 13 | 14 | desc 'Run tests in isolated processes' 15 | namespace :test do 16 | task :isolated do 17 | Dir[test_task.pattern].each do |file| 18 | cmd = ['ruby'] 19 | test_task.libs.each { |l| cmd << '-I' << l } 20 | cmd << file 21 | sh cmd.join(' ') 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /bin/rubygoal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # -*- mode: ruby -*- 3 | 4 | require 'rubygoal/simulator' 5 | 6 | simulator = Rubygoal::Simulator.new 7 | simulator.simulate 8 | 9 | timestamp = Time.now.strftime("%Y%m%d%H%M%S") 10 | file = File.new("recorded_game_#{timestamp}.json", 'w') 11 | JSON.dump(simulator.recorded_game, file) 12 | 13 | p "Match recorded in #{file.path}" 14 | 15 | 16 | -------------------------------------------------------------------------------- /gui/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /gui/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Jorge Bejar 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | ----- 16 | 17 | This does not apply to files under the "media/" folder. For licensing of 18 | those files refer to the Legal section in the README.md file. 19 | -------------------------------------------------------------------------------- /gui/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/wyeworks/rubygoal.png)](https://travis-ci.org/wyeworks/rubygoal) 2 | [![Code Climate](https://codeclimate.com/github/wyeworks/rubygoal.png)](https://codeclimate.com/github/wyeworks/rubygoal) 3 | [![Inline docs](http://inch-ci.org/github/wyeworks/rubygoal.png?branch=master)](http://inch-ci.org/github/wyeworks/rubygoal) 4 | 5 | #Welcome to RubyGoal! 6 | 7 | ## What is RubyGoal? 8 | 9 | RubyGoal is a game in which you will be coaching your football team. 10 | 11 | You will be coding your strategy in RUBY :D . 12 | 13 | ## Dependencies 14 | 15 | GNU/Linux, Make sure you have all dependencies installed. 16 | 17 | Ubuntu/Debian: 18 | 19 | ```bash 20 | # Gosu's dependencies for both C++ and Ruby 21 | sudo apt-get install build-essential libsdl2-dev libsdl2-ttf-dev 22 | libpango1.0-dev \ 23 | libgl1-mesa-dev libfreeimage-dev libopenal-dev 24 | libsndfile-dev 25 | ``` 26 | 27 | For other distros: https://github.com/jlnr/gosu/wiki/Getting-Started-on-Linux 28 | 29 | ## How do i run it? 30 | 31 | ```bash 32 | gem install rubygoal 33 | ``` 34 | 35 | Run the game with example `CoachDefinition` 36 | ```bash 37 | rubygoal 38 | ``` 39 | 40 | Run the game with your custom `CoachDefinition` implementation 41 | ```bash 42 | rubygoal coach_1.rb 43 | ``` 44 | 45 | Run the game with your home and away `CoachDefinition` implementations 46 | ```bash 47 | rubygoal coach_1.rb coach_2.rb 48 | ``` 49 | 50 | If you want to run the game from the source code, clone the project and 51 | run the following commands: 52 | 53 | ```bash 54 | BUNDLE_GEMFILE=Gemfile.dev bundle install 55 | BUNDLE_GEMFILE=Gemfile.dev bundle exec ruby gui/bin/rubygoal [coach_file] [coach_file] 56 | ``` 57 | 58 | Also, you can simulate a game without the GUI by running 59 | 60 | ```bash 61 | bundle install 62 | bundle exec ruby bin/rubygoal [coach_file] [coach_file] 63 | ``` 64 | 65 | When you simuate a game, a JSON file is created in the same folder. You 66 | could run this using our experiment webcomponent to play Rubygoal in the 67 | web: https://github.com/jmbejar/rubygoal-webplayer 68 | 69 | ## How do i write my own coach class? 70 | 71 | You can find a complete guide explaining how to program a coach in 72 | www.rubygoal.com 73 | 74 | Aditionally, you can take a look to the already defined `CoachDefinition` at 75 | `lib/rubygoal/coach_definition`. 76 | Specially pay attention to the example coaches in `lib/rubygoal/coaches/` 77 | 78 | 79 | ## Legal 80 | All source code, except the files under the `media/` folder, is 81 | licensed under the Apache License 2.0. Please see the `LICENSE` file under 82 | the gem root folder. 83 | 84 | All media files under the `media/` folder are licensed under the Creative 85 | Commons 4.0 Attribution license. Please see https://creativecommons.org/licenses/by/4.0/ 86 | -------------------------------------------------------------------------------- /gui/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # 3 | require 'rake/testtask' 4 | 5 | desc 'Run tests' 6 | test_task = Rake::TestTask.new(:test) do |t| 7 | t.libs << 'test' 8 | t.pattern = 'test/**/*_test.rb' 9 | t.verbose = true 10 | end 11 | 12 | task default: :test 13 | 14 | desc 'Run tests in isolated processes' 15 | namespace :test do 16 | task :isolated do 17 | Dir[test_task.pattern].each do |file| 18 | cmd = ['ruby'] 19 | test_task.libs.each { |l| cmd << '-I' << l } 20 | cmd << file 21 | sh cmd.join(' ') 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /gui/bin/rubygoal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # -*- mode: ruby -*- 3 | 4 | require 'rubygoal/gui' 5 | Rubygoal::Gui.start 6 | -------------------------------------------------------------------------------- /gui/lib/rubygoal.rb: -------------------------------------------------------------------------------- 1 | require 'rubygoal-core' 2 | require 'rubygoal/gui/game' 3 | 4 | module Rubygoal 5 | class << self 6 | def start 7 | game = Game.new(load_coach(:home), load_coach(:away)) 8 | gui = Gui::Game.new(game) 9 | gui.show 10 | end 11 | 12 | private 13 | 14 | def load_coach(side) 15 | CoachLoader.new(side).coach 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /gui/lib/rubygoal/gui.rb: -------------------------------------------------------------------------------- 1 | require 'rubygoal' 2 | require 'rubygoal/gui/game' 3 | 4 | module Rubygoal 5 | module Gui 6 | class << self 7 | def start 8 | game = Rubygoal::Game.new(load_coach(:home), load_coach(:away)) 9 | gui = Game.new(game) 10 | gui.show 11 | end 12 | 13 | private 14 | 15 | def load_coach(side) 16 | CoachLoader.new(side).coach 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /gui/lib/rubygoal/gui/ball.rb: -------------------------------------------------------------------------------- 1 | require 'gosu' 2 | 3 | module Rubygoal::Gui 4 | class Ball 5 | IMAGE_SIZE = 20 6 | 7 | def initialize(window, ball) 8 | @ball = ball 9 | 10 | image_path = File.dirname(__FILE__) + '/../../../media/ball.png' 11 | @image = Gosu::Image.new(window, image_path, false) 12 | end 13 | 14 | def draw 15 | half_side_lenght = IMAGE_SIZE / 2 16 | image_center_x = ball.position.x - half_side_lenght 17 | image_center_y = ball.position.y - half_side_lenght 18 | 19 | image.draw(image_center_x, image_center_y, 1) 20 | end 21 | 22 | private 23 | 24 | attr_reader :ball, :image 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /gui/lib/rubygoal/gui/field.rb: -------------------------------------------------------------------------------- 1 | require 'gosu' 2 | 3 | require 'rubygoal/gui/ball' 4 | require 'rubygoal/gui/players' 5 | 6 | module Rubygoal::Gui 7 | class Field 8 | def initialize(window) 9 | image_path = File.dirname(__FILE__) + '/../../../media/background.png' 10 | @background_image = Gosu::Image.new(window, image_path, true) 11 | end 12 | 13 | def draw 14 | background_image.draw(0, 0, 0); 15 | end 16 | 17 | private 18 | 19 | attr_reader :background_image 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /gui/lib/rubygoal/gui/game.rb: -------------------------------------------------------------------------------- 1 | require 'gosu' 2 | 3 | require 'rubygoal/gui/goal' 4 | require 'rubygoal/gui/field' 5 | require 'rubygoal/gui/ball' 6 | require 'rubygoal/gui/players' 7 | 8 | require 'rubygoal/coordinate' 9 | require 'rubygoal/game' 10 | 11 | module Rubygoal::Gui 12 | class Game < Gosu::Window 13 | WINDOW_WIDTH = 1920 14 | WINDOW_HEIGHT = 1080 15 | 16 | TIME_LABEL_POSITION = Rubygoal::Position.new(870, 68) 17 | SCORE_HOME_LABEL_POSITION = Rubygoal::Position.new(1150, 68) 18 | SCORE_AWAY_LABEL_POSITION = Rubygoal::Position.new(1220, 68) 19 | 20 | TEAM_NAME_HOME_LABEL_POSITION = Rubygoal::Position.new(105, 580) 21 | TEAM_NAME_AWAY_LABEL_POSITION = Rubygoal::Position.new(1815, 580) 22 | 23 | DEFAULT_FONT_SIZE = 48 24 | 25 | LABEL_IMAGE_FONT_SIZE = 64 26 | LABEL_IMAGE_WIDTH = 669 27 | 28 | def initialize(game) 29 | super(WINDOW_WIDTH, WINDOW_HEIGHT, false) 30 | self.caption = "Ruby Goal" 31 | 32 | @game = game 33 | 34 | @gui_field = Field.new(self) 35 | @gui_goal = Goal.new(self) 36 | @gui_ball = Ball.new(self, game.ball) 37 | @gui_players = players.map { |p| Players.new(self, p) } 38 | 39 | @font = Gosu::Font.new( 40 | self, 41 | Gosu.default_font_name, 42 | DEFAULT_FONT_SIZE 43 | ) 44 | 45 | @home_team_label = create_label_image(game.coach_home.name) 46 | @away_team_label = create_label_image(game.coach_away.name) 47 | end 48 | 49 | def update 50 | game.update 51 | end 52 | 53 | def draw 54 | gui_field.draw 55 | gui_ball.draw 56 | gui_players.each(&:draw) 57 | 58 | draw_scoreboard 59 | draw_team_labels 60 | gui_goal.draw if game.celebrating_goal? 61 | end 62 | 63 | def button_down(id) 64 | if id == Gosu::KbEscape 65 | close 66 | end 67 | end 68 | 69 | private 70 | 71 | def players 72 | game.players 73 | end 74 | 75 | def create_label_image(name) 76 | name_characters_limit = 20 77 | name = truncate_label(name, name_characters_limit) 78 | 79 | font_name = Gosu.default_font_name 80 | font_size = LABEL_IMAGE_FONT_SIZE 81 | line_spacing = 1 82 | label_width = LABEL_IMAGE_WIDTH 83 | alignment = :center 84 | 85 | Gosu::Image.from_text( 86 | self, 87 | name, 88 | font_name, 89 | font_size, 90 | line_spacing, 91 | label_width, 92 | alignment 93 | ) 94 | end 95 | 96 | def truncate_label(name, limit) 97 | words = name.split 98 | 99 | left = limit 100 | truncated = [] 101 | words.each do |word| 102 | break unless left > 0 103 | 104 | truncated << word 105 | left -= word.length + 1 106 | end 107 | 108 | truncated.join(' ') 109 | end 110 | 111 | def draw_scoreboard 112 | draw_text(time_text, TIME_LABEL_POSITION, :gray) 113 | draw_text(game.score_home.to_s, SCORE_HOME_LABEL_POSITION, :white) 114 | draw_text(game.score_away.to_s, SCORE_AWAY_LABEL_POSITION, :white) 115 | end 116 | 117 | def draw_team_labels 118 | draw_vertical_text(home_team_label, TEAM_NAME_HOME_LABEL_POSITION, :down) 119 | draw_vertical_text(away_team_label, TEAM_NAME_AWAY_LABEL_POSITION, :up) 120 | end 121 | 122 | def draw_text(text, position, color, scale = 1) 123 | horizontal_alignment = 0.5 124 | vertical_alignment = 0.5 125 | z_order = 1 126 | horizontal_scale = scale 127 | vertical_scale = scale 128 | 129 | font.draw_rel( 130 | text, 131 | position.x, 132 | position.y, 133 | horizontal_alignment, 134 | vertical_alignment, 135 | z_order, 136 | horizontal_scale, 137 | vertical_scale, 138 | color_to_hex(color) 139 | ) 140 | end 141 | 142 | def draw_vertical_text(label, position, direction = :down) 143 | angle = direction == :down ? -90 : 90 144 | label.draw_rot(position.x, position.y, 1, angle) 145 | end 146 | 147 | def time_text 148 | minutes = Integer(game.time) / 60 149 | seconds = Integer(game.time) % 60 150 | "%d:%02d" % [minutes, seconds] 151 | end 152 | 153 | def color_to_hex(color) 154 | case color 155 | when :white 156 | 0xffffffff 157 | when :gray 158 | 0xff6d6e70 159 | end 160 | end 161 | 162 | attr_reader :game, :gui_field, :gui_goal, 163 | :gui_ball, :gui_players, 164 | :font, :home_team_label, :away_team_label 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /gui/lib/rubygoal/gui/goal.rb: -------------------------------------------------------------------------------- 1 | require 'gosu' 2 | 3 | require 'rubygoal/coordinate' 4 | 5 | module Rubygoal::Gui 6 | class Goal 7 | CELEBRATION_IMAGE_POSITION = Rubygoal::Position.new(680, 466) 8 | 9 | def initialize(window) 10 | image_path = File.dirname(__FILE__) + '/../../../media/goal.png' 11 | @image = Gosu::Image.new(window, image_path, true) 12 | end 13 | 14 | def draw 15 | position = CELEBRATION_IMAGE_POSITION 16 | image.draw(position.x, position.y, 1) 17 | end 18 | 19 | private 20 | 21 | attr_reader :image 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /gui/lib/rubygoal/gui/players.rb: -------------------------------------------------------------------------------- 1 | require 'gosu' 2 | require 'forwardable' 3 | 4 | require 'rubygoal/util' 5 | 6 | module Rubygoal::Gui 7 | class Players 8 | extend Forwardable 9 | def_delegators :player, :type, :side, :position, :destination, :rotation, :moving? 10 | 11 | def initialize(window, player) 12 | @player = player 13 | @image = Gosu::Image.new(window, image_filename, false) 14 | end 15 | 16 | def draw 17 | image.draw_rot(position.x, position.y, 1, rotation - 180) 18 | end 19 | 20 | private 21 | 22 | def image_filename 23 | File.dirname(__FILE__) + "/../../../media/#{type}_#{side}.png" 24 | end 25 | 26 | attr_reader :player, :image 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /gui/lib/rubygoal/gui/version.rb: -------------------------------------------------------------------------------- 1 | module Rubygoal 2 | module Gui 3 | VERSION = Gem::Version.new '2.1.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /gui/media/average_away.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyeworks/rubygoal/88e643f925b7a7b72aaa1a611eb458113e68af28/gui/media/average_away.png -------------------------------------------------------------------------------- /gui/media/average_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyeworks/rubygoal/88e643f925b7a7b72aaa1a611eb458113e68af28/gui/media/average_home.png -------------------------------------------------------------------------------- /gui/media/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyeworks/rubygoal/88e643f925b7a7b72aaa1a611eb458113e68af28/gui/media/background.png -------------------------------------------------------------------------------- /gui/media/ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyeworks/rubygoal/88e643f925b7a7b72aaa1a611eb458113e68af28/gui/media/ball.png -------------------------------------------------------------------------------- /gui/media/captain_away.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyeworks/rubygoal/88e643f925b7a7b72aaa1a611eb458113e68af28/gui/media/captain_away.png -------------------------------------------------------------------------------- /gui/media/captain_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyeworks/rubygoal/88e643f925b7a7b72aaa1a611eb458113e68af28/gui/media/captain_home.png -------------------------------------------------------------------------------- /gui/media/fast_away.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyeworks/rubygoal/88e643f925b7a7b72aaa1a611eb458113e68af28/gui/media/fast_away.png -------------------------------------------------------------------------------- /gui/media/fast_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyeworks/rubygoal/88e643f925b7a7b72aaa1a611eb458113e68af28/gui/media/fast_home.png -------------------------------------------------------------------------------- /gui/media/goal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyeworks/rubygoal/88e643f925b7a7b72aaa1a611eb458113e68af28/gui/media/goal.png -------------------------------------------------------------------------------- /gui/rubygoal.gemspec: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../lib", __FILE__) 2 | require 'rubygoal/gui/version' 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ['Jorge Bejar'] 6 | gem.email = ['jorge@wyeworks.com'] 7 | gem.description = %q{Rubygoal} 8 | gem.summary = %q{Rubygoal Game - Soccer game for Rubysts} 9 | gem.homepage = 'https://github.com/wyeworks/rubygoal' 10 | gem.license = 'Apache License 2.0' 11 | 12 | gem.files = Dir['README.md', 'LICENSE', 'bin/**/*', 'lib/**/*', 'media/**/*'] 13 | gem.executables = %w[rubygoal] 14 | 15 | gem.name = 'rubygoal' 16 | gem.require_paths = ['lib'] 17 | gem.version = Rubygoal::Gui::VERSION.to_s 18 | 19 | gem.required_ruby_version = '>= 2.2.2' 20 | 21 | gem.add_dependency 'rubygoal-core', '~> 1.1' 22 | gem.add_dependency 'gosu', '~> 0.9' 23 | end 24 | -------------------------------------------------------------------------------- /lib/rubygoal.rb: -------------------------------------------------------------------------------- 1 | require 'rubygoal/game' 2 | -------------------------------------------------------------------------------- /lib/rubygoal/ball.rb: -------------------------------------------------------------------------------- 1 | require 'rubygoal/util' 2 | require 'rubygoal/field' 3 | require 'rubygoal/moveable' 4 | 5 | module Rubygoal 6 | class Ball 7 | include Moveable 8 | attr_reader :last_kicker 9 | 10 | def initialize 11 | super 12 | reinitialize_position 13 | end 14 | 15 | def goal? 16 | Field.goal?(position) 17 | end 18 | 19 | def move(direction, speed, kicker) 20 | self.velocity = Velocity.new( 21 | Util.offset_x(direction, speed), 22 | Util.offset_y(direction, speed) 23 | ) 24 | self.last_kicker = kicker 25 | end 26 | 27 | def reinitialize_position 28 | self.position = Field.center_position 29 | self.last_kicker = nil 30 | end 31 | 32 | def update(elapsed_time) 33 | super 34 | 35 | prevent_out_of_bounds 36 | decelerate(elapsed_time) 37 | end 38 | 39 | private 40 | 41 | attr_writer :last_kicker 42 | 43 | def prevent_out_of_bounds 44 | velocity.x *= -1 if Field.out_of_bounds_width?(position) 45 | velocity.y *= -1 if Field.out_of_bounds_height?(position) 46 | end 47 | 48 | def decelerate(elapsed_time) 49 | coef = deceleration_coef(elapsed_time) 50 | 51 | self.velocity = velocity.mult(coef) 52 | end 53 | 54 | def deceleration_coef(elapsed_time) 55 | custom_frame_rate = 1 / 60.0 56 | time_coef = elapsed_time / custom_frame_rate 57 | 58 | Rubygoal.configuration.deceleration_coef ** time_coef 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/rubygoal/coach.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Rubygoal 4 | class Coach 5 | extend Forwardable 6 | 7 | def_delegators :coach_definition, :players, :name, :formation 8 | 9 | def initialize(coach_definition) 10 | @coach_definition = coach_definition 11 | end 12 | 13 | def errors 14 | [].tap do |errors| 15 | check_unique_captain(errors) 16 | check_players_count(:average, errors) 17 | check_players_count(:fast, errors) 18 | end 19 | end 20 | 21 | def valid? 22 | errors.empty? 23 | end 24 | 25 | def players_by_type(type) 26 | players.select { |p| p.type == type } 27 | end 28 | 29 | def captain_player 30 | players_by_type(:captain).first 31 | end 32 | 33 | def fast_players 34 | players_by_type(:fast) 35 | end 36 | 37 | def average_players 38 | players_by_type(:average) 39 | end 40 | 41 | def initial_formation 42 | average_names = average_players.map(&:name) 43 | fast_names = fast_players.map(&:name) 44 | captain_name = captain_player.name 45 | 46 | formation = Formation.new 47 | 48 | formation.lineup do 49 | defenders average_names[0], average_names[2], :none, average_names[3], average_names[4] 50 | midfielders average_names[1], fast_names[0], :none, fast_names[1], average_names[5] 51 | attackers :none, captain_name, :none, fast_names[2], :none 52 | end 53 | 54 | formation 55 | end 56 | 57 | private 58 | 59 | attr_reader :coach_definition 60 | 61 | def game_config 62 | Rubygoal.configuration 63 | end 64 | 65 | def check_unique_captain(errors) 66 | captain_count = players_by_type(:captain).size 67 | 68 | if captain_count != 1 69 | errors << "The number of captains is #{captain_count}" 70 | end 71 | end 72 | 73 | def check_players_count(type, errors) 74 | players_count = players_by_type(type).size 75 | 76 | if players_count != game_config.send("#{type}_players_count") 77 | errors << "The number of #{type} players is #{players_count}" 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/rubygoal/coach_definition.rb: -------------------------------------------------------------------------------- 1 | module Rubygoal 2 | class CoachDefinition 3 | PlayerDefinition = Struct.new(:name, :type) 4 | 5 | class << self 6 | attr_reader :team_name 7 | 8 | def team(&block) 9 | instance_eval(&block) 10 | end 11 | 12 | def name(team_name) 13 | @team_name = team_name 14 | end 15 | 16 | def players(&block) 17 | @team_players = [] 18 | instance_eval(&block) 19 | end 20 | 21 | def method_missing(method, *args) 22 | name = args.first.to_sym 23 | @team_players << PlayerDefinition.new(name, method.to_sym) 24 | end 25 | 26 | def team_players 27 | @team_players || [ 28 | PlayerDefinition.new(:captain, :captain), 29 | PlayerDefinition.new(:fast1, :fast), 30 | PlayerDefinition.new(:fast2, :fast), 31 | PlayerDefinition.new(:fast3, :fast), 32 | PlayerDefinition.new(:average1, :average), 33 | PlayerDefinition.new(:average2, :average), 34 | PlayerDefinition.new(:average3, :average), 35 | PlayerDefinition.new(:average4, :average), 36 | PlayerDefinition.new(:average5, :average), 37 | PlayerDefinition.new(:average6, :average), 38 | ] 39 | end 40 | end 41 | 42 | def players 43 | self.class.team_players 44 | end 45 | 46 | def name 47 | self.class.team_name 48 | end 49 | 50 | def formation(match) 51 | raise NotImplementedError 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/rubygoal/coach_loader.rb: -------------------------------------------------------------------------------- 1 | require 'rubygoal/coach' 2 | require 'rubygoal/coaches/coach_definition_home' 3 | require 'rubygoal/coaches/coach_definition_away' 4 | 5 | module Rubygoal 6 | class CoachLoader 7 | DEFAULT_COACH_DEFINITIONS = { 8 | home: CoachDefinitionHome, 9 | away: CoachDefinitionAway 10 | } 11 | 12 | def initialize(side) 13 | @side = side 14 | end 15 | 16 | def coach 17 | Coach.new(load_definition_coach) 18 | end 19 | 20 | private 21 | 22 | attr_reader :side 23 | 24 | def default_definition_class 25 | DEFAULT_COACH_DEFINITIONS[side] 26 | end 27 | 28 | def load_definition_coach 29 | if filename && File.exists?(filename) 30 | load filename 31 | 32 | class_name = camelize(File.basename(filename, ".rb")) 33 | Rubygoal.const_get(class_name).new 34 | else 35 | if filename && Rubygoal.configuration.debug_output 36 | puts "File `#{filename}` doesn't exist. Using #{default_definition_class.name}." 37 | end 38 | 39 | default_definition_class.new 40 | end 41 | end 42 | 43 | def filename 44 | side == :home ? ARGV[0] : ARGV[1] 45 | end 46 | 47 | def camelize(term) 48 | string = term.to_s 49 | string = string.sub(/^[a-z\d]*/) { $&.capitalize } 50 | string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" } 51 | string.gsub!('/', '::') 52 | string 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/rubygoal/coaches/coach_definition_away.rb: -------------------------------------------------------------------------------- 1 | require 'rubygoal/coach_definition' 2 | require 'rubygoal/formation' 3 | 4 | module Rubygoal 5 | class CoachDefinitionAway < CoachDefinition 6 | 7 | team do 8 | name "Argentina" 9 | end 10 | 11 | def formation(match) 12 | formation = Formation.new 13 | 14 | if match.me.winning? 15 | formation.lineup do 16 | defenders :average1, :fast1, :none, :fast3, :average5 17 | midfielders :average2, :none, :captain, :none, :average6 18 | att_midfielders :average3 19 | attackers :none, :none, :fast2, :average4, :none 20 | end 21 | elsif match.me.draw? 22 | formation.lineup do 23 | lines do 24 | defenders 13 25 | midfielders 40 26 | attackers 65 27 | end 28 | 29 | defenders :average1, :fast1, :none, :average3, :average5 30 | midfielders :average2, :none, :none, :none, :average6 31 | attackers :none, :fast2, :none, :average4, :none 32 | 33 | custom_position do 34 | player :fast3 35 | position 30, 10 36 | end 37 | custom_position do 38 | player :captain 39 | position match.ball.x, match.ball.y 40 | end 41 | end 42 | elsif match.me.losing? 43 | if match.time < 30 44 | formation.lineup do 45 | defenders :none, :average2, :average4, :average5, :none 46 | midfielders :average1, :none, :none, :fast2, :average6 47 | attackers :none, :average3, :none, :fast3, :none 48 | 49 | custom_position do 50 | player :fast1 51 | position 33, 50 52 | end 53 | custom_position do 54 | player :captain 55 | position 67, 50 56 | end 57 | end 58 | else 59 | formation.lineup do 60 | defenders :none, :average2, :fast1, :average5, :none 61 | def_midfielders :average4 62 | midfielders :average1, :none, :none, :captain, :average6 63 | att_midfielders :fast2 64 | attackers :none, :average3, :none, :fast3, :none 65 | end 66 | end 67 | end 68 | 69 | formation 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/rubygoal/coaches/coach_definition_home.rb: -------------------------------------------------------------------------------- 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 77 | -------------------------------------------------------------------------------- /lib/rubygoal/configuration.rb: -------------------------------------------------------------------------------- 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 | :record_last_kicker 12 | end 13 | 14 | class << self 15 | attr_writer :configuration 16 | 17 | def configuration 18 | @configuration ||= Configuration.new 19 | end 20 | 21 | def configure 22 | yield(configuration) 23 | end 24 | end 25 | 26 | Rubygoal.configure do |config| 27 | config.average_players_count = 6 28 | config.fast_players_count = 3 29 | 30 | config.kick_strength = 20 31 | config.kick_again_delay = 1 32 | config.distance_control_ball = 30 33 | config.deceleration_coef = 0.95 34 | 35 | config.average_lower_error = 0.1 36 | config.average_upper_error = 0.15 37 | config.average_speed = 3.5 38 | 39 | config.fast_lower_error = 0.1 40 | config.fast_upper_error = 0.15 41 | config.fast_speed = 4 42 | 43 | config.captain_error = 0.05 44 | config.captain_speed = 4.5 45 | 46 | config.game_time = 120 47 | config.debug_output = true 48 | config.record_game = false 49 | config.record_last_kicker = false 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/rubygoal/coordinate.rb: -------------------------------------------------------------------------------- 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 34 | -------------------------------------------------------------------------------- /lib/rubygoal/field.rb: -------------------------------------------------------------------------------- 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 135 | -------------------------------------------------------------------------------- /lib/rubygoal/formation.rb: -------------------------------------------------------------------------------- 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 74 | -------------------------------------------------------------------------------- /lib/rubygoal/formation/formation_dsl.rb: -------------------------------------------------------------------------------- 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 68 | -------------------------------------------------------------------------------- /lib/rubygoal/game.rb: -------------------------------------------------------------------------------- 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 163 | -------------------------------------------------------------------------------- /lib/rubygoal/goal.rb: -------------------------------------------------------------------------------- 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 27 | -------------------------------------------------------------------------------- /lib/rubygoal/match_data.rb: -------------------------------------------------------------------------------- 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 131 | -------------------------------------------------------------------------------- /lib/rubygoal/moveable.rb: -------------------------------------------------------------------------------- 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 | attr_accessor :destination 59 | 60 | private 61 | 62 | attr_reader :speed 63 | 64 | def reset_rotation 65 | self.rotation = 0 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/rubygoal/player.rb: -------------------------------------------------------------------------------- 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, :name 15 | attr_accessor :coach_defined_position 16 | 17 | def initialize(game, side, name) 18 | super() 19 | 20 | @time_to_kick_again = 0 21 | @side = side 22 | @player_movement = PlayerMovement.new(game, self) 23 | @name = name 24 | end 25 | 26 | def can_kick?(ball) 27 | !waiting_to_kick_again? && control_ball?(ball) 28 | end 29 | 30 | def kick(ball, target) 31 | direction = random_direction(target) 32 | strength = random_strength 33 | 34 | ball.move(direction, strength, name: name, side: side) 35 | reset_waiting_to_kick! 36 | end 37 | 38 | def move_to_coach_position 39 | move_to(coach_defined_position) 40 | end 41 | 42 | def update(elapsed_time) 43 | update_waiting_to_kick(elapsed_time) 44 | player_movement.update(elapsed_time) if moving? 45 | 46 | super 47 | end 48 | 49 | protected 50 | 51 | attr_accessor :time_to_kick_again, :player_movement 52 | 53 | private 54 | 55 | attr_reader :error 56 | 57 | def waiting_to_kick_again? 58 | time_to_kick_again > 0 59 | end 60 | 61 | def reset_waiting_to_kick! 62 | self.time_to_kick_again = Rubygoal.configuration.kick_again_delay 63 | end 64 | 65 | def update_waiting_to_kick(time_elapsed) 66 | self.time_to_kick_again -= time_elapsed if waiting_to_kick_again? 67 | end 68 | 69 | def control_ball?(ball) 70 | distance(ball.position) < Rubygoal.configuration.distance_control_ball 71 | end 72 | 73 | def random_strength 74 | error_range = (1 - error)..(1 + error) 75 | error_coef = Random.rand(error_range) 76 | Rubygoal.configuration.kick_strength * error_coef 77 | end 78 | 79 | def random_direction(target) 80 | direction = Util.angle(position.x, position.y, target.x, target.y) 81 | 82 | max_angle_error = STRAIGHT_ANGLE * error 83 | angle_error_range = -max_angle_error..max_angle_error 84 | 85 | direction += Random.rand(angle_error_range) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/rubygoal/players/average.rb: -------------------------------------------------------------------------------- 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 17 | -------------------------------------------------------------------------------- /lib/rubygoal/players/captain.rb: -------------------------------------------------------------------------------- 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 16 | -------------------------------------------------------------------------------- /lib/rubygoal/players/fast.rb: -------------------------------------------------------------------------------- 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 17 | -------------------------------------------------------------------------------- /lib/rubygoal/players/goalkeeper.rb: -------------------------------------------------------------------------------- 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 27 | -------------------------------------------------------------------------------- /lib/rubygoal/players/player_movement.rb: -------------------------------------------------------------------------------- 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 99 | -------------------------------------------------------------------------------- /lib/rubygoal/recorder.rb: -------------------------------------------------------------------------------- 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: [@game.score_home, @game.score_away], 21 | frames: @frames 22 | } 23 | end 24 | 25 | private 26 | 27 | attr_reader :game, :frames 28 | 29 | def frame_info 30 | frame = { 31 | time: @game.time.round(0), 32 | score: [@game.score_home, @game.score_away], 33 | ball: [ 34 | @game.ball.position.x.round(0), 35 | @game.ball.position.y.round(0) 36 | ], 37 | home: team_info(@game.team_home), 38 | away: team_info(@game.team_away) 39 | } 40 | last_kicker_info(frame) if Rubygoal.configuration.record_last_kicker 41 | 42 | frame 43 | end 44 | 45 | def team_info(team) 46 | team.players.map do |_, player| 47 | [ 48 | player.position.x.round(0), 49 | player.position.y.round(0), 50 | player.rotation.round(0), 51 | player.type[0] 52 | ] 53 | end 54 | end 55 | 56 | def last_kicker_info(frame) 57 | if @game.ball.last_kicker 58 | frame[:last_kicker] = [ 59 | @game.ball.last_kicker[:name], 60 | @game.ball.last_kicker[:side] 61 | ] 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/rubygoal/simulator.rb: -------------------------------------------------------------------------------- 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 34 | -------------------------------------------------------------------------------- /lib/rubygoal/team.rb: -------------------------------------------------------------------------------- 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, :goalkeeper) } 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, @coach.captain_player.name) 112 | 113 | @coach.players_by_type(:fast).each do |player_def| 114 | @players[player_def.name] = FastPlayer.new(game, side, player_def.name) 115 | end 116 | 117 | @coach.players_by_type(:average).each do |player_def| 118 | @players[player_def.name] = AveragePlayer.new(game, side, player_def.name) 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 | next unless 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 | 151 | nearest_teammate 152 | end 153 | 154 | def shoot_target 155 | # Do not kick always to the center, look for the sides of the goal 156 | limit = Field::GOAL_HEIGHT / 2 157 | offset = Random.rand(-limit..limit) 158 | 159 | target = Field.goal_position(opponent_side) 160 | target.y += offset 161 | target 162 | end 163 | 164 | def initialize_player_positions 165 | Field.default_player_field_positions.each_with_index do |pos, index| 166 | players.values[index].position = lineup_to_position(pos) 167 | players.values[index].coach_defined_position = lineup_to_position(pos) 168 | end 169 | end 170 | 171 | def update_coach_defined_positions(formation) 172 | formation.players_position.each do |player_name, pos| 173 | players[player_name].coach_defined_position = lineup_to_position(pos) 174 | end 175 | end 176 | 177 | def restart_player_positions_in_own_field(formation) 178 | formation.players_position.each do |player_name, pos| 179 | pos.x *= 0.5 180 | pos = lineup_to_position(pos) 181 | 182 | player = players[player_name] 183 | 184 | player.coach_defined_position = pos 185 | player.position = pos 186 | end 187 | end 188 | 189 | def lineup_to_position(field_position) 190 | Field.absolute_position(field_position, side) 191 | end 192 | 193 | def goalkeeper 194 | players[:goalkeeper] 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /lib/rubygoal/teams/away.rb: -------------------------------------------------------------------------------- 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 16 | -------------------------------------------------------------------------------- /lib/rubygoal/teams/home.rb: -------------------------------------------------------------------------------- 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 16 | -------------------------------------------------------------------------------- /lib/rubygoal/util.rb: -------------------------------------------------------------------------------- 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 37 | -------------------------------------------------------------------------------- /lib/rubygoal/version.rb: -------------------------------------------------------------------------------- 1 | module Rubygoal 2 | VERSION = Gem::Version.new '1.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /rubygoal-core.gemspec: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../lib", __FILE__) 2 | require 'rubygoal/version' 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ['Jorge Bejar'] 6 | gem.email = ['jorge@wyeworks.com'] 7 | gem.description = %q{Rubygoal core} 8 | gem.summary = %q{Rubygoal core} 9 | gem.homepage = 'https://github.com/wyeworks/rubygoal' 10 | gem.license = 'Apache License 2.0' 11 | 12 | gem.files = Dir['README.md', 'LICENSE', 'bin/**/*', 'lib/**/*', 'test/**/*'] 13 | gem.test_files = Dir['test/**/*'] 14 | 15 | gem.name = 'rubygoal-core' 16 | gem.require_paths = ['lib'] 17 | gem.version = Rubygoal::VERSION.to_s 18 | 19 | gem.required_ruby_version = '>= 2.2.2' 20 | 21 | gem.add_development_dependency 'rake' 22 | gem.add_development_dependency 'timecop' 23 | gem.add_development_dependency 'minitest-reporters' 24 | end 25 | -------------------------------------------------------------------------------- /test/ball_test.rb: -------------------------------------------------------------------------------- 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 57 | -------------------------------------------------------------------------------- /test/coach_test.rb: -------------------------------------------------------------------------------- 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 50 | -------------------------------------------------------------------------------- /test/field_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require 'rubygoal/field' 4 | 5 | module Rubygoal 6 | class FieldTest < Minitest::Test 7 | def test_center_position 8 | assert_equal Position.new(959, 581), Field.center_position 9 | end 10 | 11 | def test_home_goal_position 12 | assert_equal Position.new(262, 581), Field.goal_position(:home) 13 | end 14 | 15 | def test_away_goal_position 16 | assert_equal Position.new(1656, 581), Field.goal_position(:away) 17 | end 18 | 19 | def test_home_position_side 20 | assert_equal :home, Field.position_side(Position.new(958, 0)) 21 | end 22 | 23 | def test_away_position_side 24 | assert_equal :away, Field.position_side(Position.new(960, 0)) 25 | end 26 | 27 | def test_left_out_of_bounds_width 28 | assert Field.out_of_bounds_width?(Position.new(261, 0)) 29 | end 30 | 31 | def test_inside_left_bounds_width 32 | refute Field.out_of_bounds_width?(Position.new(262, 0)) 33 | end 34 | 35 | def test_inside_right_bounds_width 36 | refute Field.out_of_bounds_width?(Position.new(1656, 0)) 37 | end 38 | 39 | def test_right_out_of_bounds_width 40 | assert Field.out_of_bounds_width?(Position.new(1657, 0)) 41 | end 42 | 43 | def test_up_out_of_bounds_height 44 | assert Field.out_of_bounds_height?(Position.new(0, 111)) 45 | end 46 | 47 | def test_inside_upper_bounds_height 48 | refute Field.out_of_bounds_height?(Position.new(0, 112)) 49 | end 50 | 51 | def test_inside_bottom_bounds_height 52 | refute Field.out_of_bounds_height?(Position.new(0, 1050)) 53 | end 54 | 55 | def test_down_out_of_bounds_width 56 | assert Field.out_of_bounds_height?(Position.new(0, 1051)) 57 | end 58 | 59 | def test_middle_goal 60 | assert Field.goal?(Position.new(261, 581)) 61 | end 62 | 63 | def test_upper_goal 64 | assert Field.goal?(Position.new(261, 718)) 65 | end 66 | 67 | def test_bottom_goal 68 | assert Field.goal?(Position.new(261, 444)) 69 | end 70 | 71 | def test_upper_missed_goal 72 | refute Field.goal?(Position.new(261, 719)) 73 | end 74 | 75 | def test_bottom_missed_goal 76 | refute Field.goal?(Position.new(261, 443)) 77 | end 78 | 79 | def test_close_to_goal_straight 80 | assert Field.close_to_goal?(Position.new(536, 581), :home) 81 | end 82 | 83 | def test_not_close_to_goal_straight 84 | refute Field.close_to_goal?(Position.new(537, 581), :home) 85 | end 86 | 87 | def test_close_to_goal_upper_diagonal 88 | assert Field.close_to_goal?(Position.new(456, 775), :home) 89 | end 90 | 91 | def test_not_close_to_goal_upper_diagonal 92 | refute Field.close_to_goal?(Position.new(457, 776), :home) 93 | end 94 | 95 | def test_close_to_goal_lower_diagonal 96 | assert Field.close_to_goal?(Position.new(456, 387), :home) 97 | end 98 | 99 | def test_not_close_to_goal_lower_diagonal 100 | refute Field.close_to_goal?(Position.new(457, 386), :home) 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/fixtures/four_fast_players_coach_definition.rb: -------------------------------------------------------------------------------- 1 | module Rubygoal 2 | class FourFastPlayersCoachDefinition < CoachDefinition 3 | team do 4 | name "FourFastPlayers" 5 | 6 | players do 7 | captain :captain 8 | 9 | fast :fast1 10 | fast :fast2 11 | fast :fast3 12 | fast :fast4 13 | 14 | average :average1 15 | average :average2 16 | average :average3 17 | average :average4 18 | average :average5 19 | average :average6 20 | end 21 | end 22 | 23 | def formation(match) 24 | Formation.new 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/fixtures/less_players_coach_definition.rb: -------------------------------------------------------------------------------- 1 | module Rubygoal 2 | class LessPlayersCoachDefnition < CoachDefinition 3 | team do 4 | name "LessPlayers" 5 | 6 | players do 7 | captain :captain 8 | 9 | fast :fast1 10 | fast :fast2 11 | fast :fast3 12 | 13 | average :average2 14 | average :average3 15 | average :average4 16 | average :average5 17 | average :average6 18 | end 19 | end 20 | 21 | def formation(match) 22 | Formation.new 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/fixtures/mirror_strategy_coach_definition.rb: -------------------------------------------------------------------------------- 1 | module Rubygoal 2 | class MirrorStrategyCoachDefinition < CoachDefinition 3 | team do 4 | name "Mimic Team" 5 | 6 | players do 7 | average :average1 8 | average :average2 9 | average :average3 10 | average :average4 11 | average :average5 12 | average :average6 13 | 14 | fast :fast1 15 | fast :fast2 16 | fast :fast3 17 | 18 | captain :captain 19 | end 20 | end 21 | 22 | def formation(match) 23 | # Mirror opponent players 24 | 25 | formation = Formation.new 26 | 27 | opponent = match.other.positions 28 | my_players = players 29 | 30 | opponent.each_with_index do |(_, opponent_pos), index| 31 | formation.lineup do 32 | custom_position do 33 | player my_players[index].name 34 | position opponent_pos.x, 100.0 - opponent_pos.y 35 | end 36 | end 37 | end 38 | 39 | formation 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/fixtures/more_players_coach_definition.rb: -------------------------------------------------------------------------------- 1 | module Rubygoal 2 | class MorePlayersCoachDefinition < CoachDefinition 3 | team do 4 | name "MorePlayers" 5 | 6 | players do 7 | captain :captain 8 | 9 | fast :fast1 10 | fast :fast2 11 | fast :fast3 12 | 13 | average :average1 14 | average :average2 15 | average :average3 16 | average :average4 17 | average :average5 18 | average :average6 19 | average :average7 20 | end 21 | end 22 | 23 | def formation(match) 24 | Formation.new 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/fixtures/test_away_coach_definition.rb: -------------------------------------------------------------------------------- 1 | module Rubygoal 2 | class TestAwayCoachDefinition < CoachDefinition 3 | team do 4 | name "Test Away Team" 5 | 6 | players do 7 | captain :away_captain 8 | 9 | fast :away_fast1 10 | fast :away_fast2 11 | fast :away_fast3 12 | 13 | average :away_average1 14 | average :away_average2 15 | average :away_average3 16 | average :away_average4 17 | average :away_average5 18 | average :away_average6 19 | end 20 | end 21 | 22 | def formation(match) 23 | formation = Formation.new 24 | 25 | formation.lineup do 26 | defenders :away_average1, :away_fast1, :none, :away_fast3, :away_average5 27 | midfielders :away_average2, :away_average3, :away_captain, :away_average4, :away_average6 28 | attackers :away_fast2 29 | end 30 | 31 | formation 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/fixtures/test_home_coach_definition.rb: -------------------------------------------------------------------------------- 1 | module Rubygoal 2 | class TestHomeCoachDefinition < CoachDefinition 3 | team do 4 | name "Test Home Team" 5 | 6 | players do 7 | captain :home_captain 8 | 9 | fast :home_fast1 10 | fast :home_fast2 11 | fast :home_fast3 12 | 13 | average :home_average1 14 | average :home_average2 15 | average :home_average3 16 | average :home_average4 17 | average :home_average5 18 | average :home_average6 19 | end 20 | end 21 | 22 | def formation(match) 23 | formation = Formation.new 24 | 25 | formation.lineup do 26 | defenders :home_average1, :home_fast1, :none, :home_fast3, :home_average5 27 | midfielders :home_average2, :none, :home_captain, :none, :home_average6 28 | attackers :none, :home_average3, :home_fast2, :home_average4, :none 29 | end 30 | 31 | formation 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/fixtures/two_captains_coach_definition.rb: -------------------------------------------------------------------------------- 1 | module Rubygoal 2 | class TwoCaptainsCoachDefinition < CoachDefinition 3 | team do 4 | name "TwoCaptains" 5 | 6 | players do 7 | captain :captain 8 | captain :captain2 9 | 10 | fast :fast1 11 | fast :fast2 12 | fast :fast3 13 | 14 | average :average1 15 | average :average2 16 | average :average3 17 | average :average4 18 | average :average5 19 | average :average6 20 | end 21 | end 22 | 23 | def formation(match) 24 | Formation.new 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/fixtures/valid_coach_definition.rb: -------------------------------------------------------------------------------- 1 | module Rubygoal 2 | class ValidCoachDefinition < CoachDefinition 3 | team do 4 | name "ValidTeam" 5 | 6 | players do 7 | captain :captain 8 | 9 | fast :fast1 10 | fast :fast2 11 | fast :fast3 12 | 13 | average :average1 14 | average :average2 15 | average :average3 16 | average :average4 17 | average :average5 18 | average :average6 19 | end 20 | end 21 | 22 | def formation(match) 23 | Formation.new 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/formation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require 'rubygoal/formation' 4 | 5 | module Rubygoal 6 | class TestFormation < Minitest::Test 7 | def setup 8 | @formation = Formation.new 9 | @formation.lineup do 10 | defenders :average1, :average2, :none, :average3, :average4 11 | midfielders :average5, :fast1, :none, :fast2, :average6 12 | attackers :none, :captain, :none, :fast3, :none 13 | end 14 | end 15 | 16 | def test_default_formation_is_valid 17 | assert @formation.valid? 18 | assert_empty @formation.errors 19 | end 20 | 21 | def test_player_default_positions 22 | expected_positions = { 23 | average1: Position.new(232, 156), 24 | average2: Position.new(232, 312), 25 | average3: Position.new(232, 625), 26 | average4: Position.new(232, 782), 27 | average5: Position.new(697, 156), 28 | average6: Position.new(697, 782), 29 | captain: Position.new(1162, 312), 30 | fast1: Position.new(697, 312), 31 | fast2: Position.new(697, 625), 32 | fast3: Position.new(1162, 625) 33 | } 34 | 35 | expected_positions.each do |name, pos| 36 | assert_in_delta pos, @formation.players_position[name], 1 37 | end 38 | end 39 | 40 | def test_custom_default_positions 41 | @formation.lineup do 42 | lines do 43 | defenders 13 44 | midfielders 43 45 | attackers 65 46 | end 47 | 48 | defenders :average1, :average2, :none, :average3, :average4 49 | midfielders :none, :fast1, :none, :fast2, :average6 50 | attackers :none, :captain, :none, :none, :none 51 | 52 | custom_position do 53 | player :fast3 54 | position 30, 10 55 | end 56 | custom_position do 57 | player :average5 58 | position 60, 50 59 | end 60 | end 61 | 62 | expected_positions = { 63 | average1: Position.new(181, 156), 64 | average2: Position.new(181, 312), 65 | average3: Position.new(181, 625), 66 | average4: Position.new(181, 781), 67 | average6: Position.new(600, 781), 68 | captain: Position.new(906, 312), 69 | fast1: Position.new(600, 312), 70 | fast2: Position.new(600, 625), 71 | 72 | fast3: Position.new(418, 94), 73 | average5: Position.new(836, 469) 74 | } 75 | 76 | expected_positions.each do |name, pos| 77 | assert_in_delta pos, @formation.players_position[name], 1 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/game_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'timecop' 3 | 4 | module Rubygoal 5 | class GameTest < Minitest::Test 6 | def setup 7 | home_coach = Coach.new(TestHomeCoachDefinition.new) 8 | away_coach = Coach.new(TestAwayCoachDefinition.new) 9 | @game = Game.new(home_coach, away_coach) 10 | end 11 | 12 | def test_initial_score 13 | assert_equal 0, @game.score_home 14 | assert_equal 0, @game.score_away 15 | end 16 | 17 | def test_score_after_home_team_goal 18 | goal_position = Field::goal_position(:away).add(Position.new(1, 0)) 19 | @game.ball.position = goal_position 20 | 21 | @game.update 22 | 23 | assert_equal 1, @game.score_home 24 | end 25 | 26 | def test_score_after_away_team_goal 27 | goal_position = Field::goal_position(:home).add(Position.new(-1, 0)) 28 | @game.ball.position = goal_position 29 | 30 | @game.update 31 | 32 | assert_equal 1, @game.score_away 33 | end 34 | 35 | def test_celebrating_goal_after_init 36 | refute @game.celebrating_goal? 37 | end 38 | 39 | def test_celebrating_goal_after_away_team_goal 40 | goal_position = Field::goal_position(:home).add(Position.new(-1, 0)) 41 | @game.ball.position = goal_position 42 | 43 | @game.update 44 | 45 | assert @game.celebrating_goal? 46 | end 47 | 48 | def test_initial_time 49 | assert_equal Rubygoal.configuration.game_time, @game.time 50 | end 51 | 52 | def test_time_pass 53 | # Begin game with an initial update call 54 | @game.update 55 | 56 | # Simulate time passing 57 | elapsed_time = 70 58 | Timecop.travel(Time.now + elapsed_time) 59 | 60 | @game.update 61 | 62 | remaining_time = Rubygoal.configuration.game_time - elapsed_time 63 | assert_in_delta remaining_time, @game.time, 0.05 64 | end 65 | 66 | def test_players 67 | assert_equal 22, @game.players.size 68 | end 69 | 70 | def test_recorded_game 71 | Rubygoal.configuration.record_game = true 72 | home_coach = Coach.new(TestHomeCoachDefinition.new) 73 | away_coach = Coach.new(TestAwayCoachDefinition.new) 74 | game = Game.new(home_coach, away_coach) 75 | 76 | expected_teams = { 77 | home: 'Test Home Team', 78 | away: 'Test Away Team' 79 | } 80 | 81 | assert_equal expected_teams, game.recorded_game[:teams] 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/match_data_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Rubygoal 4 | class MatchDataTest < Minitest::Test 5 | def test_home_team_is_winning 6 | match_data = create_match_data( 7 | :home, 8 | score_home: 2, 9 | score_away: 0 10 | ) 11 | 12 | assert match_data.me.winning? 13 | assert match_data.other.losing? 14 | end 15 | 16 | def test_home_team_is_losing 17 | match_data = create_match_data( 18 | :home, 19 | score_home: 1, 20 | score_away: 3 21 | ) 22 | 23 | assert match_data.me.losing? 24 | assert match_data.other.winning? 25 | end 26 | 27 | def test_away_team_is_winning 28 | match_data = create_match_data( 29 | :away, 30 | score_home: 0, 31 | score_away: 2 32 | ) 33 | 34 | assert match_data.me.winning? 35 | assert match_data.other.losing? 36 | end 37 | 38 | def test_away_team_is_losing 39 | match_data = create_match_data( 40 | :away, 41 | score_home: 3, 42 | score_away: 1 43 | ) 44 | 45 | assert match_data.me.losing? 46 | assert match_data.other.winning? 47 | end 48 | 49 | def test_home_team_is_a_draw 50 | match_data = create_match_data( 51 | :home, 52 | score_home: 1, 53 | score_away: 1 54 | ) 55 | 56 | assert match_data.me.draw? 57 | assert match_data.other.draw? 58 | end 59 | 60 | def test_away_team_is_a_draw 61 | match_data = create_match_data( 62 | :away, 63 | score_home: 1, 64 | score_away: 1 65 | ) 66 | 67 | assert match_data.me.draw? 68 | assert match_data.other.draw? 69 | end 70 | 71 | def test_match_info_includes_time 72 | match_data = create_match_data( 73 | :away, 74 | time: 100 75 | ) 76 | 77 | assert_equal 100, match_data.time 78 | end 79 | 80 | def test_match_info_includes_player_positions 81 | match_data = create_match_data( 82 | :away, 83 | home_players_positions: { 84 | name: Position.new(Field::WIDTH, Field::HEIGHT) 85 | }, 86 | away_players_positions: { 87 | name: Position.new(Field::WIDTH / 2, 0) 88 | } 89 | ) 90 | 91 | assert_equal({ name: Position.new(50, 0) }, match_data.me.positions) 92 | assert_equal({ name: Position.new(100, 100) }, match_data.other.positions) 93 | end 94 | 95 | def test_match_info_exclude_goalkeeper_position 96 | match_data = create_match_data( 97 | :home, 98 | home_players_positions: { 99 | goalkeeper: Position.new(Field::WIDTH, Field::HEIGHT), 100 | name: Position.new(Field::WIDTH, Field::HEIGHT) 101 | }, 102 | ) 103 | 104 | assert_equal({ name: Position.new(100, 100) }, match_data.me.positions) 105 | end 106 | 107 | def test_home_match_info_includes_ball_position 108 | match_data = create_match_data( 109 | :home, 110 | ball_position: Field.absolute_position( 111 | Position.new(Field::WIDTH / 4, Field::HEIGHT / 2), 112 | :home 113 | ) 114 | ) 115 | 116 | assert_equal Position.new(25, 50), match_data.ball 117 | end 118 | 119 | def test_away_match_info_includes_ball_position 120 | match_data = create_match_data( 121 | :away, 122 | ball_position: Field.absolute_position( 123 | Position.new(Field::WIDTH / 4, Field::HEIGHT / 2), 124 | :away 125 | ) 126 | ) 127 | 128 | assert_equal Position.new(25, 50), match_data.ball 129 | end 130 | 131 | private 132 | 133 | def create_match_data(side, game_options) 134 | MatchData::Factory.new(game_test_double(game_options), side) 135 | .create 136 | end 137 | 138 | def game_test_double(game_options) 139 | OpenStruct.new({ 140 | time: 0, 141 | score_home: 0, 142 | score_away: 0, 143 | ball_position: Field.center_position, 144 | home_players_positions: {}, 145 | away_players_positions: {} 146 | }.merge(game_options)) 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /test/player_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Rubygoal 4 | class PlayerTest < Minitest::Test 5 | def setup 6 | home_coach = Coach.new(TestHomeCoachDefinition.new) 7 | away_coach = Coach.new(TestAwayCoachDefinition.new) 8 | @game = Game.new(home_coach, away_coach) 9 | 10 | @player = game.players.first 11 | @player.send(:time_to_kick_again=, 0) 12 | end 13 | 14 | def test_player_can_kick_the_ball_in_the_same_position 15 | position = Position.new(100, 100) 16 | game.ball.position = position 17 | player.position = position 18 | 19 | assert player.can_kick?(game.ball) 20 | end 21 | 22 | def test_player_can_kick_the_ball_when_is_close 23 | game.ball.position = Position.new(100, 100) 24 | player.position = Position.new(110, 115) 25 | 26 | assert player.can_kick?(game.ball) 27 | end 28 | 29 | def test_player_can_not_kick_the_ball_when_is_far 30 | game.ball.position = Position.new(100, 100) 31 | player.position = Position.new(200, 200) 32 | 33 | refute player.can_kick?(game.ball) 34 | end 35 | 36 | def test_player_can_not_kick_the_ball_again 37 | position = Position.new(100, 100) 38 | game.ball.position = position 39 | player.position = position 40 | 41 | player.kick(game.ball, Position.new(300, 300)) 42 | 43 | refute player.can_kick?(game.ball) 44 | end 45 | 46 | def test_player_can_kick_the_ball_again_after_time 47 | position = Position.new(100, 100) 48 | game.ball.position = position 49 | player.position = position 50 | 51 | player.kick(game.ball, Position.new(300, 300)) 52 | player.update(Rubygoal.configuration.kick_again_delay) 53 | 54 | assert player.can_kick?(game.ball) 55 | end 56 | 57 | def test_kick_the_ball_to_a_different_place 58 | position = Position.new(100, 100) 59 | game.ball.position = position 60 | game.ball.velocity = Velocity.new(0, 0) 61 | player.position = position 62 | 63 | player.kick(game.ball, Position.new(300, 300)) 64 | 65 | refute_equal Velocity.new(0, 0), game.ball.velocity 66 | end 67 | 68 | def test_kick_direction_range_right 69 | # Set little error: < 2 degrees (180 * 0.01 < 2) 70 | player.instance_variable_set(:@error, 0.01) 71 | 72 | position = Position.new(100, 100) 73 | game.ball.position = position 74 | game.ball.velocity = Velocity.new(0, 0) 75 | player.position = position 76 | 77 | # 0 degree kick 78 | player.kick(game.ball, Position.new(200, 100)) 79 | 80 | velocity = game.ball.velocity 81 | velocity_angle = Util.angle(0, 0, velocity.x, velocity.y) 82 | 83 | assert_in_delta 0, velocity_angle, 2 84 | end 85 | 86 | def test_kick_direction_range_left 87 | # Set little error: < 2 degrees (180 * 0.01 < 2) 88 | player.instance_variable_set(:@error, 0.01) 89 | 90 | position = Position.new(100, 100) 91 | game.ball.position = position 92 | game.ball.velocity = Velocity.new(0, 0) 93 | player.position = position 94 | 95 | # 180 degree kick 96 | player.kick(game.ball, Position.new(0, 100)) 97 | 98 | velocity = game.ball.velocity 99 | velocity_angle = Util.positive_angle(0, 0, velocity.x, velocity.y) 100 | 101 | assert_in_delta 180, velocity_angle, 2 102 | end 103 | 104 | def test_kick_strength 105 | # Set little error: distance error = 1 (20 * 0.05 = 1) 106 | player.instance_variable_set(:@error, 0.05) 107 | 108 | position = Position.new(100, 100) 109 | game.ball.position = position 110 | game.ball.velocity = Velocity.new(0, 0) 111 | player.position = position 112 | 113 | player.kick(game.ball, Position.new(200, 200)) 114 | 115 | velocity = game.ball.velocity 116 | velocity_strength = Util.distance(0, 0, velocity.x, velocity.y) 117 | 118 | assert_in_delta 20, velocity_strength, 1 119 | end 120 | 121 | def test_kicker_registered 122 | assert_nil game.ball.last_kicker 123 | 124 | position = Position.new(100, 100) 125 | game.ball.position = position 126 | game.ball.velocity = Velocity.new(0, 0) 127 | player.position = position 128 | 129 | player.kick(game.ball, Position.new(300, 300)) 130 | 131 | assert_equal player.name, game.ball.last_kicker[:name] 132 | assert_equal player.side, game.ball.last_kicker[:side] 133 | end 134 | 135 | private 136 | 137 | attr_reader :game, :player 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /test/players/goalkeeper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Rubygoal 4 | class GaolKeeperPlayerTest < Minitest::Test 5 | def setup 6 | home_coach = Coach.new(TestHomeCoachDefinition.new) 7 | away_coach = Coach.new(TestAwayCoachDefinition.new) 8 | @game = Game.new(home_coach, away_coach) 9 | 10 | @player = game.players.first 11 | end 12 | 13 | def test_goalkeeper_is_already_covering_goal 14 | goal_pos = Field.goal_position(:home) 15 | game.ball.position = Position.new(300, goal_pos.y) 16 | 17 | player.move_to_cover_goal(game.ball) 18 | player.update(elapsed_time) 19 | 20 | assert_equal Velocity.new(0, 0), player.velocity 21 | end 22 | 23 | def test_goalkeeper_moves_down_to_cover_goal 24 | game.ball.position = Position.new(300, 300) 25 | 26 | player.move_to_cover_goal(game.ball) 27 | player.update(elapsed_time) 28 | 29 | assert_in_delta Velocity.new(0, -3.5), player.velocity, 0.001 30 | end 31 | 32 | def test_goalkeeper_moves_up_to_cover_goal 33 | game.ball.position = Position.new(300, 900) 34 | 35 | player.move_to_cover_goal(game.ball) 36 | player.update(elapsed_time) 37 | 38 | assert_in_delta Velocity.new(0, 3.5), player.velocity, 0.001 39 | end 40 | 41 | private 42 | 43 | attr_reader :game, :player 44 | 45 | def elapsed_time 46 | 1 / 60.0 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/players/player_movement_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Rubygoal 4 | class PlayerMovementTest < Minitest::Test 5 | def setup 6 | home_coach = Coach.new(TestHomeCoachDefinition.new) 7 | away_coach = Coach.new(TestAwayCoachDefinition.new) 8 | game = Game.new(home_coach, away_coach) 9 | home_team = game.team_home 10 | 11 | @player = home_team.players.values[0] 12 | @other_player = home_team.players.values[1] 13 | 14 | @player_movement = PlayerMovement.new(game, player) 15 | end 16 | 17 | def test_do_not_modify_velocity_if_not_blocker 18 | player.position = Position.new(100, 0) 19 | other_player.position = Position.new(170, 0) 20 | 21 | player.move_to(Position.new(500, 0)) 22 | player_movement.update(elapsed_time) 23 | 24 | assert_equal Velocity.new(3.5, 0), player.velocity 25 | end 26 | 27 | def test_modify_velocity_if_there_is_a_blocker_very_close 28 | player.position = Position.new(100, 0) 29 | other_player.position = Position.new(149, 0) 30 | 31 | player.move_to(Position.new(500, 0)) 32 | player_movement.update(elapsed_time) 33 | 34 | assert_in_delta Velocity.new(2.5, -2.5), player.velocity, 0.1 35 | end 36 | 37 | def test_decelerate_if_there_is_a_blocker_a_bit_close 38 | player.position = Position.new(100, 0) 39 | other_player.position = Position.new(169, 0) 40 | 41 | player.move_to(Position.new(500, 0)) 42 | player_movement.update(elapsed_time) 43 | 44 | assert_in_delta Velocity.new(3.1, 0), player.velocity, 0.1 45 | end 46 | 47 | def test_stop_if_close_to_destination_and_there_is_blocker 48 | player.position = Position.new(100, 0) 49 | other_player.position = Position.new(149, 0) 50 | 51 | player.move_to(Position.new(160, 0)) 52 | player_movement.update(elapsed_time) 53 | 54 | assert_equal Velocity.new(0, 0), player.velocity 55 | end 56 | 57 | def test_stop_if_blocker_is_ver_close_and_moving 58 | player.position = Position.new(100, 0) 59 | other_player.position = Position.new(150, 0) 60 | 61 | player.move_to(Position.new(500, 0)) 62 | other_player.move_to(Position.new(150, 500)) 63 | player_movement.update(elapsed_time) 64 | 65 | assert_equal Velocity.new(0, 0), player.velocity 66 | end 67 | 68 | private 69 | 70 | attr_reader :player_movement, :player, :other_player 71 | 72 | def elapsed_time 73 | 1 / 60.0 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/recorder_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'timecop' 3 | 4 | require 'rubygoal/recorder' 5 | 6 | module Rubygoal 7 | class RecorderTest < Minitest::Test 8 | def setup 9 | Rubygoal.configuration.record_game = true 10 | 11 | home_coach = Coach.new(TestHomeCoachDefinition.new) 12 | away_coach = Coach.new(TestAwayCoachDefinition.new) 13 | @game = Game.new(home_coach, away_coach) 14 | @recorder = @game.recorder 15 | end 16 | 17 | def test_recorded_team_names 18 | expected_teams = { 19 | home: 'Test Home Team', 20 | away: 'Test Away Team' 21 | } 22 | 23 | assert_equal expected_teams, recorder.to_hash[:teams] 24 | end 25 | 26 | def test_recorded_score 27 | expected_score = [0, 0] 28 | 29 | assert_equal expected_score, recorder.to_hash[:score] 30 | end 31 | 32 | def test_initial_recorded_frames 33 | assert_equal [], recorder.to_hash[:frames] 34 | end 35 | 36 | def test_recorded_frame_after_first_update 37 | @game.update 38 | 39 | ball_position = Field.center_position 40 | frames = recorder.to_hash[:frames] 41 | 42 | assert_equal 1, frames.count 43 | assert_in_delta 120, frames.first[:time], 0.001 44 | assert_equal( 45 | [0, 0], 46 | frames.first[:score] 47 | ) 48 | assert_equal( 49 | [ball_position.x, ball_position.y], 50 | frames.first[:ball] 51 | ) 52 | end 53 | 54 | def test_recorded_frame_with_11_values_for_player_info 55 | @game.update 56 | 57 | first_frame = recorder.to_hash[:frames].first 58 | home_players = first_frame[:home] 59 | away_players = first_frame[:away] 60 | 61 | assert_equal 11, home_players.count 62 | assert_equal 11, away_players.count 63 | end 64 | 65 | def test_recorded_frame_with_detailed_player_info 66 | @game.update 67 | 68 | first_frame = recorder.to_hash[:frames].first 69 | home_players = first_frame[:home] 70 | away_players = first_frame[:away] 71 | 72 | goalkeeper_field_pos = Position.new(50, Field::HEIGHT / 2) 73 | goalkeeper_pos_home = Field.absolute_position(goalkeeper_field_pos, :home) 74 | goalkeeper_pos_away = Field.absolute_position(goalkeeper_field_pos, :away) 75 | 76 | assert_equal( 77 | [ 78 | goalkeeper_pos_home.x, 79 | goalkeeper_pos_home.y, 80 | 0, 81 | "a" 82 | ], 83 | home_players.first 84 | ) 85 | assert_equal( 86 | [ 87 | goalkeeper_pos_away.x, 88 | goalkeeper_pos_away.y, 89 | 0, 90 | "a" 91 | ], 92 | away_players.first 93 | ) 94 | end 95 | 96 | def test_recorded_frame_some_updates 97 | time = Time.now 98 | 21.times do 99 | @game.update 100 | time += 0.25 101 | Timecop.travel(time) 102 | end 103 | 104 | frames = recorder.to_hash[:frames] 105 | 106 | assert_equal 21, frames.count 107 | assert_in_delta 120, frames.first[:time], 0.001 108 | assert_in_delta 115, frames.last[:time], 0.001 109 | end 110 | 111 | def test_recorded_frame_with_last_kick 112 | Rubygoal.configuration.record_last_kicker = true 113 | name, player = @game.team_home.players.first 114 | player.position = @game.ball.position 115 | player.kick(@game.ball, Position.new(100, 100)) 116 | @game.update 117 | 118 | last_frame = recorder.to_hash[:frames].last 119 | 120 | assert !last_frame[:last_kicker].nil? 121 | assert_equal name, last_frame[:last_kicker][0] 122 | assert_equal :home, last_frame[:last_kicker][1] 123 | end 124 | 125 | private 126 | 127 | attr_reader :game, :recorder 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /test/team_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'fixtures/mirror_strategy_coach_definition' 3 | 4 | module Rubygoal 5 | class TeamTest < Minitest::Test 6 | def setup 7 | home_coach = Coach.new(TestHomeCoachDefinition.new) 8 | away_coach = Coach.new(TestAwayCoachDefinition.new) 9 | game = Game.new(home_coach, away_coach) 10 | 11 | @home_team = game.team_home 12 | @away_team = game.team_away 13 | end 14 | 15 | def test_home_team_initial_positions_are_in_own_field 16 | expected_positions = { 17 | home_average1: Position.new(232 / 2, 156), 18 | home_average2: Position.new(697 / 2, 156), 19 | home_average3: Position.new(1162 / 2, 312), 20 | home_average4: Position.new(1162 / 2, 625), 21 | home_average5: Position.new(232 / 2, 782), 22 | home_average6: Position.new(697 / 2, 782), 23 | home_fast1: Position.new(232 / 2, 312), 24 | home_fast2: Position.new(1162 / 2, 469), 25 | home_fast3: Position.new(232 / 2, 625), 26 | home_captain: Position.new(697 / 2, 469) 27 | } 28 | 29 | expected_positions.each do |name, pos| 30 | assert_in_delta pos, @home_team.players_position[name], 1 31 | end 32 | end 33 | 34 | def test_away_team_initial_positions_are_in_own_field 35 | expected_positions = { 36 | away_average1: Position.new(232 / 2, 156), 37 | away_average2: Position.new(697 / 2, 156), 38 | away_average3: Position.new(697 / 2, 312), 39 | away_average4: Position.new(697 / 2, 625), 40 | away_average5: Position.new(232 / 2, 782), 41 | away_average6: Position.new(697 / 2, 782), 42 | away_fast1: Position.new(232 / 2, 312), 43 | away_fast2: Position.new(1162 / 2, 469), 44 | away_fast3: Position.new(232 / 2, 625), 45 | away_captain: Position.new(697 / 2, 469) 46 | } 47 | 48 | expected_positions.each do |name, pos| 49 | assert_in_delta pos, @away_team.players_position[name], 1 50 | end 51 | end 52 | 53 | def test_default_opponent_positions_for_initial_positions 54 | home_coach = Coach.new(MirrorStrategyCoachDefinition.new) 55 | away_coach = Coach.new(TestAwayCoachDefinition.new) 56 | game = Game.new(home_coach, away_coach) 57 | 58 | home_team = game.team_home 59 | 60 | expected_positions = { 61 | average1: Position.new(232 / 2, 750), 62 | average2: Position.new(232 / 2, 563), 63 | average3: Position.new(232 / 2, 375), 64 | average4: Position.new(232 / 2, 187), 65 | average5: Position.new(697 / 2, 750), 66 | average6: Position.new(697 / 2, 563), 67 | fast1: Position.new(697 / 2, 375), 68 | fast2: Position.new(697 / 2, 187), 69 | fast3: Position.new(1162 / 2, 625), 70 | captain: Position.new(1162 / 2, 312) 71 | } 72 | 73 | expected_positions.each do |name, pos| 74 | assert_in_delta pos, home_team.players_position[name], 1 75 | end 76 | end 77 | 78 | def test_home_goalkeeper_position 79 | goalkeeper = @home_team.players[:goalkeeper] 80 | 81 | assert_equal Position.new(312, 581), goalkeeper.position 82 | end 83 | 84 | def test_away_goalkeeper_position 85 | goalkeeper = @away_team.players[:goalkeeper] 86 | 87 | assert_equal Position.new(1606, 581), goalkeeper.position 88 | end 89 | 90 | private 91 | 92 | def players_positions(team) 93 | team.players_list.map(&:position) 94 | end 95 | end 96 | end 97 | 98 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require "minitest/reporters" 3 | 4 | require 'rubygoal/game' 5 | require 'fixtures/test_home_coach_definition' 6 | require 'fixtures/test_away_coach_definition' 7 | 8 | # Ensure backward compatibility with Minitest 4 9 | Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) 10 | Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new 11 | 12 | module Rubygoal 13 | class Coordinate 14 | # Needed to use assert_in_delta with Coordinate instances 15 | alias_method :-, :distance 16 | end 17 | end 18 | 19 | Rubygoal.configure do |config| 20 | config.debug_output = false 21 | end 22 | -------------------------------------------------------------------------------- /test_mirror.rb: -------------------------------------------------------------------------------- 1 | require 'rubygoal/coach' 2 | require 'rubygoal/formation' 3 | 4 | module Rubygoal 5 | class TestMirror < Coach 6 | 7 | def name 8 | "Racing" 9 | end 10 | 11 | def players 12 | { 13 | captain: [:capitan], 14 | fast: [:rapido1, :rapido2, :rapido3], 15 | average: [:average01, :average02, :average03, :average04, :average05, :average06] 16 | } 17 | end 18 | 19 | def formation(match) 20 | mirror_formation(match.other.lineup) 21 | end 22 | end 23 | end 24 | --------------------------------------------------------------------------------