├── .gitignore ├── .rspec ├── .rvmrc ├── .travis.yml ├── Capfile ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE ├── README.markdown ├── Rakefile ├── app ├── controllers │ ├── application_controller.rb │ ├── authentications_controller.rb │ ├── games_controller.rb │ ├── messages_controller.rb │ ├── moves_controller.rb │ ├── sessions_controller.rb │ └── users_controller.rb ├── helpers │ ├── application_helper.rb │ ├── authentications_helper.rb │ ├── error_messages_helper.rb │ ├── games_helper.rb │ ├── layout_helper.rb │ ├── messages_helper.rb │ ├── moves_helper.rb │ ├── sessions_helper.rb │ └── users_helper.rb ├── mailers │ └── notifications.rb ├── models │ ├── authentication.rb │ ├── game.rb │ ├── message.rb │ ├── profile.rb │ └── user.rb └── views │ ├── authentications │ └── index.html.erb │ ├── games │ ├── _form.html.erb │ ├── _game.html.erb │ ├── _other_games.html.erb │ ├── _pagination.html.erb │ ├── _panel.html.erb │ ├── _profile.html.erb │ ├── _side.html.erb │ ├── _your_games.html.erb │ ├── edit.js.erb │ ├── index.html.erb │ ├── new.html.erb │ ├── other.js.erb │ ├── resources.html.erb │ ├── show.html.erb │ ├── update.js.erb │ └── your.js.erb │ ├── layouts │ └── application.html.erb │ ├── messages │ ├── _message.html.erb │ └── create.js.erb │ ├── moves │ ├── _update_moves.js.erb │ ├── create.js.erb │ └── index.js.erb │ ├── notifications │ ├── invitation.text.erb │ ├── message.text.erb │ └── move.text.erb │ ├── sessions │ └── new.html.erb │ └── users │ ├── _form.html.erb │ ├── edit.html.erb │ ├── new.html.erb │ └── show.html.erb ├── autotest └── discover.rb ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.example.yml ├── deploy.rb ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── god.rb ├── initializers │ ├── backtrace_silencers.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── omniauth.rb │ ├── secret_token.rb │ ├── security_fix.rb │ └── session_store.rb ├── locales │ └── en.yml ├── private.example.yml ├── routes.rb └── schedule.rb ├── db ├── migrate │ ├── 20101016000701_create_games.rb │ ├── 20101016012116_create_users.rb │ ├── 20101016144720_add_guest_to_users.rb │ ├── 20101016203316_create_authentications.rb │ ├── 20101017234019_add_indexes.rb │ ├── 20101022221421_add_last_request_at_to_users.rb │ ├── 20101022230300_add_rank_to_users.rb │ ├── 20101023213811_remove_valid_positions_from_games.rb │ ├── 20101028222332_add_token_to_users.rb │ ├── 20101029233118_add_avatar_url_to_users.rb │ ├── 20101215010754_add_email_options_to_users.rb │ ├── 20101216234423_add_publicized_at_to_users.rb │ ├── 20110212173010_create_messages.rb │ ├── 20110212190716_add_time_zone_to_users.rb │ └── 20110212191244_add_email_on_message_to_users.rb └── seeds.rb ├── doc └── README_FOR_APP ├── lib ├── controller_authentication.rb ├── game_engine.rb ├── game_thumb.rb └── tasks │ ├── .gitkeep │ └── application.rake ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico ├── images │ ├── auth │ │ ├── facebook_128.png │ │ ├── facebook_32.png │ │ ├── facebook_64.png │ │ ├── open_id_128.png │ │ ├── open_id_32.png │ │ ├── open_id_64.png │ │ ├── twitter_128.png │ │ ├── twitter_32.png │ │ └── twitter_64.png │ ├── avatars │ │ ├── gnugo.png │ │ └── guest.png │ ├── game │ │ ├── 9 │ │ │ ├── black_stone.png │ │ │ ├── board.png │ │ │ ├── last_black_stone.png │ │ │ ├── last_white_stone.png │ │ │ └── white_stone.png │ │ ├── 13 │ │ │ ├── black_stone.png │ │ │ ├── board.png │ │ │ ├── last_black_stone.png │ │ │ ├── last_white_stone.png │ │ │ └── white_stone.png │ │ ├── 19 │ │ │ ├── black_stone.png │ │ │ ├── board.png │ │ │ ├── last_black_stone.png │ │ │ ├── last_white_stone.png │ │ │ └── white_stone.png │ │ ├── background.png │ │ ├── black_overlay.png │ │ ├── bowl_black.png │ │ ├── bowl_white.png │ │ ├── clear_white_overlay.png │ │ ├── go_button.png │ │ ├── lid_black.png │ │ ├── lid_white.png │ │ ├── small_black_stone.png │ │ ├── small_white_stone.png │ │ ├── solid_white_overlay.png │ │ ├── sound_off.png │ │ └── sound_on.png │ ├── layout │ │ ├── background.png │ │ ├── facebook.png │ │ ├── logo.png │ │ ├── right_triangle.png │ │ ├── three_stones.png │ │ ├── twitter.png │ │ └── two_stones.png │ ├── screencast.png │ └── thumbnail │ │ ├── 9 │ │ ├── black_stone.png │ │ ├── board.png │ │ └── white_stone.png │ │ ├── 13 │ │ ├── black_stone.png │ │ ├── board.png │ │ └── white_stone.png │ │ └── 19 │ │ ├── black_stone.png │ │ ├── board.png │ │ └── white_stone.png ├── javascripts │ ├── application.js │ ├── jquery.js │ ├── jquery.min.js │ └── rails.js ├── robots.txt ├── sounds │ ├── pass.mp3 │ ├── pass.ogg │ ├── resign.mp3 │ ├── resign.ogg │ ├── stone.mp3 │ ├── stone.ogg │ ├── stone2.mp3 │ └── stone2.ogg └── stylesheets │ ├── .gitkeep │ ├── application.css │ ├── authentication.css │ ├── game.css │ ├── home.css │ └── user.css ├── script ├── rails └── worker ├── spec ├── controllers │ ├── authentications_controller_spec.rb │ ├── games_controller_spec.rb │ ├── messages_controller_spec.rb │ ├── moves_controller_spec.rb │ ├── sessions_controller_spec.rb │ └── users_controller_spec.rb ├── factories.rb ├── fixtures │ ├── authentications.yml │ ├── games.yml │ ├── messages.yml │ └── users.yml ├── lib │ └── game_engine_spec.rb ├── mailers │ └── notifications_spec.rb ├── models │ ├── authentication_spec.rb │ ├── game_spec.rb │ ├── message_spec.rb │ └── user_spec.rb └── spec_helper.rb └── vendor └── plugins └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | db/*.sqlite3 3 | log/*.log 4 | tmp/**/* 5 | config/database.yml 6 | config/private.yml 7 | config/initializers/development_mail.rb 8 | db/schema.rb 9 | public/assets -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | 3 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.2@govsgo --create 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: "bundle exec rake db:drop db:create db:migrate spec" 2 | 3 | before_script: 4 | - "cp config/database.example.yml config/database.yml" 5 | - "cp config/private.example.yml config/private.yml" 6 | 7 | rvm: 8 | - 1.9.2 9 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | load 'deploy' if respond_to?(:namespace) # cap2 differentiator 2 | Dir['vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) } 3 | 4 | load 'config/deploy' # remove this line to skip loading any of the default tasks 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'rails', '3.0.3' 4 | gem 'mysql2' 5 | gem 'omniauth' 6 | gem 'jquery-rails' 7 | gem 'bcrypt-ruby', :require => 'bcrypt' 8 | gem 'will_paginate', '3.0.pre2' 9 | gem 'whenever', :require => false 10 | gem 'go_gtp', '0.0.4', :require => 'go/gtp', :git => "git://github.com/JEG2/go_gtp.git" 11 | gem 'stalker', '0.5.0' 12 | gem 'oily_png', '0.1.0' 13 | gem 'exception_notification', :git => 'git://github.com/rails/exception_notification.git', :require => 'exception_notifier' 14 | 15 | group :development, :test do 16 | gem 'mocha' 17 | gem 'rspec-rails', '>= 2.0.1' 18 | gem 'factory_girl_rails' 19 | gem 'autotest' 20 | gem 'autotest-rails' 21 | end 22 | 23 | group :development do 24 | gem 'capistrano' 25 | gem 'mongrel', '1.2.0.pre2' 26 | gem 'nifty-generators' 27 | end 28 | 29 | group :test do 30 | gem 'guard' 31 | gem 'guard-rspec' 32 | end 33 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/JEG2/go_gtp.git 3 | revision: 2753f3ebeb3c058bfc05eca20810df67557844c6 4 | specs: 5 | go_gtp (0.0.4) 6 | 7 | GIT 8 | remote: git://github.com/rails/exception_notification.git 9 | revision: 192a49a02d63d28b23ed41cebadfedd490929cf1 10 | specs: 11 | exception_notification (1.0.0) 12 | 13 | GEM 14 | remote: http://rubygems.org/ 15 | specs: 16 | ZenTest (4.4.0) 17 | aaronh-chronic (0.3.9) 18 | abstract (1.0.0) 19 | actionmailer (3.0.3) 20 | actionpack (= 3.0.3) 21 | mail (~> 2.2.9) 22 | actionpack (3.0.3) 23 | activemodel (= 3.0.3) 24 | activesupport (= 3.0.3) 25 | builder (~> 2.1.2) 26 | erubis (~> 2.6.6) 27 | i18n (~> 0.4) 28 | rack (~> 1.2.1) 29 | rack-mount (~> 0.6.13) 30 | rack-test (~> 0.5.6) 31 | tzinfo (~> 0.3.23) 32 | activemodel (3.0.3) 33 | activesupport (= 3.0.3) 34 | builder (~> 2.1.2) 35 | i18n (~> 0.4) 36 | activerecord (3.0.3) 37 | activemodel (= 3.0.3) 38 | activesupport (= 3.0.3) 39 | arel (~> 2.0.2) 40 | tzinfo (~> 0.3.23) 41 | activeresource (3.0.3) 42 | activemodel (= 3.0.3) 43 | activesupport (= 3.0.3) 44 | activesupport (3.0.3) 45 | addressable (2.2.2) 46 | arel (2.0.6) 47 | autotest (4.4.1) 48 | autotest-rails (4.1.0) 49 | ZenTest 50 | bcrypt-ruby (2.1.2) 51 | beanstalk-client (1.1.0) 52 | builder (2.1.2) 53 | capistrano (2.5.19) 54 | highline 55 | net-scp (>= 1.0.0) 56 | net-sftp (>= 2.0.0) 57 | net-ssh (>= 2.0.14) 58 | net-ssh-gateway (>= 1.0.0) 59 | chunky_png (0.10.5) 60 | configuration (1.2.0) 61 | daemons (1.0.10) 62 | diff-lcs (1.1.2) 63 | erubis (2.6.6) 64 | abstract (>= 1.0.0) 65 | factory_girl (1.3.2) 66 | factory_girl_rails (1.0) 67 | factory_girl (~> 1.3) 68 | rails (>= 3.0.0.beta4) 69 | faraday (0.4.6) 70 | addressable (>= 2.1.1) 71 | rack (>= 1.0.1) 72 | gem_plugin (0.2.3) 73 | guard (0.3.0) 74 | open_gem (~> 1.4.2) 75 | thor (~> 0.14.6) 76 | guard-rspec (0.2.0) 77 | guard (>= 0.2.2) 78 | highline (1.6.1) 79 | i18n (0.5.0) 80 | jquery-rails (0.2.4) 81 | rails (~> 3.0) 82 | json_pure (1.4.6) 83 | launchy (0.3.7) 84 | configuration (>= 0.0.5) 85 | rake (>= 0.8.1) 86 | mail (2.2.12) 87 | activesupport (>= 2.3.6) 88 | i18n (>= 0.4.0) 89 | mime-types (~> 1.16) 90 | treetop (~> 1.4.8) 91 | mime-types (1.16) 92 | mocha (0.9.9) 93 | rake 94 | mongrel (1.2.0.pre2) 95 | daemons (~> 1.0.10) 96 | gem_plugin (~> 0.2.3) 97 | multi_json (0.0.4) 98 | mysql2 (0.2.6) 99 | net-ldap (0.1.1) 100 | net-scp (1.0.4) 101 | net-ssh (>= 1.99.1) 102 | net-sftp (2.0.5) 103 | net-ssh (>= 2.0.9) 104 | net-ssh (2.0.23) 105 | net-ssh-gateway (1.0.1) 106 | net-ssh (>= 1.99.1) 107 | nifty-generators (0.4.2) 108 | nokogiri (1.4.3.1) 109 | oa-basic (0.1.5) 110 | multi_json (~> 0.0.2) 111 | nokogiri (~> 1.4.2) 112 | oa-core (= 0.1.5) 113 | rest-client (~> 1.6.0) 114 | oa-core (0.1.5) 115 | rack (~> 1.1) 116 | oa-enterprise (0.1.5) 117 | net-ldap (~> 0.1.1) 118 | nokogiri (~> 1.4.2) 119 | oa-core (= 0.1.5) 120 | pyu-ruby-sasl (~> 0.0.3.1) 121 | rubyntlm (~> 0.1.1) 122 | oa-oauth (0.1.5) 123 | multi_json (~> 0.0.2) 124 | nokogiri (~> 1.4.2) 125 | oa-core (= 0.1.5) 126 | oauth (~> 0.4.0) 127 | oauth2 (~> 0.0.10) 128 | oa-openid (0.1.5) 129 | oa-core (= 0.1.5) 130 | rack-openid (~> 1.1.1) 131 | ruby-openid-apps-discovery 132 | oauth (0.4.3) 133 | oauth2 (0.0.13) 134 | faraday (~> 0.4.1) 135 | multi_json (>= 0.0.4) 136 | oily_png (0.1.0) 137 | chunky_png (~> 0.10.2) 138 | omniauth (0.1.5) 139 | oa-basic (= 0.1.5) 140 | oa-core (= 0.1.5) 141 | oa-enterprise (= 0.1.5) 142 | oa-oauth (= 0.1.5) 143 | oa-openid (= 0.1.5) 144 | open_gem (1.4.2) 145 | launchy (~> 0.3.5) 146 | polyglot (0.3.1) 147 | pyu-ruby-sasl (0.0.3.2) 148 | rack (1.2.1) 149 | rack-mount (0.6.13) 150 | rack (>= 1.0.0) 151 | rack-openid (1.1.2) 152 | rack (>= 0.4) 153 | ruby-openid (>= 2.0.3) 154 | rack-test (0.5.6) 155 | rack (>= 1.0) 156 | rails (3.0.3) 157 | actionmailer (= 3.0.3) 158 | actionpack (= 3.0.3) 159 | activerecord (= 3.0.3) 160 | activeresource (= 3.0.3) 161 | activesupport (= 3.0.3) 162 | bundler (~> 1.0) 163 | railties (= 3.0.3) 164 | railties (3.0.3) 165 | actionpack (= 3.0.3) 166 | activesupport (= 3.0.3) 167 | rake (>= 0.8.7) 168 | thor (~> 0.14.4) 169 | rake (0.8.7) 170 | rest-client (1.6.1) 171 | mime-types (>= 1.16) 172 | rspec (2.0.1) 173 | rspec-core (~> 2.0.1) 174 | rspec-expectations (~> 2.0.1) 175 | rspec-mocks (~> 2.0.1) 176 | rspec-core (2.0.1) 177 | rspec-expectations (2.0.1) 178 | diff-lcs (>= 1.1.2) 179 | rspec-mocks (2.0.1) 180 | rspec-core (~> 2.0.1) 181 | rspec-expectations (~> 2.0.1) 182 | rspec-rails (2.0.1) 183 | rspec (~> 2.0.0) 184 | ruby-openid (2.1.8) 185 | ruby-openid-apps-discovery (1.2.0) 186 | ruby-openid (>= 2.1.7) 187 | rubyntlm (0.1.1) 188 | stalker (0.5.0) 189 | beanstalk-client 190 | json_pure 191 | thor (0.14.6) 192 | treetop (1.4.9) 193 | polyglot (>= 0.3.1) 194 | tzinfo (0.3.23) 195 | whenever (0.6.2) 196 | aaronh-chronic (>= 0.3.9) 197 | activesupport (>= 2.3.4) 198 | will_paginate (3.0.pre2) 199 | 200 | PLATFORMS 201 | ruby 202 | 203 | DEPENDENCIES 204 | autotest 205 | autotest-rails 206 | bcrypt-ruby 207 | capistrano 208 | exception_notification! 209 | factory_girl_rails 210 | go_gtp (= 0.0.4)! 211 | guard 212 | guard-rspec 213 | jquery-rails 214 | mocha 215 | mongrel (= 1.2.0.pre2) 216 | mysql2 217 | nifty-generators 218 | oily_png (= 0.1.0) 219 | omniauth 220 | rails (= 3.0.3) 221 | rspec-rails (>= 2.0.1) 222 | stalker (= 0.5.0) 223 | whenever 224 | will_paginate (= 3.0.pre2) 225 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec', :version => 2 do 5 | watch(%r{^spec/.+_spec\.rb}) 6 | watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | 9 | # Rails example 10 | watch('spec/spec_helper.rb') { "spec" } 11 | watch('config/routes.rb') { "spec/routing" } 12 | watch('app/controllers/application_controller.rb') { "spec/controllers" } 13 | watch(%r{^spec/.+_spec\.rb}) 14 | watch(%r{^app/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" } 15 | watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" } 16 | watch(%r{^app/controllers/(.+)_(controller)\.rb}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } 17 | end 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Ryan Bates and James Edward Gray II 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Go vs Go 2 | 3 | This is the source code for [govsgo.com](http://govsgo.com), a site for playing the board game [Go](http://bit.ly/9xwZTy) online with other players or against the computer. 4 | 5 | If you have problems or suggestions, please post them on the [Issue Tracker](http://github.com/ryanb/govsgo/issues). 6 | 7 | 8 | ## Setup 9 | 10 | Ruby 1.9.2 is required. If you're using [RVM](http://rvm.beginrescueend.com/) it should automatically switch to 1.9.2 when entering the directory. 11 | 12 | Run the following commands to set it up. Note the [Homebrew](http://github.com/mxcl/homebrew) command to install [GNU Go](http://www.gnu.org/software/gnugo/) and [Beanstalk](http://kr.github.com/beanstalkd/). You may want to use a different packaging system or install them from the source. 13 | 14 |
15 | bundle 16 | cp config/database.example.yml config/database.yml 17 | cp config/private.example.yml config/private.yml 18 | rake db:create db:migrate 19 | brew install gnu-go beanstalk 20 |21 | 22 | You can start up the server with `rails s` and run the specs with `rake`. 23 | 24 | 25 | ### Background Process 26 | 27 | In production, the computer moves are handled in a background process because GNU Go can take a while and we don't want to tie up the Rails process during this time. 28 | 29 | If you want to test the background process, set `background_process: true` in your `config/private.yml` file. Next run `beanstalkd` and `script/worker`. to start up the processes. Alternatively you can use [god](http://god.rubyforge.org/) to start and monitor it. See the `config/god.rb` file. 30 | 31 | 32 | ### Fix GNU Go 33 | 34 | If GNU Go seems to be stuck, it may have run into a game or two that it cannot complete. Run the following command in the rails console to clear out any old games that are stuck. 35 | 36 | ``` 37 | Game.where("current_player_id IS NULL AND finished_at IS NULL and updated_at < ?", 1.day.ago).update_all(:finished_at, Time.zone.now) 38 | ``` 39 | 40 | Then restart Beanstalkd, the worker, and web server. 41 | 42 | ``` 43 | sudo /etc/init.d/beanstalkd restart 44 | god restart govsgo-worker 45 | touch tmp/restart.txt 46 | ``` 47 | 48 | ## Credits 49 | 50 | This site was originally created for [Rails Rumble 2010](http://r10.railsrumble.com/) by [Ryan Bates](http://railscasts.com/), [James Edward Gray II](http://blog.grayproductions.net/) and [Phil Bates](http://www.prbates.com/). 51 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | require 'rake' 6 | 7 | Govsgo::Application.load_tasks 8 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include ControllerAuthentication 3 | protect_from_forgery 4 | before_filter :mark_user_request, :if => :logged_in? 5 | before_filter :set_user_time_zone, :if => :logged_in? 6 | 7 | private 8 | 9 | def mark_user_request 10 | current_user.update_attribute(:last_request_at, Time.now) 11 | end 12 | 13 | def set_user_time_zone 14 | Time.zone = current_user.time_zone if current_user.time_zone.present? 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/authentications_controller.rb: -------------------------------------------------------------------------------- 1 | class AuthenticationsController < ApplicationController 2 | def index 3 | end 4 | 5 | def create 6 | omniauth = request.env["omniauth.auth"] 7 | authentication = Authentication.find_by_provider_and_uid(omniauth['provider'], omniauth['uid']) 8 | if authentication 9 | current_user.move_games_to(authentication.user) if current_user && current_user.guest? 10 | authentication.user.apply_omniauth(omniauth) 11 | authentication.user.save! 12 | remember_user(authentication.user) 13 | flash[:notice] = "Signed in as #{authentication.user.username}" 14 | redirect_to_target_or_default root_url 15 | else 16 | user = current_user_or_guest 17 | user.authentications.create!(:provider => omniauth['provider'], :uid => omniauth['uid']) 18 | user.apply_omniauth(omniauth) 19 | was_guest = user.guest? 20 | user.guest = false 21 | if user.save 22 | if was_guest 23 | flash[:notice] = "Signed in as #{user.username}." 24 | remember_user(user) 25 | redirect_to_target_or_default root_url 26 | else 27 | flash[:notice] = "Authentication successful." 28 | redirect_to edit_current_user_url 29 | end 30 | else 31 | session[:omniauth] = omniauth.except('extra') 32 | redirect_to edit_current_user_url 33 | end 34 | end 35 | end 36 | 37 | def destroy 38 | @authentication = current_user.authentications.find(params[:id]) 39 | @authentication.destroy 40 | flash[:notice] = "Successfully destroyed authentication." 41 | redirect_to edit_current_user_path 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/controllers/games_controller.rb: -------------------------------------------------------------------------------- 1 | class GamesController < ApplicationController 2 | before_filter :login_required, :only => [:your, :edit, :update] 3 | before_filter :fetch_games, :only => [:index, :show, :other, :your, :new] 4 | 5 | def index 6 | @users = User.where("publicized_at is not null").order("publicized_at desc").limit(7) 7 | @other_games = @other_games.paginate(:page => 1, :per_page => 5) 8 | @your_games = @your_games.paginate(:page => 1, :per_page => 5) if @your_games 9 | end 10 | 11 | def show 12 | @game = Game.find(params[:id]) 13 | @other_games = @other_games.paginate(:page => 1, :per_page => 5) 14 | @your_games = @your_games.paginate(:page => 1, :per_page => 5) if @your_games 15 | @profiles = @game.profiles 16 | @profiles.reverse! if current_user && @profiles.first.user == current_user 17 | end 18 | 19 | def new 20 | @game = Game.new 21 | if params[:username] 22 | user_required("You must first sign in before you can challenge another player to a game.") 23 | @game.chosen_opponent = "user" 24 | @game.opponent_username = params[:username] 25 | else 26 | @game.chosen_opponent = "gnugo" 27 | @game.chosen_color = "black" 28 | @game.adjust_difficulty = true 29 | end 30 | @game.komi = params[:komi] || 6.5 31 | @game.handicap = params[:handicap] || 0 32 | @game.board_size = params[:board_size] || 19 33 | @other_games = @other_games.paginate(:page => 1, :per_page => 5) 34 | @your_games = @your_games.paginate(:page => 1, :per_page => 5) if @your_games 35 | end 36 | 37 | def create 38 | @game = Game.new 39 | @game.creator = current_user_or_guest 40 | @game.attributes = params[:game] 41 | @game.prepare 42 | if @game.save 43 | @game.queue_computer_move 44 | Notifications.invitation(@game).deliver if @game.send_invitation_email? 45 | flash[:notice] = "Game started. Click on a point below to place your stone." 46 | redirect_to @game 47 | else 48 | fetch_games 49 | @other_games = @other_games.paginate(:page => 1, :per_page => 5) 50 | @your_games = @your_games.paginate(:page => 1, :per_page => 5) if @your_games 51 | render :action => 'new' 52 | end 53 | end 54 | 55 | def edit 56 | @game = Game.find(params[:id]) 57 | @game.chosen_opponent = "user" 58 | @game.opponent_username = @game.opponent(current_user).username 59 | @game.chosen_color = @game.black_player == current_user ? "black" : "white" 60 | end 61 | 62 | def update 63 | @game = Game.find(params[:id]) 64 | raise "Unable to update game because you are not the current player" if @game.current_player != current_user 65 | raise "Unable to update game because it has already started." if @game.started? 66 | if params[:invitation_button] == "Accept" 67 | @game.start 68 | @game.save! 69 | elsif params[:invitation_button] == "Decline" 70 | @game.update_attribute(:finished_at, Time.now) 71 | else 72 | @game.creator = current_user 73 | @game.attributes = params[:game] 74 | @game.prepare 75 | @game.save! 76 | end 77 | end 78 | 79 | def other 80 | @other_games = @other_games.paginate(:page => params[:page], :per_page => params[:per_page]) 81 | end 82 | 83 | def your 84 | @your_games = @your_games.paginate(:page => params[:page], :per_page => params[:per_page]) 85 | end 86 | 87 | def resources 88 | end 89 | 90 | def sgf 91 | @game = Game.find(params[:id]) 92 | render :text => @game.sgf, :content_type => 'application/x-go-sgf' 93 | end 94 | 95 | private 96 | 97 | def fetch_games 98 | if logged_in? 99 | @your_games = current_user.games.recent 100 | @other_games = current_user.other_games.recent 101 | else 102 | @other_games = Game.recent 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /app/controllers/messages_controller.rb: -------------------------------------------------------------------------------- 1 | class MessagesController < ApplicationController 2 | before_filter :user_required 3 | 4 | def create 5 | @message = Message.new(params[:message]) 6 | @message.user = current_user 7 | @message.save 8 | Notifications.chat_message(@message).deliver if @message.send_email? 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/moves_controller.rb: -------------------------------------------------------------------------------- 1 | class MovesController < ApplicationController 2 | def index 3 | @game = Game.find(params[:game_id]) 4 | @moves = @game.moves_after(params[:after].to_i) 5 | end 6 | 7 | def create 8 | @game = Game.find(params[:game_id]) 9 | @game.move(params[:move], current_user) 10 | @game.queue_computer_move 11 | Notifications.move(@game).deliver if @game.current_player && @game.current_player.email.present? && @game.current_player.email_on_move? 12 | rescue GameEngine::IllegalMove 13 | flash[:alert] = "That is an illegal move." 14 | rescue GameEngine::OutOfTurn 15 | flash[:alert] = "It is not your turn to move." 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | def new 3 | end 4 | 5 | def create 6 | user = User.authenticate(params[:login], params[:password]) 7 | if user 8 | remember_user(user) 9 | flash[:notice] = "Logged in successfully." 10 | redirect_to_target_or_default("/") 11 | else 12 | flash.now[:alert] = "Invalid login or password." 13 | render :action => 'new' 14 | end 15 | end 16 | 17 | def destroy 18 | forget_user 19 | flash[:notice] = "You have been logged out." 20 | redirect_to root_url 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | before_filter :login_required, :except => [:show, :new, :create, :unsubscribe] 3 | 4 | def show 5 | @user = User.where(:guest => false).find(params[:id]) 6 | @games = @user.games.recent.paginate(:per_page => 5, :page => params[:page]) 7 | end 8 | 9 | def new 10 | if params[:email] 11 | if logged_in? 12 | flash[:notice] = "Please update your profile below." 13 | redirect_to edit_current_user_url(:email => params[:email]) 14 | elsif User.find_by_email(params[:email]) 15 | flash[:notice] = "It appears you already have an account, please login below." 16 | redirect_to login_url(:login => params[:email]) 17 | end 18 | end 19 | @user = User.new(:email => params[:email], :email_on_invitation => true) 20 | end 21 | 22 | def create 23 | @user = User.new(params[:user]) 24 | if @user.save 25 | remember_user(@user) 26 | flash[:notice] = "Thank you for signing up! You are now logged in." 27 | redirect_to root_url 28 | else 29 | render :action => 'new' 30 | end 31 | end 32 | 33 | def edit 34 | @user = current_user 35 | if params[:email] 36 | @user.email = params[:email] 37 | end 38 | if session[:omniauth] 39 | @user.apply_omniauth(session[:omniauth]) 40 | @user.valid? 41 | end 42 | end 43 | 44 | def update 45 | @user = current_user 46 | @user.attributes = params[:user] 47 | @user.guest = false 48 | if @user.save 49 | session[:omniauth] = nil 50 | flash[:notice] = "Your profile has been updated." 51 | redirect_to root_url 52 | else 53 | render :action => 'edit' 54 | end 55 | end 56 | 57 | def unsubscribe 58 | @user = User.find_by_unsubscribe_token!(params[:token]) 59 | @user.update_attributes!(:email_on_invitation => false, :email_on_move => false, :email_on_message => false) 60 | flash[:notice] = "You have been unsubscribed from further email notifications." 61 | redirect_to root_url 62 | end 63 | 64 | def publicize 65 | if guest? 66 | redirect_to signin_url, :alert => "You must first sign in to be added to the Looking for Games list." 67 | else 68 | @user = current_user 69 | @user.update_attribute(:publicized_at, (params[:remove] ? nil : Time.now)) 70 | redirect_to root_url, :notice => "You have been #{params[:remove] ? 'removed from' : 'added to'} the Looking for Games list." 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def online_status(user) 3 | content_tag(:span, raw("•"), :class => (user.online? ? "user_online" : "user_offline")) if user 4 | end 5 | 6 | def div_hidden_if(condition, options, &block) 7 | options[:style] = "#{options[:style]}display:none;" if condition 8 | content_tag(:div, options, &block) 9 | end 10 | 11 | def avatar_image_tag(user) 12 | content_tag(:div, :class => "avatar") do 13 | link_to image_tag(avatar_url(user)), user 14 | end 15 | end 16 | 17 | def avatar_url(user) 18 | if user.nil? 19 | "avatars/gnugo.png" 20 | elsif user.avatar_url.present? 21 | user.avatar_url 22 | elsif user.email.present? 23 | default_url = request.protocol + request.host_with_port + "/images/avatars/guest.png" 24 | "http://gravatar.com/avatar/#{Digest::MD5.hexdigest(user.email.downcase)}.png?s=48&d=#{CGI.escape(default_url)}" 25 | else 26 | "avatars/guest.png" 27 | end 28 | end 29 | 30 | def link_to_user(user) 31 | if user 32 | user.guest? ? "Guest" : link_to(user.name_with_rank, user, :class => "subtle") 33 | else 34 | "GNU Go" 35 | end 36 | end 37 | 38 | def relative_time(time) 39 | [relative_date(time.to_date), time.strftime("%I:%M %p")].compact.join(" ") 40 | end 41 | 42 | def relative_date(date) 43 | today = Time.zone.now.to_date 44 | if date == today 45 | nil 46 | elsif date == today-1 47 | "yesterday" 48 | elsif date.year == today.year 49 | date.strftime("%b %d") 50 | else 51 | date.strftime("%b %d, %Y") 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/helpers/authentications_helper.rb: -------------------------------------------------------------------------------- 1 | module AuthenticationsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/error_messages_helper.rb: -------------------------------------------------------------------------------- 1 | module ErrorMessagesHelper 2 | # Render error messages for the given objects. The :message and :header_message options are allowed. 3 | def error_messages_for(*objects) 4 | options = objects.extract_options! 5 | options[:header_message] ||= "Invalid Fields" 6 | options[:message] ||= "Correct the following errors and try again." 7 | messages = objects.compact.map { |o| o.errors.full_messages }.flatten 8 | unless messages.empty? 9 | content_tag(:div, :class => "error_messages") do 10 | list_items = messages.map { |msg| content_tag(:li, msg) } 11 | content_tag(:h2, options[:header_message]) + content_tag(:p, options[:message]) + content_tag(:ul, list_items.join.html_safe) 12 | end 13 | end 14 | end 15 | 16 | module FormBuilderAdditions 17 | def error_messages(options = {}) 18 | @template.error_messages_for(@object, options) 19 | end 20 | end 21 | end 22 | 23 | ActionView::Helpers::FormBuilder.send(:include, ErrorMessagesHelper::FormBuilderAdditions) 24 | -------------------------------------------------------------------------------- /app/helpers/games_helper.rb: -------------------------------------------------------------------------------- 1 | module GamesHelper 2 | THUMB_DIR = Rails.root + "public" + "assets" + "games" + "thumbs" 3 | 4 | def color_at(position, game = @game) 5 | if game.black_positions_list.include? position then "b" 6 | elsif game.white_positions_list.include? position then "w" 7 | else "e" 8 | end 9 | end 10 | 11 | def thumbnail(game) 12 | thumb = THUMB_DIR + "#{game.id}.png" 13 | path = thumb.exist? ? thumb.to_s.sub(/\A.*\bpublic\b/, "") : 14 | "thumbnail/#{game.board_size}/board.png" 15 | image_tag(path, :size => "80x80") 16 | end 17 | 18 | def link_to_game_thumbnail(game) 19 | link_to thumbnail(game), game, :class => "thumbnail" 20 | end 21 | 22 | def game_score(score, finished) 23 | if finished 24 | if score.to_f == 1.0 25 | "won" 26 | elsif score.to_f > 0 27 | "won by #{score}" 28 | end 29 | else 30 | pluralize(score.to_i, "captured stone") 31 | end 32 | end 33 | 34 | def board_div(game, &block) 35 | options = { 36 | "id" => "board", 37 | "class" => "size_#{game.board_size}", 38 | "data-moves" => game.moves.to_s, 39 | "data-handicap" => game.handicap.to_i, 40 | "data-current-player" => game.current_player_id.to_i, 41 | "data-current-user" => current_user.try(:id).to_s, 42 | "data-started" => game.started?.to_s, 43 | "data-finished" => game.finished?.to_s, 44 | } 45 | content_tag(:div, options, &block) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/helpers/layout_helper.rb: -------------------------------------------------------------------------------- 1 | # These helper methods can be called in your template to set variables to be used in the layout 2 | # This module should be included in all views globally, 3 | # to do so you may need to add this line to your ApplicationController 4 | # helper :layout 5 | module LayoutHelper 6 | def title(page_title, show_title = true) 7 | content_for(:title) { h(page_title.to_s) } 8 | @show_title = show_title 9 | end 10 | 11 | def show_title? 12 | @show_title 13 | end 14 | 15 | def stylesheet(*args) 16 | content_for(:head) { stylesheet_link_tag(*args) } 17 | end 18 | 19 | def javascript(*args) 20 | content_for(:head) { javascript_include_tag(*args) } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/helpers/messages_helper.rb: -------------------------------------------------------------------------------- 1 | module MessagesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/moves_helper.rb: -------------------------------------------------------------------------------- 1 | module MovesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module SessionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/mailers/notifications.rb: -------------------------------------------------------------------------------- 1 | class Notifications < ActionMailer::Base 2 | default :from => "noreply@govsgo.com" 3 | 4 | def invitation(game) 5 | @game = game 6 | mail :to => @game.current_player.email, :subject => "[Go vs Go] Invitation from #{@game.opponent.username}" 7 | end 8 | 9 | def move(game) 10 | @game = game 11 | mail :to => @game.current_player.email, :subject => "[Go vs Go] Move by #{@game.opponent.username}" 12 | end 13 | 14 | def chat_message(message) 15 | @message = message 16 | mail :to => @message.recipient.email, :subject => "[Go vs Go] Chat from #{@message.user.username}" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/models/authentication.rb: -------------------------------------------------------------------------------- 1 | class Authentication < ActiveRecord::Base 2 | belongs_to :user 3 | 4 | def provider_name 5 | if provider == 'open_id' 6 | "OpenID" 7 | else 8 | provider.titleize 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/game.rb: -------------------------------------------------------------------------------- 1 | class Game < ActiveRecord::Base 2 | ################# 3 | ### Callbacks ### 4 | ################# 5 | 6 | after_save :generate_thumbnail, :if => :update_thumbnail 7 | 8 | def generate_thumbnail 9 | GameThumb.generate(id, board_size, black_positions, white_positions) unless Rails.env.test? 10 | end 11 | 12 | #################### 13 | ### Associations ### 14 | #################### 15 | 16 | belongs_to :black_player, :class_name => "User" 17 | belongs_to :white_player, :class_name => "User" 18 | belongs_to :current_player, :class_name => "User" 19 | has_many :messages 20 | 21 | ################### 22 | ### Validations ### 23 | ################### 24 | 25 | attr_accessible :komi, :handicap, :board_size, :chosen_color, :chosen_opponent, :opponent_username, :adjust_difficulty 26 | 27 | validates_inclusion_of :board_size, :in => [9, 13, 19], :allow_nil => true 28 | validates_inclusion_of :handicap, :in => (0..9).to_a, :allow_nil => true 29 | validates_inclusion_of :komi, :in => [0.5, 5.5, 6.5], :allow_nil => true 30 | validate :opponent_found 31 | 32 | def opponent_found 33 | if chosen_opponent == "user" && (black_player.blank? || white_player.blank?) 34 | errors.add(:opponent_username, "not found") 35 | end 36 | end 37 | 38 | ############## 39 | ### Scopes ### 40 | ############## 41 | 42 | scope :finished, where("finished_at is not null") 43 | scope :unfinished, where("finished_at is null") 44 | scope :with_gnugo, where("white_player_id is null or black_player_id is null") 45 | scope :without_gnugo, where("white_player_id is not null and black_player_id is not null") 46 | 47 | scope :recent, order("updated_at desc") 48 | 49 | ######################## 50 | ### Instance Methods ### 51 | ######################## 52 | 53 | attr_accessor :chosen_color, :creator, :chosen_opponent, :opponent_username, :update_thumbnail, :adjust_difficulty 54 | 55 | def adjust_difficulty? 56 | adjust_difficulty == true || adjust_difficulty.to_i == 1 57 | end 58 | 59 | def black_positions_list 60 | if black_positions && @black_positions_list && @black_positions_list.size != black_positions.size / 2 61 | @black_positions_list = nil 62 | end 63 | @black_positions_list ||= black_positions.to_s.scan(/[a-s]{2}/) 64 | end 65 | 66 | def white_positions_list 67 | if white_positions && @white_positions_list && @white_positions_list.size != white_positions.size / 2 68 | @white_positions_list = nil 69 | end 70 | @white_positions_list ||= white_positions.to_s.scan(/[a-s]{2}/) 71 | end 72 | 73 | # todo: This method needs to be tested better 74 | def prepare 75 | opponent = nil 76 | if chosen_opponent == "user" 77 | opponent = User.find_by_username(opponent_username) 78 | else 79 | adjust_to_level(creator.gnugo_level) if creator && adjust_difficulty? 80 | end 81 | color = chosen_color.blank? ? %w[black white].sample : chosen_color 82 | if color == "black" 83 | self.black_player = creator 84 | self.white_player = opponent 85 | elsif color == "white" 86 | self.black_player = opponent 87 | self.white_player = creator 88 | end 89 | self.current_player = opponent 90 | start if opponent.nil? # Skip invitation when playing GNU Go 91 | end 92 | 93 | def start 94 | if handicap.to_i > 0 95 | self.black_positions = handicap_positions 96 | self.current_player = white_player 97 | else 98 | self.current_player = black_player 99 | end 100 | self.started_at = Time.now 101 | self.update_thumbnail = true 102 | end 103 | 104 | # todo: This method needs to be tested better 105 | def move(position, user) 106 | raise GameEngine::OutOfTurn if user.try(:id) != current_player_id 107 | GameEngine.update_game_attributes_with_move(attributes.symbolize_keys, position).each do |name, value| 108 | self.send("#{name}=", value) 109 | end 110 | self.update_thumbnail = true # todo: this could be made smarter 111 | # Check current_player again, fetching from database to prevent async double move problem 112 | # This should probably be moved into a database lock so no updates happen between here and the save 113 | raise GameEngine::OutOfTurn if user.try(:id) != Game.find(id, :select => "current_player_id").current_player_id 114 | save! 115 | end 116 | 117 | def queue_computer_move 118 | if !finished? && !current_player 119 | if PRIVATE_CONFIG["background_process"] && !Rails.env.test? 120 | Stalker.enqueue("Game.move", :id => id) 121 | else 122 | move(nil, nil) 123 | end 124 | end 125 | end 126 | 127 | def split_moves 128 | moves.to_s.split("-") 129 | end 130 | 131 | def moves_after(index) 132 | (split_moves[index..-1] || []).join('-') 133 | end 134 | 135 | def last_move 136 | split_moves.last.to_s 137 | end 138 | 139 | def last_position 140 | last_move[/^[a-s]{2}/] 141 | end 142 | 143 | def finished? 144 | finished_at.present? 145 | end 146 | 147 | def started? 148 | started_at.present? 149 | end 150 | 151 | def active? 152 | started? && !finished? 153 | end 154 | 155 | def profile_for(color) 156 | Profile.new(color).tap do |profile| 157 | if color.to_sym == :white 158 | profile.handicap_or_komi = "#{komi} komi" 159 | else 160 | profile.handicap_or_komi = "#{handicap} handicap" 161 | end 162 | profile.score = send("#{color}_score") 163 | profile.captured = captured(color) 164 | profile.user = send("#{color}_player") 165 | if profile.user == current_player 166 | profile.current = true 167 | else 168 | case last_move 169 | when "PASS" then profile.last_status = "passed" 170 | when "RESIGN" then profile.last_status = "resigned" 171 | end 172 | end 173 | end 174 | end 175 | 176 | def profiles 177 | [profile_for(:white), profile_for(:black)] 178 | end 179 | 180 | def profiles_with_current_first 181 | profiles.sort_by { |p| p.current ? 0 : 1 } 182 | end 183 | 184 | def sgf 185 | sgf = ";FF[4]GM[1]CA[utf-8]AP[govsgo:0.1]RU[Japanese]" 186 | sgf << "SZ[#{board_size}]KM[#{komi}]HA[#{handicap.to_i}]" 187 | colors = %w[B W].cycle 188 | if handicap.to_i > 0 189 | colors.next 190 | sgf << "AB" + handicap_positions.gsub(/../, "[\\0]") 191 | end 192 | {"B" => black_player, "W" => white_player}.each do |color, player| 193 | name = player ? (player.username.blank? ? "Guest" : player.username) : "GNU Go" 194 | sgf << "P#{color}[#{name}]#{color}R[#{player.try(:rank)}]" 195 | end 196 | if finished? 197 | score = last_move == "RESIGN" ? "R" : [white_score.to_f, black_score.to_f].max 198 | sgf << "RE[#{black_score.to_i == 0 ? 'W' : 'B'}+#{score}]" 199 | end 200 | split_moves.each do |move| 201 | unless move == "RESIGN" 202 | sgf << ";#{colors.next}[#{move == 'PASS' ? '' : move[0..1]}]" 203 | end 204 | end 205 | "(#{sgf})" 206 | end 207 | 208 | def handicap_positions 209 | positions = "" 210 | GameEngine.run(attributes.symbolize_keys) do |engine| 211 | positions << engine.positions(:black) 212 | end 213 | positions 214 | end 215 | 216 | def captured(color) 217 | if started? && finished? 218 | count = 0 219 | offset = (color == :white && handicap.to_i == 0 || color == :black && handicap.to_i > 0) ? 1 : 0 220 | split_moves.each_with_index do |move, index| 221 | if (index+offset) % 2 == 0 222 | count += move.length/2-1 if move =~ /^[a-z]/ 223 | end 224 | end 225 | count 226 | else 227 | send("#{color}_score").to_i 228 | end 229 | end 230 | 231 | def level_for(user) 232 | adjust = 0 233 | adjust += 1 if komi < 1.0 234 | adjust += handicap.to_i 235 | if user == black_player 236 | 10 - adjust 237 | elsif user == white_player 238 | 11 + adjust 239 | end 240 | end 241 | 242 | def adjust_to_level(level) 243 | self.chosen_color = level > 10 ? "white" : "black" 244 | self.komi = [9, 12].include?(level) ? 0.5 : 6.5 245 | if (9..12).include? level 246 | self.handicap = 0 247 | elsif level < 9 248 | self.handicap = (level - 10).abs 249 | elsif level > 12 250 | self.handicap = level - 11 251 | end 252 | end 253 | 254 | def resulting_level_for(user) 255 | if winner == user 256 | level_for(user) + 1 257 | else 258 | level_for(user) - 1 259 | end 260 | end 261 | 262 | def winner 263 | black_score.to_f >= white_score.to_f ? black_player : white_player if started? && finished? 264 | end 265 | 266 | def loser 267 | black_score.to_f < white_score.to_f ? black_player : white_player if started? && finished? 268 | end 269 | 270 | def switch_current_player 271 | self.current_player = opponent 272 | end 273 | 274 | def opponent(player = current_player) 275 | player == black_player ? white_player : black_player 276 | end 277 | 278 | def player?(user) 279 | black_player == user || white_player == user 280 | end 281 | 282 | def send_invitation_email? 283 | black_player && white_player && current_player && current_player.email.present? && current_player.email_on_invitation? 284 | end 285 | end 286 | -------------------------------------------------------------------------------- /app/models/message.rb: -------------------------------------------------------------------------------- 1 | class Message < ActiveRecord::Base 2 | attr_accessible :game_id, :content, :move_index 3 | belongs_to :game 4 | belongs_to :user 5 | 6 | validate :ensure_player 7 | validates_presence_of :user_id, :game_id, :content 8 | 9 | before_create :remember_move_index 10 | 11 | def ensure_player 12 | if game && !game.player?(user) 13 | errors.add :game_id, "is not owned by you so you cannot send the message." 14 | end 15 | end 16 | 17 | def remember_move_index 18 | self.move_index = game.split_moves.size-1 if game && game.moves.present? 19 | end 20 | 21 | def send_email? 22 | recipient && recipient.email_on_message? && !recipient.online? 23 | end 24 | 25 | def recipient 26 | game && game.opponent(user) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/models/profile.rb: -------------------------------------------------------------------------------- 1 | class Profile 2 | attr_accessor :captured, :handicap_or_komi, :user, :color, :score, :last_status, :current 3 | 4 | def initialize(color) 5 | @color = color 6 | end 7 | 8 | def user_id 9 | user ? user.id : 0 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | attr_accessible :username, :email, :password, :password_confirmation, :guest, :rank, :email_on_invitation, :email_on_move, :email_on_message, :time_zone 3 | 4 | has_many :authentications 5 | has_many :messages 6 | 7 | attr_accessor :password 8 | before_save :prepare_password 9 | before_create :generate_tokens 10 | 11 | validates_presence_of :username, :unless => :guest? 12 | validates_uniqueness_of :username, :email, :allow_blank => true 13 | validates_format_of :username, :with => /^[-\w\._]+$/i, :allow_blank => true, :message => "should only contain letters, numbers, or .-_" 14 | validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i, :allow_blank => true 15 | validates_presence_of :password, :if => :password_required? 16 | validates_confirmation_of :password 17 | validates_length_of :password, :minimum => 4, :allow_blank => true 18 | 19 | # login can be either username or email address 20 | def self.authenticate(login, pass) 21 | user = find_by_username(login) || find_by_email(login) 22 | return user if user && user.matching_password?(pass) 23 | end 24 | 25 | def matching_password?(pass) 26 | self.password_hash == encrypt_password(pass) 27 | end 28 | 29 | def games 30 | Game.where("black_player_id = ? or white_player_id = ?", id, id) 31 | end 32 | 33 | def other_games 34 | Game.where("(black_player_id != ? or black_player_id is null) and (white_player_id != ? or white_player_id is null)", id, id) 35 | end 36 | 37 | def games_your_turn 38 | games.unfinished.where("current_player_id = ?", id) 39 | end 40 | 41 | def games_their_turn 42 | games.unfinished.where("current_player_id != ? or current_player_id is null", id) 43 | end 44 | 45 | def password_required? 46 | !guest? && (new_record? || password_hash.blank?) && authentications.empty? 47 | end 48 | 49 | def apply_omniauth(omniauth) 50 | if email.blank? 51 | self.email = omniauth['user_info']['email'] 52 | self.email_on_invitation = true unless email.blank? 53 | end 54 | self.username = omniauth['user_info']['nickname'] if username.blank? 55 | self.avatar_url = omniauth['user_info']['image'] if omniauth['user_info'] && omniauth['user_info']['image'].present? 56 | end 57 | 58 | def online? 59 | last_request_at && last_request_at > 5.minutes.ago 60 | end 61 | 62 | def name_with_rank 63 | [username, rank].reject(&:blank?).join(" ") 64 | end 65 | 66 | def move_games_to(user) 67 | games.each do |game| 68 | %w[black white current].each do |type| 69 | game.update_attribute("#{type}_player_id", user.id) if game.send("#{type}_player_id") == id 70 | end 71 | end 72 | end 73 | 74 | def generate_tokens 75 | generate_token(:token) 76 | generate_token(:unsubscribe_token) 77 | end 78 | 79 | def generate_token(name) 80 | if self[name].blank? 81 | characters = ('a'..'z').to_a + ('A'..'Z').to_a + ('1'..'9').to_a 82 | begin 83 | self[name] = Array.new(16) { characters.sample }.join 84 | end while self.class.exists?(name => token) 85 | end 86 | end 87 | 88 | def gnugo_level 89 | @gnugo_level ||= calculate_gnugo_level 90 | end 91 | 92 | private 93 | 94 | def prepare_password 95 | unless password.blank? 96 | self.password_salt = BCrypt::Engine.generate_salt 97 | self.password_hash = encrypt_password(password) 98 | end 99 | end 100 | 101 | def encrypt_password(pass) 102 | BCrypt::Engine.hash_secret(pass, password_salt) 103 | end 104 | 105 | def calculate_gnugo_level 106 | finished_games = games.finished.with_gnugo 107 | if finished_games.size > 0 108 | levels = finished_games.order("finished_at desc").limit(3).map { |g| g.resulting_level_for(self) } 109 | level = (levels.inject(:+).to_f / levels.size).round 110 | [[level, 20].min, 1].max 111 | else 112 | 6 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /app/views/authentications/index.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Sign In" %> 2 | <% stylesheet "authentication" %> 3 | 4 |
25 | <%= label_tag :email, "Email Address" %> 26 | <%= text_field_tag :email, params[:email] %> 27 | <%= submit_tag "Continue", :name => nil %> 28 |
29 | <% end %> 30 |
6 | Playing against computer (GNU Go).
7 | To play against others you must first <%= link_to "sign in", signin_path %>.
8 |
11 | Play against:
12 | <%= f.radio_button :chosen_opponent, "gnugo" %> Computer (GNU Go)
13 | <%= f.radio_button :chosen_opponent, "user" %> Username:
14 | <%= f.text_field :opponent_username %>
15 |
19 | <%= f.check_box :adjust_difficulty %> 20 | <%= f.label :adjust_difficulty, "Automatically adjust difficulty", :class => "inline" %> 21 | <%= "(Level #{current_user.gnugo_level})" if current_user %> 22 |
23 | <% end %> 24 | <% else %> 25 | <%= f.hidden_field :chosen_opponent %> 26 | <%= f.hidden_field :opponent_username %> 27 | <% end %> 28 |29 | <%= f.label :board_size %> 30 | <%= f.select :board_size, [["19 x 19", 19], ["13 x 13", 13], ["9 x 9", 9]] %> 31 |
32 | <%= div_hidden_if @game.new_record? && @game.adjust_difficulty? && @game.chosen_opponent != "user", :class => "manual_difficulty" do %> 33 |34 | <%= f.label :chosen_color, "Play as" %> 35 | <%= f.select :chosen_color, [["Random", ""], ["Black", "black"], ["White", "white"]] %> 36 |
37 |38 | <%= f.label :handicap %> 39 | <%= f.select :handicap, [0, 2, 3, 4, 5, 6, 7, 8, 9] %> 40 |
41 |42 | <%= f.label :komi %> 43 | <%= f.select :komi, [0.5, 5.5, 6.5] %> 44 |
45 | <% end %> 46 |<%= f.submit (@game.new_record? ? "Start Game" : "Update Game") %>
47 | <% end %> 48 | -------------------------------------------------------------------------------- /app/views/games/_game.html.erb: -------------------------------------------------------------------------------- 1 |<%= link_to "Play Again", new_game_path(:board_size => @game.board_size) %>
8 | <% elsif current_user && @game.loser == current_user %> 9 |Need some help? Check out the <%= link_to "Go Resources", go_resources_path %>.
11 |<%= link_to "Play Again", new_game_path(:board_size => @game.board_size) %>
12 | <% else %> 13 |Looks like the game has started, <%= link_to "reload", @game %> to play.
19 |Board Size: <%= @game.board_size %>
25 |Playing as: <%= @game.white_player == current_user ? "White" : "Black" %>
26 |Komi: <%= @game.komi %>
27 |Handicap: <%= @game.handicap %>
28 |Want to change something? <%= link_to "Edit Settings", edit_game_path(@game) %>
29 |30 | <%= f.submit "Accept", :name => "invitation_button", :id => "accept_button" %> 31 | <%= f.submit "Decline", :name => "invitation_button", :id => "decline_button" %> 32 |
33 | <% end %> 34 |Waiting for opponent to accept game.
38 |Go is a board game invented in China more than 4,000 years ago. It is more popular in the far East than Chess in the West. The rules are simple but mastering the game can take a lifetime.
9 |We invite you to <%= link_to "try a game", new_game_path(:board_size => 9) %> against the computer or online with other players.
10 |Use these links to find everything you want to know about Go.
7 |