├── .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 |
5 |
6 |

Sign in through one of the following services

7 | 8 | 9 | <%= image_tag "auth/twitter_64.png", :size => "64x64", :alt => "Twitter" %> 10 | Twitter 11 | 12 | 13 | <%= image_tag "auth/facebook_64.png", :size => "64x64", :alt => "Facebook" %> 14 | Facebook 15 | 16 | 17 | <%= image_tag "auth/open_id_64.png", :size => "64x64", :alt => "OpenID" %> 18 | OpenID 19 | 20 |
21 | 22 |

Don't use these services?
Just enter your email address:

23 | <%= form_tag new_user_path, :method => :get do %> 24 |

25 | <%= label_tag :email, "Email Address" %> 26 | <%= text_field_tag :email, params[:email] %> 27 | <%= submit_tag "Continue", :name => nil %> 28 |

29 | <% end %> 30 |
31 |
32 | -------------------------------------------------------------------------------- /app/views/games/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for @game do |f| %> 2 | <%= f.error_messages %> 3 | <% if @game.new_record? %> 4 | <% if guest? %> 5 |

6 | Playing against computer (GNU Go).
7 | To play against others you must first <%= link_to "sign in", signin_path %>. 8 |

9 | <% else %> 10 |

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 |

16 | <% end %> 17 | <%= div_hidden_if @game.chosen_opponent == "user", :class => "auto_difficulty" do %> 18 |

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 |
2 | <%= link_to_game_thumbnail game %> 3 |
4 | <% game.profiles_with_current_first.each do |profile| %> 5 |
<%= link_to_user(profile.user) %> <%= online_status(profile.user) %>
6 | <% end %> 7 | <% if game.last_move_at %> 8 |
moved <%= time_ago_in_words(game.last_move_at) %> ago
9 | <% end %> 10 |
11 | <%= link_to "Go", game, :class => "go_button" %> 12 |
13 | -------------------------------------------------------------------------------- /app/views/games/_other_games.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Watch a Game

3 | <% @other_games.each_with_index do |game, index| %> 4 | <% unless index.zero? %> 5 |
6 | <% end %> 7 | <%= render game %> 8 | <% end %> 9 | <%= render "pagination", :games => @other_games, :game_type => "other" %> 10 |
11 | -------------------------------------------------------------------------------- /app/views/games/_pagination.html.erb: -------------------------------------------------------------------------------- 1 | <% unless games.previous_page.nil? && games.next_page.nil? %> 2 |
3 | 12 | <% end %> 13 | -------------------------------------------------------------------------------- /app/views/games/_panel.html.erb: -------------------------------------------------------------------------------- 1 | <% if @game.finished? %> 2 |
3 | <% if !@game.started? %> 4 |

Game has been declined.

5 | <% elsif current_user && @game.winner == current_user %> 6 |

Congratulations, you won!

7 |

<%= link_to "Play Again", new_game_path(:board_size => @game.board_size) %>

8 | <% elsif current_user && @game.loser == current_user %> 9 |

Sorry, you lost.

10 |

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 |

<%= @game.winner ? (@game.winner.username || "Guest") : "GNU Go" %> won!

14 | <% end %> 15 |
16 | <% elsif @game.started? %> 17 |
18 |

Looks like the game has started, <%= link_to "reload", @game %> to play.

19 |
20 | <% elsif @game.current_player == current_user %> 21 |
22 | <%= form_for @game do |f| %> 23 |

Challenge from <%= @game.opponent(current_user).username %>

24 |

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 |
35 | <% else %> 36 |
37 |

Waiting for opponent to accept game.

38 |
39 | <% end %> 40 | -------------------------------------------------------------------------------- /app/views/games/_profile.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | <%= image_tag "game/bowl_#{profile.color}.png", :class => "bowl" %> 5 | <%= image_tag "game/lid_#{profile.color}.png", :class => "lid" %> 6 | <%= avatar_image_tag(profile.user) %> 7 |
8 |
9 |
<%= link_to_user(profile.user) %> as <%= profile.color.to_s.titleize %> <%= online_status(profile.user) %>
10 | 11 |
<%= profile.last_status %>
12 |
<%= profile.handicap_or_komi %>
13 |
<%= game_score profile.score, @game.finished? %>
14 |
15 | <% if profile.user && profile.user == current_user %> 16 |
17 | <%= link_to "Pass", '#', :id => "play_pass" %> 18 | <%= link_to "Resign", '#', :id => "play_resign" %> 19 |
20 | <% end %> 21 |
22 | -------------------------------------------------------------------------------- /app/views/games/_side.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% unless @your_games.blank? %> 3 |
4 | <%= render "your_games" %> 5 |
6 | <% end %> 7 | <% if @game.player?(current_user) && @game.black_player.present? && @game.white_player.present? %> 8 |
9 |

Private Chat

10 |
<%= render @game.messages.order(:created_at) %>
11 | <%= form_for Message.new(:game_id => @game.id) do |f| %> 12 | <%= f.hidden_field :game_id %> 13 | <%= f.text_field :content %> 14 | <%= f.submit "Send" %> 15 | <% end %> 16 |
17 | <% else %> 18 |
19 | <%= render "other_games" %> 20 |
21 | <% end %> 22 |
23 | -------------------------------------------------------------------------------- /app/views/games/_your_games.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Your Games <%= link_to "New Game", new_game_path(:board_size => 9) %>

3 | <% if @your_games.blank? %> 4 |
5 |
6 | You do not have any games yet.
7 | <%= link_to "Try a quick game", new_game_path(:board_size => 9) %> 8 | or <%= link_to "watch a video tour", "http://media.railscasts.com/videos/govsgo.mov" %>. 9 |
10 | <%= link_to image_tag("screencast.png", :size => "259x278", :alt => "Watch the Screencast"), "http://media.railscasts.com/videos/govsgo.mov", :class => "screencast" %> 11 |
12 | <% else %> 13 | <% @your_games.each_with_index do |game, index| %> 14 | <% unless index.zero? %> 15 |
16 | <% end %> 17 | <%= render game %> 18 | <% end %> 19 | <%= render "pagination", :games => @your_games, :game_type => "your" %> 20 | <% end %> 21 |
22 | -------------------------------------------------------------------------------- /app/views/games/edit.js.erb: -------------------------------------------------------------------------------- 1 | $("#panel_container .tall_panel").html("<%= escape_javascript(render("form")) %>"); 2 | -------------------------------------------------------------------------------- /app/views/games/index.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Welcome" %> 2 | <% stylesheet "home" %> 3 | 4 |
5 | 6 |
7 |

Welcome

8 |

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 |
11 | 12 |
13 |
14 |
15 | <%= render "your_games" %> 16 |
17 |
18 |
19 |
20 | <%= render "other_games" %> 21 |
22 |
23 |
24 |
25 |

Looking for Games

26 | <% @users.each_with_index do |user, index| %> 27 | <% unless index.zero? %> 28 |
29 | <% end %> 30 |
31 | <%= avatar_image_tag(user) %> 32 |
33 | <%= link_to_user user %> <%= online_status(user) %> 34 |
35 | <% unless user == current_user %> 36 | <%= link_to "Challenge", new_game_path(:username => user.username), :class => "go_button" %> 37 | <% end %> 38 |
39 | <% end %> 40 |
41 | <% if guest? %> 42 | To add yourself to this list, first <%= link_to "sign in", signin_url %>. 43 | <% elsif @users.include? current_user %> 44 | <%= link_to "Remove Yourself from List", publicize_path(:remove => true), :method => :put, :class => "button" %> 45 | <% else %> 46 | <%= link_to "Add Yourself to List", publicize_path, :method => :put, :class => "button" %> 47 | <% end %> 48 |
49 |
50 |
51 |
52 |
53 |
54 | -------------------------------------------------------------------------------- /app/views/games/new.html.erb: -------------------------------------------------------------------------------- 1 | <% title "New Go Game" %> 2 | <% stylesheet "game" %> 3 | <% @hide_footer = true %> 4 | 5 |
6 | <%= render "side" %> 7 |
8 |
9 | <%= image_tag "game/bowl_white.png", :class => "bowl" %> 10 | <%= image_tag "game/lid_white.png", :class => "lid" %> 11 |
12 |
13 |
14 | <%= render "form" %> 15 |
16 |
17 |
18 |
19 | <%= image_tag "game/bowl_black.png", :class => "bowl" %> 20 | <%= image_tag "game/lid_black.png", :class => "lid" %> 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /app/views/games/other.js.erb: -------------------------------------------------------------------------------- 1 | $("#other_games").replaceWith("<%= escape_javascript(render("other_games")) %>"); 2 | -------------------------------------------------------------------------------- /app/views/games/resources.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Go Resources" %> 2 | 3 |
4 |
5 |

Go Resources

6 |

Use these links to find everything you want to know about Go.

7 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /app/views/games/show.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Play Go" %> 2 | <% stylesheet "game" %> 3 | <% @hide_footer = true %> 4 | 5 |
6 | <%= render "side" %> 7 |
8 |
<%= render "profile", :profile => @profiles.first %>
9 | <%= board_div(@game) do %> 10 |
11 | <% for y in ('a'..'s').first(@game.board_size) %> 12 | <% for x in ('a'..'s').first(@game.board_size) %> 13 |
" id="<%= x + y %>">
14 | <% end %> 15 | <% end %> 16 |
17 | <% end %> 18 | <%= link_to image_tag("game/sound_on.png", :size => "18x16"), '#', :id => "sound_switch" %> 19 |
20 | <%= link_to "SGF", game_sgf_path(@game), :id => "sgf" %> 21 | <%= link_to "<<", '#', :id => "first_move" %> 22 | <%= link_to "<", '#', :id => "previous_move" %> 23 | <%= link_to ">", '#', :id => "next_move" %> 24 | <%= link_to ">>", '#', :id => "last_move" %> 25 |
26 |
<%= render "profile", :profile => @profiles.last %>
27 |
<%= render "panel" unless @game.active? %>
28 |
29 |
30 | 31 | 36 | <% for sound in %w[pass resign stone2] %> 37 | 41 | <% end %> 42 | -------------------------------------------------------------------------------- /app/views/games/update.js.erb: -------------------------------------------------------------------------------- 1 | $("#panel_container").html("<%= escape_javascript render("panel") %>"); -------------------------------------------------------------------------------- /app/views/games/your.js.erb: -------------------------------------------------------------------------------- 1 | $("#your_games").replaceWith("<%= escape_javascript(render("your_games")) %>"); 2 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= content_for?(:title) ? yield(:title) : "" %> - Go vs Go 5 | <%= stylesheet_link_tag "application" %> 6 | <%= javascript_include_tag :defaults %> 7 | <%= csrf_meta_tag %> 8 | <%= yield(:head) %> 9 | 10 | 11 | 12 | 26 |
27 | <% flash.each do |name, msg| %> 28 | <%= content_tag :div, msg, :id => "flash_#{name}" %> 29 | <% end %> 30 | <%= yield %> 31 | <% unless @hide_footer %> 32 | 40 | <% end %> 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/views/messages/_message.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
<%= relative_time(message.created_at) %>
3 | <%= message.user.username %>: <%= message.content %> 4 |
5 | -------------------------------------------------------------------------------- /app/views/messages/create.js.erb: -------------------------------------------------------------------------------- 1 | <% if @message.persisted? %> 2 | $("#messages").append("<%= escape_javascript(render(@message)) %>"); 3 | $("#messages")[0].scrollTop = $("#messages")[0].scrollHeight; 4 | $("#new_message")[0].reset(); 5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/moves/_update_moves.js.erb: -------------------------------------------------------------------------------- 1 | <% if @game.finished? %> 2 | $("#board").attr("data-finished", "true"); 3 | <% end %> 4 | addMoves("<%= escape_javascript(@game.moves_after(params[:after].to_i)) %>", "<%= @game.current_player_id.to_i %>"); 5 | $('.profile .details #score_<%= @game.black_player_id.to_i %>').html("<%= escape_javascript(game_score(@game.black_score, @game.finished?).to_s) %>"); 6 | $('.profile .details #score_<%= @game.white_player_id.to_i %>').html("<%= escape_javascript(game_score(@game.white_score, @game.finished?).to_s) %>"); 7 | <% if @game.finished? %> 8 | $("#panel_container").html("<%= escape_javascript render("games/panel") %>"); 9 | $("#player_actions").hide(); 10 | <% else %> 11 | startPolling(); 12 | <% end %> 13 | -------------------------------------------------------------------------------- /app/views/moves/create.js.erb: -------------------------------------------------------------------------------- 1 | <% if flash[:alert] %> 2 | alert("<%= escape_javascript(flash.delete(:alert)) %>"); 3 | <% else %> 4 | <%= render "update_moves" %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/moves/index.js.erb: -------------------------------------------------------------------------------- 1 | <% if @moves.blank? %> 2 | setTimeout(pollMoves, pollTimer); 3 | <% else %> 4 | <%= render "update_moves" %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/notifications/invitation.text.erb: -------------------------------------------------------------------------------- 1 | Hi <%= @game.current_player.username %>, 2 | 3 | You have been challenged by <%= @game.opponent.username %> to play a game on Go vs Go. 4 | 5 | Game: <%= game_url(@game) %> 6 | 7 | This is an automated message sent from govsgo.com. 8 | 9 | -- 10 | 11 | If you no longer wish to receive notifications from Go vs Go you can unsubscribe. 12 | 13 | Unsubscribe: <%= unsubscribe_url(@game.current_player.unsubscribe_token) %> 14 | 15 | -------------------------------------------------------------------------------- /app/views/notifications/message.text.erb: -------------------------------------------------------------------------------- 1 | Hi <%= @message.recipient.username %>, 2 | 3 | <%= @message.user.username %> has sent you a message on Go vs Go: <%= @message.content %> 4 | 5 | Game: <%= game_url(@game) %> 6 | 7 | This is an automated message sent from govsgo.com. 8 | 9 | -- 10 | 11 | If you no longer wish to receive notifications from Go vs Go you can unsubscribe. 12 | 13 | Unsubscribe: <%= unsubscribe_url(@game.current_player.unsubscribe_token) %> 14 | -------------------------------------------------------------------------------- /app/views/notifications/move.text.erb: -------------------------------------------------------------------------------- 1 | Hi <%= @game.current_player.username %>, 2 | 3 | A move has been made on your game against <%= @game.opponent.username %> on Go vs Go. 4 | 5 | Game: <%= game_url(@game) %> 6 | 7 | This is an automated message sent from govsgo.com. 8 | 9 | -- 10 | 11 | If you no longer wish to receive notifications from Go vs Go you can unsubscribe. 12 | 13 | Unsubscribe: <%= unsubscribe_url(@game.current_player.unsubscribe_token) %> 14 | 15 | -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Log In" %> 2 | <% stylesheet "authentication" %> 3 | 4 |
5 |
6 |

Log In

7 |

Don't have an account? <%= link_to "Sign up!", signup_path %>

8 | <%= form_tag sessions_path do %> 9 |

10 | <%= label_tag :login, "Username or Email" %> 11 | <%= text_field_tag :login, params[:login] || params[:email] %> 12 |

13 |

14 | <%= label_tag :password %> 15 | <%= password_field_tag :password %> 16 |

17 |

<%= submit_tag "Log in" %>

18 | <% end %> 19 |
20 |
-------------------------------------------------------------------------------- /app/views/users/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for @user do |f| %> 2 | <%= f.error_messages %> 3 |

4 | <%= f.label :username %> 5 | <%= f.text_field :username %> 6 |

7 |

8 | <%= f.label :email, "Email Address" %> 9 | <%= f.text_field :email %> 10 |

11 | <% unless session[:omniauth] %> 12 |

13 | <%= f.label :password %> 14 | <%= f.password_field :password %> 15 |

16 |

17 | <%= f.label :password_confirmation, "Confirm Password" %> 18 | <%= f.password_field :password_confirmation %> 19 |

20 | <% end %> 21 |

22 | <%= f.label :rank %> 23 | <%= f.text_field :rank, :size => 10 %> 24 | such as "4k"
25 | leave blank if unknown 26 |

27 |

28 | <%= f.label :time_zone, "Time Zone" %> 29 | <%= f.time_zone_select :time_zone, nil, :include_blank => true %> 30 |

31 |

32 | <%= f.check_box :email_on_invitation %> 33 | <%= f.label :email_on_invitation, "Email me new game invitations", :class => "inline" %> 34 |

35 |

36 | <%= f.check_box :email_on_move %> 37 | <%= f.label :email_on_move, "Email me when opponent moves", :class => "inline" %> 38 |

39 |

40 | <%= f.check_box :email_on_message %> 41 | <%= f.label :email_on_message, "Email me when opponent chats", :class => "inline" %> 42 |

43 |

<%= f.submit (@user.new_record? ? "Sign up" : "Update") %>

44 | <% end %> 45 | -------------------------------------------------------------------------------- /app/views/users/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Update Profile" %> 2 | <% stylesheet "authentication" %> 3 | 4 |
5 |
6 |

Update Profile

7 | <%= render 'form' %> 8 |
9 | 10 |
11 |

Sign In Options

12 | <% unless current_user.authentications.empty? %> 13 |

You have signed in with these services:

14 |
15 | <% for authentication in current_user.authentications %> 16 |
17 | <%= image_tag "auth/#{authentication.provider}_32.png", :size => "32x32" %> 18 |
<%= authentication.provider_name %>
19 |
<%= authentication.uid %>
20 | <%= link_to "X", authentication, :confirm => 'Are you sure you want to remove this authentication option?', :method => :delete, :class => "remove" %> 21 |
22 | <% end %> 23 |
24 | <% end %> 25 |

Add another service to sign in with:

26 | 27 | 28 | <%= image_tag "auth/twitter_64.png", :size => "64x64", :alt => "Twitter" %> 29 | Twitter 30 | 31 | 32 | <%= image_tag "auth/facebook_64.png", :size => "64x64", :alt => "Facebook" %> 33 | Facebook 34 | 35 | 36 | <%= image_tag "auth/open_id_64.png", :size => "64x64", :alt => "OpenID" %> 37 | OpenID 38 | 39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /app/views/users/new.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Sign Up" %> 2 | <% stylesheet "authentication" %> 3 | 4 |
5 |
6 |

Sign Up

7 |

Already have an account? <%= link_to "Log in", login_path %>.

8 | <%= render 'form' %> 9 |
10 |
11 | -------------------------------------------------------------------------------- /app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 | <% title @user.username %> 2 | <% stylesheet "user" %> 3 | 4 |
5 |
6 | <%= avatar_image_tag(@user) %> 7 |

<%= @user.username %>

8 |
9 |
10 |

11 | Currently: <%= @user.online? ? "Online" : "Offline" %> 12 |

13 | <% if @user.rank.present? %> 14 |

15 | Go Rank: <%= @user.rank %> 16 |

17 | <% end %> 18 |

19 | GNU Go Auto-level: 20 | <%= @user.gnugo_level %> 21 |

22 |

23 | Games with GNU Go: 24 | <%= @user.games.with_gnugo.size %> 25 |

26 |

27 | Games with Others: 28 | <%= @user.games.without_gnugo.size %> 29 |

30 |
31 | <% if @user == current_user %> 32 |

<%= link_to "Edit Profile", edit_current_user_path %> or <%= link_to "Log Out", logout_path %>

33 | <% else %> 34 | <%= link_to "Challenge to a Game", new_game_path(:username => @user.username), :class => "button" %> 35 | <% end %> 36 |
37 |
38 |

<%= @user.username %>'s Games

39 | <% @games.each_with_index do |game, index| %> 40 | <% unless index.zero? %> 41 |
42 | <% end %> 43 | <%= render game %> 44 | <% end %> 45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | Autotest.add_discovery { "rails" } 2 | Autotest.add_discovery { "rspec2" } 3 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Govsgo::Application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | PRIVATE_CONFIG = YAML.load_file(File.expand_path('../private.yml', __FILE__)) 6 | 7 | # If you have a Gemfile, require the gems listed there, including any gems 8 | # you've limited to :test, :development, or :production. 9 | Bundler.require(:default, Rails.env) if defined?(Bundler) 10 | 11 | module Govsgo 12 | class Application < Rails::Application 13 | config.autoload_paths << "#{config.root}/lib" 14 | 15 | # Settings in config/environments/* take precedence over those specified here. 16 | # Application configuration should go into files in config/initializers 17 | # -- all .rb files in that directory are automatically loaded. 18 | 19 | # Custom directories with classes and modules you want to be autoloadable. 20 | # config.autoload_paths += %W(#{config.root}/extras) 21 | 22 | # Only load the plugins named here, in the order given (default is alphabetical). 23 | # :all can be used as a placeholder for all plugins not explicitly named. 24 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 25 | 26 | # Activate observers that should always be running. 27 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 28 | 29 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 30 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 31 | # config.time_zone = 'Central Time (US & Canada)' 32 | 33 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 34 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 35 | # config.i18n.default_locale = :de 36 | 37 | # JavaScript files you want as :defaults (application.js is always included). 38 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 39 | 40 | # Configure the default encoding used in templates for Ruby 1.9. 41 | config.encoding = "utf-8" 42 | 43 | # Configure sensitive parameters which will be filtered from the log file. 44 | config.filter_parameters += [:password] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | gemfile = File.expand_path('../../Gemfile', __FILE__) 5 | begin 6 | ENV['BUNDLE_GEMFILE'] = gemfile 7 | require 'bundler' 8 | Bundler.setup 9 | rescue Bundler::GemNotFound => e 10 | STDERR.puts e.message 11 | STDERR.puts "Try running `bundle install`." 12 | exit! 13 | end if File.exist?(gemfile) 14 | -------------------------------------------------------------------------------- /config/database.example.yml: -------------------------------------------------------------------------------- 1 | # MySQL. Versions 4.1 and 5.0 are recommended. 2 | # 3 | # Install the MySQL driver: 4 | # gem install mysql2 5 | # 6 | # And be sure to use new-style password hashing: 7 | # http://dev.mysql.com/doc/refman/5.0/en/old-client.html 8 | development: 9 | adapter: mysql2 10 | encoding: utf8 11 | reconnect: false 12 | database: govsgo_development 13 | pool: 5 14 | username: root 15 | password: 16 | host: localhost 17 | 18 | # Warning: The database defined as "test" will be erased and 19 | # re-generated from your development database when you run "rake". 20 | # Do not set this db to the same as development or production. 21 | test: 22 | adapter: mysql2 23 | encoding: utf8 24 | reconnect: false 25 | database: govsgo_test 26 | pool: 5 27 | username: root 28 | password: 29 | host: localhost 30 | 31 | production: 32 | adapter: mysql2 33 | encoding: utf8 34 | reconnect: false 35 | database: govsgo_production 36 | pool: 5 37 | username: root 38 | password: 39 | host: localhost 40 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | require "bundler/capistrano" 2 | 3 | set :whenever_command, "bundle exec whenever" 4 | require "whenever/capistrano" 5 | 6 | default_run_options[:pty] = true 7 | 8 | set :application, "govsgo.com" 9 | role :app, application 10 | role :web, application 11 | role :db, application, :primary => true 12 | 13 | set :user, "rbates" 14 | set :deploy_to, "/var/apps/govsgo" 15 | set :deploy_via, :remote_cache 16 | set :use_sudo, false 17 | 18 | set :scm, "git" 19 | set :repository, "git://github.com/ryanb/govsgo.git" 20 | set :branch, "master" 21 | 22 | namespace :deploy do 23 | desc "Tell Passenger to restart." 24 | task :restart, :roles => :web do 25 | run "touch #{deploy_to}/current/tmp/restart.txt" 26 | end 27 | 28 | desc "Do nothing on startup so we don't get a script/spin error." 29 | task :start do 30 | puts "You may need to restart Apache." 31 | end 32 | 33 | desc "Symlink extra configs and folders." 34 | task :symlink_extras do 35 | run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml" 36 | run "ln -nfs #{shared_path}/config/private.yml #{release_path}/config/private.yml" 37 | run "ln -nfs #{shared_path}/assets #{release_path}/public/assets" 38 | end 39 | 40 | desc "Setup shared directory." 41 | task :setup_shared do 42 | run "mkdir #{shared_path}/config" 43 | put File.read("config/database.example.yml"), "#{shared_path}/config/database.yml" 44 | put File.read("config/private.example.yml"), "#{shared_path}/config/private.yml" 45 | puts "Now edit the config files in #{shared_path}." 46 | end 47 | 48 | desc "Make sure there is something to deploy" 49 | task :check_revision, :roles => :web do 50 | unless `git rev-parse HEAD` == `git rev-parse origin/master` 51 | puts "WARNING: HEAD is not the same as origin/master" 52 | puts "Run `git push` to sync changes." 53 | exit 54 | end 55 | end 56 | 57 | namespace :beanstalk do 58 | desc "Start the beanstalkd queue server" 59 | task :start do 60 | sudo "/etc/init.d/beanstalkd start" 61 | end 62 | 63 | desc "Stop the beanstalkd queue server" 64 | task :stop do 65 | sudo "/etc/init.d/beanstalkd stop" 66 | end 67 | 68 | desc "Start workers" 69 | task :start_workers do 70 | run "god start govsgo-worker" 71 | end 72 | 73 | desc "Stop workers" 74 | task :stop_workers do 75 | run "god stop govsgo-worker" 76 | end 77 | 78 | desc "Restart workers" 79 | task :restart_workers do 80 | run "god restart govsgo-worker" 81 | end 82 | end 83 | end 84 | 85 | before "deploy", "deploy:check_revision" 86 | after "deploy", "deploy:cleanup" # keeps only last 5 releases 87 | after "deploy:setup", "deploy:setup_shared" 88 | after "deploy:update_code", "deploy:symlink_extras" 89 | after "deploy:restart", "deploy:beanstalk:restart_workers" 90 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Govsgo::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Govsgo::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the webserver when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_view.debug_rjs = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Don't care if the mailer can't send 18 | config.action_mailer.raise_delivery_errors = false 19 | 20 | # Print deprecation notices to the Rails logger 21 | config.active_support.deprecation = :log 22 | 23 | # Only use best-standards-support built into browsers 24 | config.action_dispatch.best_standards_support = :builtin 25 | 26 | config.action_mailer.default_url_options = { :host => "localhost:3000" } 27 | end 28 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Govsgo::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # The production environment is meant for finished, "live" apps. 5 | # Code is not reloaded between requests 6 | config.cache_classes = true 7 | 8 | # Full error reports are disabled and caching is turned on 9 | config.consider_all_requests_local = false 10 | config.action_controller.perform_caching = true 11 | 12 | # Specifies the header that your server uses for sending files 13 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 14 | 15 | # For nginx: 16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 17 | 18 | # If you have no front-end server that supports something like X-Sendfile, 19 | # just comment this out and Rails will serve the files 20 | 21 | # See everything in the log (default is :info) 22 | # config.log_level = :debug 23 | 24 | # Use a different logger for distributed setups 25 | # config.logger = SyslogLogger.new 26 | 27 | # Use a different cache store in production 28 | # config.cache_store = :mem_cache_store 29 | 30 | # Disable Rails's static asset server 31 | # In production, Apache or nginx will already do this 32 | config.serve_static_assets = false 33 | 34 | # Enable serving of images, stylesheets, and javascripts from an asset server 35 | # config.action_controller.asset_host = "http://assets.example.com" 36 | 37 | # Disable delivery errors, bad email addresses will be ignored 38 | # config.action_mailer.raise_delivery_errors = false 39 | 40 | # Enable threaded mode 41 | # config.threadsafe! 42 | 43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 44 | # the I18n.default_locale when a translation can not be found) 45 | config.i18n.fallbacks = true 46 | 47 | # Send deprecation notices to registered listeners 48 | config.active_support.deprecation = :notify 49 | 50 | config.action_mailer.delivery_method = :sendmail 51 | config.action_mailer.default_url_options = { :host => "govsgo.com" } 52 | config.middleware.use ExceptionNotifier, :email_prefix => "[ERROR] ", :sender_address => 'noreply@govsgo.com', :exception_recipients => "ryan@govsgo.com" 53 | end 54 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Govsgo::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Log error messages when you accidentally call methods on nil. 11 | config.whiny_nils = true 12 | 13 | # Show full error reports and disable caching 14 | config.consider_all_requests_local = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Raise exceptions instead of rendering exception templates 18 | config.action_dispatch.show_exceptions = false 19 | 20 | # Disable request forgery protection in test environment 21 | config.action_controller.allow_forgery_protection = false 22 | 23 | # Tell Action Mailer not to deliver emails to the real world. 24 | # The :test delivery method accumulates sent emails in the 25 | # ActionMailer::Base.deliveries array. 26 | config.action_mailer.delivery_method = :test 27 | 28 | # Use SQL instead of Active Record's schema dumper when creating the test database. 29 | # This is necessary if your schema can't be completely dumped by the schema dumper, 30 | # like if you have constraints or database-specific column types 31 | # config.active_record.schema_format = :sql 32 | 33 | # Print deprecation notices to the stderr 34 | config.active_support.deprecation = :stderr 35 | 36 | config.action_mailer.default_url_options = { :host => "test.local" } 37 | end 38 | -------------------------------------------------------------------------------- /config/god.rb: -------------------------------------------------------------------------------- 1 | # run with: god -c config/god.rb 2 | RAILS_ROOT = File.expand_path("../..", __FILE__) 3 | 4 | God.watch do |w| 5 | w.name = "govsgo-worker" 6 | w.interval = 30.seconds 7 | w.env = {"RAILS_ENV" => "production"} 8 | w.start = "#{RAILS_ROOT}/script/worker" 9 | w.log = "#{RAILS_ROOT}/log/worker.log" 10 | 11 | w.start_if do |start| 12 | start.condition(:process_running) do |c| 13 | c.running = false 14 | end 15 | end 16 | 17 | w.restart_if do |restart| 18 | restart.condition(:memory_usage) do |c| 19 | c.above = 50.megabytes 20 | c.times = [3, 5] # 3 out of 5 intervals 21 | end 22 | 23 | restart.condition(:cpu_usage) do |c| 24 | c.above = 50.percent 25 | c.times = 5 26 | end 27 | end 28 | 29 | w.lifecycle do |on| 30 | on.condition(:flapping) do |c| 31 | c.to_state = [:start, :restart] 32 | c.times = 5 33 | c.within = 5.minute 34 | c.transition = :unmonitored 35 | c.retry_in = 10.minutes 36 | c.retry_times = 5 37 | c.retry_within = 2.hours 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /config/initializers/omniauth.rb: -------------------------------------------------------------------------------- 1 | require 'openid/store/filesystem' 2 | Rails.application.config.middleware.use OmniAuth::Builder do 3 | provider :twitter, PRIVATE_CONFIG["twitter_key"], PRIVATE_CONFIG["twitter_secret"] 4 | provider :facebook, PRIVATE_CONFIG["facebook_id"], PRIVATE_CONFIG["facebook_secret"] 5 | provider :open_id, OpenID::Store::Filesystem.new('/tmp') 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Govsgo::Application.config.secret_token = PRIVATE_CONFIG["secret_token"] 8 | -------------------------------------------------------------------------------- /config/initializers/security_fix.rb: -------------------------------------------------------------------------------- 1 | # from https://groups.google.com/forum/?fromgroups=#!topic/rubyonrails-security/61bkgvnSGTQ 2 | # and https://groups.google.com/forum/?fromgroups=#!topic/rubyonrails-security/1h2DR63ViGo 3 | 4 | ActiveSupport::XmlMini::PARSING.delete("symbol") 5 | ActiveSupport::XmlMini::PARSING.delete("yaml") 6 | ActionDispatch::ParamsParser::DEFAULT_PARSERS.delete(Mime::YAML) 7 | ActionDispatch::ParamsParser::DEFAULT_PARSERS.delete(Mime::XML) 8 | ActiveSupport::JSON.backend = "JSONGem" 9 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Govsgo::Application.config.session_store :cookie_store, :key => '_govsgo_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rake db:sessions:create") 8 | # Govsgo::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /config/private.example.yml: -------------------------------------------------------------------------------- 1 | secret_token: "49b5946ac102d5a612db7bb5d7c9fc9b6b0c2f8a8eb7bd9f284eaaef41e382fc2fcb65af54e3eeadd6be824c1049bbdac7ee3cf8e6b48b9364512c87be303914" 2 | twitter_key: "CONSUMER_KEY" 3 | twitter_secret: "CONSUMER_SECRET" 4 | facebook_id: "APP_ID" 5 | facebook_secret: "APP_SECRET" 6 | background_process: false 7 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Govsgo::Application.routes.draw do 2 | resources :messages 3 | 4 | match 'auth/:provider/callback' => 'authentications#create' 5 | match 'user/edit' => 'users#edit', :as => :edit_current_user 6 | match 'signin' => 'authentications#index', :as => :signin 7 | match 'signup' => 'users#new', :as => :signup 8 | match 'logout' => 'sessions#destroy', :as => :logout 9 | match 'login' => 'sessions#new', :as => :login 10 | match 'go_resources' => redirect("/resources") 11 | match 'resources' => 'games#resources', :as => :go_resources 12 | match 'games/:id.sgf' => 'games#sgf', :format => "sgf", :as => "game_sgf" 13 | match 'unsubscribe/:token' => 'users#unsubscribe', :as => "unsubscribe" 14 | match 'publicize' => 'users#publicize', :as => "publicize" 15 | resources :authentications 16 | resources :sessions 17 | resources :users 18 | resources :games do 19 | resources :moves 20 | collection do 21 | get :your 22 | get :other 23 | end 24 | end 25 | root :to => 'games#index' 26 | end 27 | -------------------------------------------------------------------------------- /config/schedule.rb: -------------------------------------------------------------------------------- 1 | set :output, "#{path}/log/cron.log" 2 | 3 | every :reboot do 4 | command "god -c #{path}/config/god.rb" 5 | end 6 | 7 | every 1.hour do 8 | rake "queue_computer_moves" # just in case some computer moves slipped through the cracks 9 | end 10 | 11 | every 1.day do 12 | rake "clear_stuck_games" # in case GNU Go gets stuck on some difficult games 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20101016000701_create_games.rb: -------------------------------------------------------------------------------- 1 | class CreateGames < ActiveRecord::Migration 2 | def self.up 3 | create_table :games do |t| 4 | t.integer :black_player_id 5 | t.integer :white_player_id 6 | t.integer :current_player_id 7 | t.float :black_score 8 | t.float :white_score 9 | t.text :black_positions 10 | t.text :white_positions 11 | t.text :moves 12 | t.text :valid_positions 13 | t.float :komi 14 | t.integer :handicap 15 | t.integer :board_size 16 | t.datetime :last_move_at 17 | t.datetime :started_at 18 | t.datetime :finished_at 19 | t.timestamps 20 | end 21 | end 22 | 23 | def self.down 24 | drop_table :games 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /db/migrate/20101016012116_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def self.up 3 | create_table :users do |t| 4 | t.string :username 5 | t.string :email 6 | t.string :password_hash 7 | t.string :password_salt 8 | t.timestamps 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :users 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20101016144720_add_guest_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddGuestToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :guest, :boolean, :default => false, :null => false 4 | end 5 | 6 | def self.down 7 | remove_column :users, :guest 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20101016203316_create_authentications.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthentications < ActiveRecord::Migration 2 | def self.up 3 | create_table :authentications do |t| 4 | t.integer :user_id 5 | t.string :provider 6 | t.string :uid 7 | t.timestamps 8 | end 9 | end 10 | 11 | def self.down 12 | drop_table :authentications 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20101017234019_add_indexes.rb: -------------------------------------------------------------------------------- 1 | class AddIndexes < ActiveRecord::Migration 2 | def self.up 3 | add_index :authentications, :user_id 4 | add_index :games, :black_player_id 5 | add_index :games, :white_player_id 6 | add_index :games, :current_player_id 7 | add_index :games, [:id, :current_player_id, :finished_at] 8 | end 9 | 10 | def self.down 11 | remove_index :authentications, :user_id 12 | remove_index :games, :black_player_id 13 | remove_index :games, :white_player_id 14 | remove_index :games, :current_player_id 15 | remove_index :games, :column => [:id, :current_player_id, :finished_at] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20101022221421_add_last_request_at_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddLastRequestAtToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :last_request_at, :datetime 4 | end 5 | 6 | def self.down 7 | remove_column :users, :last_request_at 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20101022230300_add_rank_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddRankToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :rank, :string 4 | end 5 | 6 | def self.down 7 | remove_column :users, :rank 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20101023213811_remove_valid_positions_from_games.rb: -------------------------------------------------------------------------------- 1 | class RemoveValidPositionsFromGames < ActiveRecord::Migration 2 | def self.up 3 | remove_column :games, :valid_positions 4 | end 5 | 6 | def self.down 7 | add_column :games, :valid_positions, :text 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20101028222332_add_token_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddTokenToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :token, :string 4 | add_index :users, :token 5 | end 6 | 7 | def self.down 8 | remove_index :user, :token 9 | remove_column :users, :token 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20101029233118_add_avatar_url_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAvatarUrlToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :avatar_url, :string 4 | end 5 | 6 | def self.down 7 | remove_column :users, :avatar_url 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20101215010754_add_email_options_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddEmailOptionsToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :email_on_invitation, :boolean, :default => false, :null => false 4 | add_column :users, :email_on_move, :boolean, :default => false, :null => false 5 | add_column :users, :unsubscribe_token, :string 6 | end 7 | 8 | def self.down 9 | remove_column :users, :unsubscribe_token 10 | remove_column :users, :email_on_move 11 | remove_column :users, :email_on_invitation 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20101216234423_add_publicized_at_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddPublicizedAtToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :publicized_at, :datetime 4 | end 5 | 6 | def self.down 7 | remove_column :users, :publicized_at 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20110212173010_create_messages.rb: -------------------------------------------------------------------------------- 1 | class CreateMessages < ActiveRecord::Migration 2 | def self.up 3 | create_table :messages do |t| 4 | t.integer :game_id 5 | t.integer :user_id 6 | t.text :content 7 | t.integer :move_index 8 | t.timestamps 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :messages 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20110212190716_add_time_zone_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddTimeZoneToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :time_zone, :string 4 | end 5 | 6 | def self.down 7 | remove_column :users, :time_zone 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20110212191244_add_email_on_message_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddEmailOnMessageToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :email_on_message, :boolean, :default => false, :null => false 4 | end 5 | 6 | def self.down 7 | remove_column :users, :email_on_message 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) 7 | # Mayor.create(:name => 'Daley', :city => cities.first) 8 | -------------------------------------------------------------------------------- /doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. 3 | -------------------------------------------------------------------------------- /lib/controller_authentication.rb: -------------------------------------------------------------------------------- 1 | # This module is included in your application controller which makes 2 | # several methods available to all controllers and views. Here's a 3 | # common example you might add to your application layout file. 4 | # 5 | # <% if logged_in? %> 6 | # Welcome <%= current_user.username %>. 7 | # <%= link_to "Edit profile", edit_user_path %> or 8 | # <%= link_to "Log out", logout_path %> 9 | # <% else %> 10 | # <%= link_to "Sign up", signup_path %> or 11 | # <%= link_to "log in", login_path %>. 12 | # <% end %> 13 | # 14 | # You can also restrict unregistered users from accessing a controller using 15 | # a before filter. For example. 16 | # 17 | # before_filter :login_required, :except => [:index, :show] 18 | module ControllerAuthentication 19 | def self.included(controller) 20 | controller.send :helper_method, :current_user, :logged_in?, :guest?, :redirect_to_target_or_default 21 | end 22 | 23 | def current_user 24 | @current_user ||= fetch_current_user 25 | end 26 | 27 | def fetch_current_user 28 | if session[:user_id] 29 | User.find_by_id(session[:user_id]) 30 | elsif cookies[:token] 31 | User.find_by_token(cookies[:token]) 32 | end 33 | end 34 | 35 | def current_user_or_guest 36 | unless logged_in? 37 | @current_user = User.create!(:guest => true) 38 | remember_user(@current_user) 39 | end 40 | current_user 41 | end 42 | 43 | def logged_in? 44 | current_user 45 | end 46 | 47 | def guest? 48 | current_user.nil? || current_user.guest? 49 | end 50 | 51 | def login_required(message = "You must first log in or sign up before accessing this page.") 52 | unless logged_in? 53 | flash[:alert] = message 54 | store_target_location 55 | redirect_to signin_url 56 | end 57 | end 58 | 59 | def user_required(message = "You must first log in or sign up before accessing this page.") 60 | if guest? 61 | flash[:alert] = message 62 | store_target_location 63 | redirect_to signin_url 64 | end 65 | end 66 | 67 | def redirect_to_target_or_default(default) 68 | redirect_to(session[:return_to] || default) 69 | session[:return_to] = nil 70 | end 71 | 72 | def remember_user(user) 73 | cookies.permanent[:token] = user.token 74 | end 75 | 76 | def forget_user 77 | session[:user_id] = nil 78 | cookies.delete(:token) 79 | end 80 | 81 | private 82 | 83 | def store_target_location 84 | session[:return_to] = request.url 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/game_engine.rb: -------------------------------------------------------------------------------- 1 | class GameEngine 2 | class Error < StandardError; end 3 | class IllegalMove < Error; end 4 | class OutOfTurn < Error; end 5 | 6 | def self.run(options = {}) 7 | Go::GTP.run_gnugo do |gtp| 8 | gtp.boardsize(options[:board_size]) unless options[:board_size].to_s.empty? 9 | gtp.fixed_handicap(options[:handicap]) if options[:handicap].to_i.nonzero? 10 | gtp.komi(options[:komi]) unless options[:komi].to_s.empty? 11 | yield GameEngine.new(gtp, options) 12 | end 13 | end 14 | 15 | def self.update_game_attributes_with_move(game, move = nil) 16 | update = {} 17 | run(game) do |engine| 18 | update.merge!(engine.update_game_attributes_with_move(game, move)) 19 | end 20 | update 21 | end 22 | 23 | def initialize(gtp, options = {}) 24 | @resigned = nil 25 | @gtp = gtp 26 | @board_size = options[:board_size] || 19 27 | @handicap = options[:handicap].to_i 28 | @current_color = @handicap > 0 ? :white : :black 29 | end 30 | 31 | def replay(moves) 32 | moves.to_s.split("-").each do |move| 33 | play(move =~ /[A-Z]/ ? move : move[0..1]) 34 | end 35 | end 36 | 37 | # Play at the given position, nil for computer play 38 | def play(position = nil) 39 | if position.nil? 40 | position = sgf_point(@gtp.genmove(@current_color)) 41 | else 42 | @gtp.play(@current_color, gnugo_point(position)) unless position == "RESIGN" 43 | raise IllegalMove unless @gtp.success? 44 | end 45 | if position == "RESIGN" 46 | @resigned = @current_color 47 | end 48 | @current_color = opposite_color 49 | position 50 | end 51 | 52 | # Play the move and include the captured stones afterwards 53 | def move(position = nil) 54 | raise IllegalMove if over? 55 | if %w[PASS RESIGN].include? position 56 | play(position) 57 | else 58 | other_color = opposite_color 59 | other_stones = @gtp.list_stones(other_color) 60 | position = play(position) 61 | captured = other_stones - @gtp.list_stones(other_color) 62 | position + captured.map { |v| sgf_point(v) }.join 63 | end 64 | end 65 | 66 | def update_game_attributes_with_move(game, move = nil) 67 | update = {} 68 | replay(game[:moves]) 69 | update[:moves] = [game[:moves].to_s, move(move)].reject(&:empty?).join("-") 70 | update[:last_move_at] = Time.now 71 | update[:black_positions] = positions(:black) 72 | update[:white_positions] = positions(:white) 73 | update[:current_player_id] = game["#{@current_color}_player_id".to_sym] 74 | if over? 75 | update[:finished_at] = Time.now 76 | update[:black_score] = score(:black) 77 | update[:white_score] = score(:white) 78 | else 79 | update[:black_score] = captures(:black) 80 | update[:white_score] = captures(:white) 81 | end 82 | update 83 | end 84 | 85 | def positions(color) 86 | @gtp.list_stones(color).map { |v| sgf_point(v) }.join 87 | end 88 | 89 | def captures(color) 90 | @gtp.captures(color) 91 | end 92 | 93 | def over? 94 | @resigned || @gtp.over? 95 | end 96 | 97 | def score(color) 98 | if @resigned 99 | @resigned.to_sym == color.to_sym ? 0 : 1 100 | else 101 | @gtp.final_score[/^#{color.to_s[0].upcase}\+([\d\.]+)$/, 1].to_f 102 | end 103 | end 104 | 105 | private 106 | 107 | def opposite_color 108 | @current_color == :black ? :white : :black 109 | end 110 | 111 | def point(position) 112 | args = [position] 113 | if position =~ /\A[A-HJ-T](?:1\d|[1-9])\z/ 114 | args << {:board_size => @board_size} 115 | end 116 | Go::GTP::Point.new(*args) 117 | end 118 | 119 | def gnugo_point(position) 120 | if %w[PASS RESIGN].include? position.to_s.upcase 121 | position.to_s.upcase 122 | else 123 | point(position).to_gnugo(@board_size) 124 | end 125 | end 126 | 127 | def sgf_point(position) 128 | if %w[PASS RESIGN].include? position.to_s.upcase 129 | position.to_s.upcase 130 | else 131 | point(position).to_sgf 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/game_thumb.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | module GameThumb 4 | IMAGE_DIR = File.join(File.dirname(__FILE__), *%w[.. public images thumbnail]) 5 | THUMB_DIR = File.join(File.dirname(__FILE__), *%w[.. public assets games thumbs]) 6 | 7 | module_function 8 | 9 | def generate(id, size, black_positions, white_positions) 10 | images = File.join(IMAGE_DIR, size.to_s) 11 | board = ChunkyPNG::Image.from_file(File.join(images, "board.png")) 12 | black = ChunkyPNG::Image.from_file(File.join(images, "black_stone.png")) 13 | white = ChunkyPNG::Image.from_file(File.join(images, "white_stone.png")) 14 | offset = 76 / size.to_f 15 | 16 | add_stones(board, black, black_positions, offset) 17 | add_stones(board, white, white_positions, offset) 18 | 19 | FileUtils.mkdir_p(THUMB_DIR) 20 | thumb = File.join(THUMB_DIR, "#{id}.png") 21 | board.save("#{thumb}~", :fast_rgba) 22 | FileUtils.mv("#{thumb}~", thumb) 23 | rescue ChunkyPNG::Exception => e # Mainly to catch errors in earlier games with bad data 24 | puts "Unable to generate image for game #{id}: #{e.message}" 25 | end 26 | 27 | def add_stones(board, stone, positions, offset) 28 | positions.to_s.scan(/[a-s]{2}/).each do |position| 29 | x, y = Go::GTP::Point.new(position).to_indices 30 | board.compose(stone, 2 + (x * offset).round, 2 + (y * offset).round) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/lib/tasks/.gitkeep -------------------------------------------------------------------------------- /lib/tasks/application.rake: -------------------------------------------------------------------------------- 1 | desc "Requeue all games waiting on a computer move" 2 | task :queue_computer_moves => :environment do 3 | Game.where("current_player_id IS NULL AND finished_at IS NULL").find_each do |game| 4 | game.queue_computer_move 5 | end 6 | end 7 | 8 | desc "Regenerate thumbnails for games between FROM and TO id" 9 | task :generate_thumbnails => :environment do 10 | from_id = ENV["FROM"].to_i 11 | to_id = ENV["TO"] ? ENV["TO"].to_i : Game.last.id 12 | Game.where(:id => from_id..to_id).find_each do |game| 13 | puts "Generating thumbnail for game #{game.id}" 14 | game.update_thumbnail 15 | end 16 | end 17 | 18 | desc "Fix the old move syntax by inserting PASS and RESIGN where appropriate" 19 | task :fix_moves => :environment do 20 | from_id = ENV["FROM"].to_i 21 | to_id = ENV["TO"] ? ENV["TO"].to_i : Game.last.id 22 | Game.where(:id => from_id..to_id).find_each do |game| 23 | moves = game.moves.to_s.dup 24 | moves.gsub!("--", "-PASS-") 25 | moves.sub!(/\-$/, "-PASS") 26 | moves.sub!(/^\-/, "PASS-") 27 | moves.sub!(/\-[a-z]{2}$/, "\\0-RESIGN") if game.finished? 28 | if moves != game.moves 29 | puts "GAME #{game.id}" 30 | puts "Old #{game.moves}" 31 | puts "New #{moves}" 32 | game.update_attribute(:moves, moves) 33 | end 34 | end 35 | end 36 | 37 | desc "Generate tokens for users who don't have them" 38 | task :user_tokens => :environment do 39 | User.where(:token => nil).find_each do |user| 40 | user.generate_token 41 | user.save(:validate => false) 42 | end 43 | end 44 | 45 | desc "Set the started_at time for all existing games" 46 | task :fix_started_at => :environment do 47 | Game.find_each do |game| 48 | game.update_attribute(:started_at, game.created_at) 49 | end 50 | end 51 | 52 | desc "Set the unsubscribe_token for existing users" 53 | task :fix_unsubscribe_token => :environment do 54 | User.where(:unsubscribe_token => nil).find_each do |user| 55 | user.email_on_invitation = true 56 | user.generate_token(:unsubscribe_token) 57 | user.save! 58 | end 59 | end 60 | 61 | # Do this because GNU Go can become stuck on difficult moves 62 | desc "Clear stuck games" 63 | task :clear_stuck_games => :environment do 64 | 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) 65 | end 66 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Go vs Go: Not Found 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22 |
23 | 24 |
25 |
26 |

Not Found (404)

27 |

The Go stones you were looking for do not exist. You may have mistyped the address or the page may have moved.

28 |
29 |
30 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Go vs Go: Error 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22 |
23 | 24 |
25 |
26 |

Change Rejected (422)

27 |

The change you wanted was rejected. Maybe you tried to change something you didn't have access to.

28 |
29 |
30 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Go vs Go: Error 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22 |
23 | 24 |
25 |
26 |

Error Occurred (500)

27 |

We're sorry, but something went wrong. Please try again later.

28 |
29 |
30 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/favicon.ico -------------------------------------------------------------------------------- /public/images/auth/facebook_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/auth/facebook_128.png -------------------------------------------------------------------------------- /public/images/auth/facebook_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/auth/facebook_32.png -------------------------------------------------------------------------------- /public/images/auth/facebook_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/auth/facebook_64.png -------------------------------------------------------------------------------- /public/images/auth/open_id_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/auth/open_id_128.png -------------------------------------------------------------------------------- /public/images/auth/open_id_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/auth/open_id_32.png -------------------------------------------------------------------------------- /public/images/auth/open_id_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/auth/open_id_64.png -------------------------------------------------------------------------------- /public/images/auth/twitter_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/auth/twitter_128.png -------------------------------------------------------------------------------- /public/images/auth/twitter_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/auth/twitter_32.png -------------------------------------------------------------------------------- /public/images/auth/twitter_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/auth/twitter_64.png -------------------------------------------------------------------------------- /public/images/avatars/gnugo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/avatars/gnugo.png -------------------------------------------------------------------------------- /public/images/avatars/guest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/avatars/guest.png -------------------------------------------------------------------------------- /public/images/game/13/black_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/13/black_stone.png -------------------------------------------------------------------------------- /public/images/game/13/board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/13/board.png -------------------------------------------------------------------------------- /public/images/game/13/last_black_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/13/last_black_stone.png -------------------------------------------------------------------------------- /public/images/game/13/last_white_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/13/last_white_stone.png -------------------------------------------------------------------------------- /public/images/game/13/white_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/13/white_stone.png -------------------------------------------------------------------------------- /public/images/game/19/black_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/19/black_stone.png -------------------------------------------------------------------------------- /public/images/game/19/board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/19/board.png -------------------------------------------------------------------------------- /public/images/game/19/last_black_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/19/last_black_stone.png -------------------------------------------------------------------------------- /public/images/game/19/last_white_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/19/last_white_stone.png -------------------------------------------------------------------------------- /public/images/game/19/white_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/19/white_stone.png -------------------------------------------------------------------------------- /public/images/game/9/black_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/9/black_stone.png -------------------------------------------------------------------------------- /public/images/game/9/board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/9/board.png -------------------------------------------------------------------------------- /public/images/game/9/last_black_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/9/last_black_stone.png -------------------------------------------------------------------------------- /public/images/game/9/last_white_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/9/last_white_stone.png -------------------------------------------------------------------------------- /public/images/game/9/white_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/9/white_stone.png -------------------------------------------------------------------------------- /public/images/game/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/background.png -------------------------------------------------------------------------------- /public/images/game/black_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/black_overlay.png -------------------------------------------------------------------------------- /public/images/game/bowl_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/bowl_black.png -------------------------------------------------------------------------------- /public/images/game/bowl_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/bowl_white.png -------------------------------------------------------------------------------- /public/images/game/clear_white_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/clear_white_overlay.png -------------------------------------------------------------------------------- /public/images/game/go_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/go_button.png -------------------------------------------------------------------------------- /public/images/game/lid_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/lid_black.png -------------------------------------------------------------------------------- /public/images/game/lid_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/lid_white.png -------------------------------------------------------------------------------- /public/images/game/small_black_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/small_black_stone.png -------------------------------------------------------------------------------- /public/images/game/small_white_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/small_white_stone.png -------------------------------------------------------------------------------- /public/images/game/solid_white_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/solid_white_overlay.png -------------------------------------------------------------------------------- /public/images/game/sound_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/sound_off.png -------------------------------------------------------------------------------- /public/images/game/sound_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/game/sound_on.png -------------------------------------------------------------------------------- /public/images/layout/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/layout/background.png -------------------------------------------------------------------------------- /public/images/layout/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/layout/facebook.png -------------------------------------------------------------------------------- /public/images/layout/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/layout/logo.png -------------------------------------------------------------------------------- /public/images/layout/right_triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/layout/right_triangle.png -------------------------------------------------------------------------------- /public/images/layout/three_stones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/layout/three_stones.png -------------------------------------------------------------------------------- /public/images/layout/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/layout/twitter.png -------------------------------------------------------------------------------- /public/images/layout/two_stones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/layout/two_stones.png -------------------------------------------------------------------------------- /public/images/screencast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/screencast.png -------------------------------------------------------------------------------- /public/images/thumbnail/13/black_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/thumbnail/13/black_stone.png -------------------------------------------------------------------------------- /public/images/thumbnail/13/board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/thumbnail/13/board.png -------------------------------------------------------------------------------- /public/images/thumbnail/13/white_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/thumbnail/13/white_stone.png -------------------------------------------------------------------------------- /public/images/thumbnail/19/black_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/thumbnail/19/black_stone.png -------------------------------------------------------------------------------- /public/images/thumbnail/19/board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/thumbnail/19/board.png -------------------------------------------------------------------------------- /public/images/thumbnail/19/white_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/thumbnail/19/white_stone.png -------------------------------------------------------------------------------- /public/images/thumbnail/9/black_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/thumbnail/9/black_stone.png -------------------------------------------------------------------------------- /public/images/thumbnail/9/board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/thumbnail/9/board.png -------------------------------------------------------------------------------- /public/images/thumbnail/9/white_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/images/thumbnail/9/white_stone.png -------------------------------------------------------------------------------- /public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | /* Fix ajax timeout so it doesn't leave request hanging, see http://code.google.com/p/phusion-passenger/issues/detail?id=419 */ 2 | jQuery.ajaxSetup({ 3 | timeout: 30000 4 | }); 5 | 6 | var moves = new Array(); 7 | var current_move = 0; 8 | var current_user = null; 9 | var current_player = null; 10 | var pollTimer = null; 11 | var soundEnabled = true; 12 | var loadedSounds = new Array(); 13 | 14 | $(function() { 15 | $(".pagination a").live("click", function() { 16 | $.getScript(this.href); 17 | return false; 18 | }); 19 | 20 | if ($("#board").length > 0) { 21 | setupGame(); 22 | } 23 | $("#game_opponent_username").focus(function() { 24 | $("#game_chosen_opponent_user").attr("checked", "checked"); 25 | $("#game_chosen_opponent_user").click(); 26 | }); 27 | $("#game_chosen_opponent_gnugo, #game_chosen_opponent_user, #game_adjust_difficulty").click(function() { 28 | if ($('#game_chosen_opponent_user').is(":checked")) { 29 | $("#new_game .auto_difficulty").hide(); 30 | $("#new_game .manual_difficulty").show(); 31 | } else { 32 | $("#new_game .auto_difficulty").show(); 33 | if ($("#game_adjust_difficulty").is(":checked")) { 34 | $("#new_game .manual_difficulty").hide(); 35 | } else { 36 | $("#new_game .manual_difficulty").show(); 37 | } 38 | } 39 | }); 40 | }); 41 | 42 | function setupGame() { 43 | updateCapturedStones("black"); 44 | updateCapturedStones("white"); 45 | if ($("#board").attr("data-moves").length != "") { 46 | moves = $("#board").attr("data-moves").split("-"); 47 | } 48 | current_move = moves.length; 49 | current_user = $("#board").attr("data-current-user"); 50 | current_player = $("#board").attr("data-current-player"); 51 | $("audio").bind("canplaythrough", function() { 52 | loadedSounds.push($(this).attr("data-name")); 53 | }, false); 54 | $("#board_spaces div").click(function() { 55 | if ($(this).hasClass("e") && current_move == moves.length && current_user == current_player && $("#board").attr("data-finished") != "true" && $("#board").attr("data-started") == "true") { 56 | playMove($(this).attr("id")); 57 | } 58 | }); 59 | $("#play_pass").click(function() { 60 | playMove("PASS"); 61 | return false; 62 | }); 63 | $("#play_resign").click(function() { 64 | if (confirm("Are you sure you want to resign?")) { 65 | playMove("RESIGN"); 66 | } 67 | return false; 68 | }); 69 | $("#previous_move").click(function() { 70 | $("#panel_container").html(""); 71 | if (current_move > 0) { 72 | stepMove(-1, false); 73 | } 74 | return false; 75 | }); 76 | $("#next_move").click(function() { 77 | $("#panel_container").html(""); 78 | if (current_move < moves.length) { 79 | stepMove(1, false); 80 | } 81 | return false; 82 | }); 83 | $("#first_move").click(function() { 84 | $("#panel_container").html(""); 85 | while (current_move > 0) { 86 | stepMove(-1, true); 87 | } 88 | return false; 89 | }); 90 | $("#last_move").click(function() { 91 | $("#panel_container").html(""); 92 | while (current_move < moves.length) { 93 | stepMove(1, true); 94 | } 95 | return false; 96 | }); 97 | $("#sound_switch").click(function() { 98 | soundEnabled = !soundEnabled; 99 | if (soundEnabled) { 100 | $("#sound_switch img").attr("src", "/images/game/sound_on.png"); 101 | } else { 102 | $("#sound_switch img").attr("src", "/images/game/sound_off.png"); 103 | } 104 | return false; 105 | }); 106 | $("#accept_button, #decline_button").live("click", function() { 107 | $('', {type:"hidden", name:"invitation_button", value:this.value}).appendTo("form.edit_game"); 108 | return true; 109 | }); 110 | $("form.edit_game").live("submit", function() { 111 | $.post($(this).attr("action"), $(this).serialize(), null, "script"); 112 | return false; 113 | }); 114 | $("form.edit_game a").live("click", function() { 115 | $.getScript(this.href); 116 | return false; 117 | }); 118 | $("#new_message").submit(function(e) { 119 | $.post($(this).attr("action"), $(this).serialize()); 120 | e.preventDefault(); 121 | }); 122 | if ($("#board").attr("data-finished") != "true" && $("#board").attr("data-started") == "true") { 123 | startPolling(); 124 | } 125 | if ($("#messages").length > 0) { 126 | $("#messages")[0].scrollTop = $("#messages")[0].scrollHeight; 127 | } 128 | } 129 | 130 | function playMove(move) { 131 | $.post(window.location.pathname + '/moves', {"move": move, "after": moves.length}, null, "script"); 132 | } 133 | 134 | function addMoves(new_moves, next_player) { 135 | $('.profile .details .turn').hide(); 136 | if ($("#board").attr("data-finished") != "true" && $("#board").attr("data-started") == "true") { 137 | $('.profile .details #turn_' + next_player).show(); 138 | } 139 | $.each(new_moves.split("-"), function(index, move) { 140 | moves.push(move); 141 | if (current_move == moves.length-1) { 142 | stepMove(1, index != new_moves.split("-").length-1); 143 | } 144 | }); 145 | current_player = next_player; 146 | } 147 | 148 | function stepMove(step, multistep) { 149 | current_move += step; 150 | var offset = $("#board").attr("data-handicap") > 0 ? 1 : 0; 151 | var color = (current_move + offset) % 2 ? "b" : "w"; 152 | 153 | // Update move by adding or removing stones based on what is matched 154 | if (step > 0) { 155 | updateStones(color, moves[current_move-1], false, multistep); 156 | } else { 157 | updateStones(color, moves[current_move], true, multistep); 158 | } 159 | 160 | // Update status for passed/resigned 161 | $("#board .last").removeClass("last"); 162 | $(".profile .status").text(""); 163 | if (moves[current_move-1] == "PASS") { 164 | $("#" + color + "_status").text("passed"); 165 | if (!multistep) { 166 | playSound("pass", 0.3); 167 | } 168 | } else if (moves[current_move-1] == "RESIGN") { 169 | $("#" + color + "_status").text("resigned"); 170 | if (!multistep) { 171 | playSound("resign", 0.3); 172 | } 173 | } else if (current_move > 0) { 174 | $("#" + moves[current_move-1].substr(0, 2)).addClass("last"); 175 | } 176 | } 177 | 178 | function updateStones(color, move, backwards, multistep) { 179 | if (move != "" && move != "PASS" && move != "RESIGN") { 180 | var capture_change = 0; 181 | var capturer = null; 182 | $.each(move.match(/../g), function(index, position) { 183 | if (index == 0) { 184 | if (!backwards && !multistep) { 185 | playSound("stone2", 0.7); 186 | } 187 | $("#" + position).attr("class", (backwards ? "e" : color)); 188 | } else { 189 | if (backwards) { 190 | capturer = (color == "b" ? "white" : "black"); 191 | capture_change -= 1; 192 | } else { 193 | capturer = (color == "b" ? "black" : "white"); 194 | capture_change += 1; 195 | } 196 | $("#" + position).attr("class", (backwards ? color : "e")); 197 | } 198 | }); 199 | if (capturer) { 200 | var $count = $("." + capturer + "_captured .count"); 201 | $count.text(parseInt($count.text())+capture_change); 202 | updateCapturedStones(capturer); 203 | } 204 | } 205 | } 206 | 207 | function startPolling() { 208 | if (current_user != current_player) { 209 | resetPollTimer(); 210 | setTimeout(pollMoves, pollTimer); 211 | } 212 | } 213 | 214 | function pollMoves() { 215 | // Slow down polling until it's 30 seconds apart 216 | if (pollTimer < 30000) { 217 | pollTimer += 1000; 218 | } 219 | $.getScript(window.location.pathname + '/moves?after=' + moves.length); 220 | } 221 | 222 | function resetPollTimer() { 223 | pollTimer = 1000; 224 | } 225 | 226 | function playSound(name, volume) { 227 | if (soundEnabled && $.inArray(name, loadedSounds) != -1) { 228 | try { 229 | var sound = $("#" + name + "_sound").get(0); 230 | sound.volume = volume; 231 | sound.currentTime = 0; 232 | sound.play(); 233 | } catch(err) { 234 | alert("There was an error attempting to play a sound. Press OK to continue. " + err); 235 | loadedSounds.splice($.inArray(name, loadedSounds), 1); 236 | } 237 | } 238 | } 239 | 240 | function updateCapturedStones(color) { 241 | var count = $("." + color + "_captured .count").text(); 242 | for (var i = 1; i <= 10; i++) { 243 | if (i <= count && $("." + color + "_captured .stone" + i).length == 0) { 244 | $("." + color + "_captured").prepend("
"); 245 | } else if (i > count) { 246 | $("." + color + "_captured .stone" + i).remove(); 247 | } 248 | } 249 | if (count > 6) { 250 | $("." + color + "_captured .info").show(); 251 | } else { 252 | $("." + color + "_captured .info").hide(); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /public/javascripts/rails.js: -------------------------------------------------------------------------------- 1 | jQuery(function ($) { 2 | var csrf_token = $('meta[name=csrf-token]').attr('content'), 3 | csrf_param = $('meta[name=csrf-param]').attr('content'); 4 | 5 | $.fn.extend({ 6 | /** 7 | * Triggers a custom event on an element and returns the event result 8 | * this is used to get around not being able to ensure callbacks are placed 9 | * at the end of the chain. 10 | * 11 | * TODO: deprecate with jQuery 1.4.2 release, in favor of subscribing to our 12 | * own events and placing ourselves at the end of the chain. 13 | */ 14 | triggerAndReturn: function (name, data) { 15 | var event = new $.Event(name); 16 | this.trigger(event, data); 17 | 18 | return event.result !== false; 19 | }, 20 | 21 | /** 22 | * Handles execution of remote calls firing overridable events along the way 23 | */ 24 | callRemote: function () { 25 | var el = this, 26 | method = el.attr('method') || el.attr('data-method') || 'GET', 27 | url = el.attr('action') || el.attr('href'), 28 | dataType = el.attr('data-type') || 'script'; 29 | 30 | if (url === undefined) { 31 | throw "No URL specified for remote call (action or href must be present)."; 32 | } else { 33 | if (el.triggerAndReturn('ajax:before')) { 34 | var data = el.is('form') ? el.serializeArray() : []; 35 | $.ajax({ 36 | url: url, 37 | data: data, 38 | dataType: dataType, 39 | type: method.toUpperCase(), 40 | beforeSend: function (xhr) { 41 | el.trigger('ajax:loading', xhr); 42 | }, 43 | success: function (data, status, xhr) { 44 | el.trigger('ajax:success', [data, status, xhr]); 45 | }, 46 | complete: function (xhr) { 47 | el.trigger('ajax:complete', xhr); 48 | }, 49 | error: function (xhr, status, error) { 50 | el.trigger('ajax:failure', [xhr, status, error]); 51 | } 52 | }); 53 | } 54 | 55 | el.trigger('ajax:after'); 56 | } 57 | } 58 | }); 59 | 60 | /** 61 | * confirmation handler 62 | */ 63 | $('a[data-confirm],input[data-confirm]').live('click', function () { 64 | var el = $(this); 65 | if (el.triggerAndReturn('confirm')) { 66 | if (!confirm(el.attr('data-confirm'))) { 67 | return false; 68 | } 69 | } 70 | }); 71 | 72 | 73 | /** 74 | * remote handlers 75 | */ 76 | $('form[data-remote]').live('submit', function (e) { 77 | $(this).callRemote(); 78 | e.preventDefault(); 79 | }); 80 | 81 | $('a[data-remote],input[data-remote]').live('click', function (e) { 82 | $(this).callRemote(); 83 | e.preventDefault(); 84 | }); 85 | 86 | $('a[data-method]:not([data-remote])').live('click', function (e){ 87 | var link = $(this), 88 | href = link.attr('href'), 89 | method = link.attr('data-method'), 90 | form = $('
'), 91 | metadata_input = ''; 92 | 93 | if (csrf_param != null && csrf_token != null) { 94 | metadata_input += ''; 95 | } 96 | 97 | form.hide() 98 | .append(metadata_input) 99 | .appendTo('body'); 100 | 101 | e.preventDefault(); 102 | form.submit(); 103 | }); 104 | 105 | /** 106 | * disable-with handlers 107 | */ 108 | var disable_with_input_selector = 'input[data-disable-with]'; 109 | var disable_with_form_remote_selector = 'form[data-remote]:has(' + disable_with_input_selector + ')'; 110 | var disable_with_form_not_remote_selector = 'form:not([data-remote]):has(' + disable_with_input_selector + ')'; 111 | 112 | var disable_with_input_function = function () { 113 | $(this).find(disable_with_input_selector).each(function () { 114 | var input = $(this); 115 | input.data('enable-with', input.val()) 116 | .attr('value', input.attr('data-disable-with')) 117 | .attr('disabled', 'disabled'); 118 | }); 119 | }; 120 | 121 | $(disable_with_form_remote_selector).live('ajax:before', disable_with_input_function); 122 | $(disable_with_form_not_remote_selector).live('submit', disable_with_input_function); 123 | 124 | $(disable_with_form_remote_selector).live('ajax:complete', function () { 125 | $(this).find(disable_with_input_selector).each(function () { 126 | var input = $(this); 127 | input.removeAttr('disabled') 128 | .val(input.data('enable-with')); 129 | }); 130 | }); 131 | 132 | }); 133 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /public/sounds/pass.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/sounds/pass.mp3 -------------------------------------------------------------------------------- /public/sounds/pass.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/sounds/pass.ogg -------------------------------------------------------------------------------- /public/sounds/resign.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/sounds/resign.mp3 -------------------------------------------------------------------------------- /public/sounds/resign.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/sounds/resign.ogg -------------------------------------------------------------------------------- /public/sounds/stone.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/sounds/stone.mp3 -------------------------------------------------------------------------------- /public/sounds/stone.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/sounds/stone.ogg -------------------------------------------------------------------------------- /public/sounds/stone2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/sounds/stone2.mp3 -------------------------------------------------------------------------------- /public/sounds/stone2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/sounds/stone2.ogg -------------------------------------------------------------------------------- /public/stylesheets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/public/stylesheets/.gitkeep -------------------------------------------------------------------------------- /public/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Verdana, Helvetica, Arial; 3 | margin: 0; 4 | padding: 0; 5 | background-color: #B3B3B6; 6 | color: #858585; 7 | font-size: 13px; 8 | background-image: url(/images/layout/background.png); 9 | } 10 | 11 | a img { 12 | border: none; 13 | } 14 | 15 | a { 16 | color: #3C50B1; 17 | } 18 | 19 | a.subtle { 20 | color: inherit; 21 | text-decoration: none; 22 | } 23 | 24 | a.subtle:hover { 25 | text-decoration: underline; 26 | } 27 | 28 | .clear { 29 | clear: both; 30 | height: 0; 31 | overflow: hidden; 32 | } 33 | 34 | 35 | /*** Notices and Errors ***/ 36 | 37 | #flash_notice, #flash_error, #flash_alert { 38 | padding: 6px 50px; 39 | margin-top: 10px; 40 | color: #FFF; 41 | } 42 | 43 | #flash_notice { 44 | background-color: #1D5222; 45 | } 46 | 47 | #flash_error, #flash_alert { 48 | background-color: #9D1A1C; 49 | } 50 | 51 | .fieldWithErrors { 52 | display: inline; 53 | } 54 | 55 | .error_messages { 56 | width: 360px; 57 | padding: 0px; 58 | padding: 15px 20px; 59 | margin-bottom: 20px; 60 | background-color: #B72128; 61 | -moz-border-radius: 8px; 62 | -webkit-border-radius: 8px; 63 | font-size: 12px; 64 | color: #FFF; 65 | } 66 | 67 | #container .error_messages h2 { 68 | text-align: left; 69 | font-weight: bold; 70 | padding: 5px 10px; 71 | font-size: 12px; 72 | margin: 0; 73 | color: #FFF; 74 | } 75 | 76 | .error_messages p { 77 | margin: 8px 10px; 78 | } 79 | 80 | .error_messages ul { 81 | margin: 0; 82 | } 83 | 84 | 85 | #error_message { 86 | padding: 80px 50px; 87 | font-size: 14px; 88 | text-align: left; 89 | width: 350px; 90 | line-height: 25px; 91 | } 92 | 93 | #error_message h2 { 94 | font-size: 22px; 95 | } 96 | 97 | 98 | /*** Logo ***/ 99 | 100 | #logo { 101 | float: left; 102 | margin-left: 70px; 103 | margin-top: 20px; 104 | margin-bottom: 10px; 105 | } 106 | 107 | 108 | 109 | /*** Navigation ***/ 110 | 111 | #nav { 112 | list-style: none; 113 | margin: 0; 114 | padding: 0; 115 | height: 100px; 116 | margin-left: 400px; 117 | width: 610px; 118 | } 119 | 120 | #nav li { 121 | float: left; 122 | } 123 | 124 | #nav li.gap { 125 | width: 8px; 126 | height: 1px; 127 | } 128 | 129 | #nav li.banner { 130 | box-shadow: 7px 7px 40px #202020; 131 | -webkit-box-shadow: 7px 7px 40px #202020; 132 | -moz-box-shadow: 7px 7px 40px #202020; 133 | filter: progid:DXImageTransform.Microsoft.dropShadow(color=#202020, offX=7, offY=7, positive=true); 134 | } 135 | 136 | #nav li a { 137 | display: block; 138 | padding-top: 70px; 139 | padding-bottom: 12px; 140 | width: 140px; 141 | text-align: center; 142 | color: #FFF; 143 | text-decoration: none; 144 | } 145 | 146 | #nav li.blue { 147 | background-color: #5A4AAC; 148 | background: -webkit-gradient(linear, left bottom, left top, from(#5A4AAC), to(#0E0A18)); 149 | background: -moz-linear-gradient(center bottom, #5A4AAC 0%, #0E0A18 100%); 150 | } 151 | 152 | #nav li.purple { 153 | background-color: #884EB1; 154 | background: -webkit-gradient(linear, left bottom, left top, from(#884EB1), to(#0C0610)); 155 | background: -moz-linear-gradient(center bottom, #884EB1 0%, #0C0610 100%); 156 | } 157 | 158 | #nav li.red { 159 | background-color: #A64F6B; 160 | background: -webkit-gradient(linear, left bottom, left top, from(#A64F6B), to(#10070B)); 161 | background: -moz-linear-gradient(center bottom, #A64F6B 0%, #10070B 100%); 162 | } 163 | 164 | #nav li.brown { 165 | background-color: #AB684F; 166 | background: -webkit-gradient(linear, left bottom, left top, from(#AB684F), to(#0F0806)); 167 | background: -moz-linear-gradient(center bottom, #AB684F 0%, #0F0806 100%); 168 | } 169 | 170 | #container { 171 | background-color: #F3F1F6; 172 | } 173 | 174 | #container.two_stones { 175 | background: #F3F1F6 url(/images/layout/two_stones.png) top right no-repeat; 176 | } 177 | 178 | #container.three_stones { 179 | background: #F3F1F6 url(/images/layout/three_stones.png) top right no-repeat; 180 | } 181 | 182 | h2, h3 { 183 | color: #979797; 184 | } 185 | 186 | #go_resources { 187 | padding: 50px 80px; 188 | font-size: 14px; 189 | text-align: left; 190 | width: 350px; 191 | line-height: 25px; 192 | } 193 | 194 | #go_resources h2 { 195 | font-size: 22px; 196 | } 197 | 198 | #go_resources ul { 199 | margin-left: 18px; 200 | padding-left: 0; 201 | } 202 | 203 | 204 | /*** Forms ***/ 205 | 206 | form label { 207 | float: left; 208 | width: 142px; 209 | text-align: right; 210 | padding-right: 8px; 211 | padding-top: 2px; 212 | } 213 | 214 | form label.inline { 215 | float: none; 216 | width: auto; 217 | text-align: left; 218 | padding: 0; 219 | } 220 | 221 | .field { 222 | margin-left: 150px; 223 | } 224 | 225 | .hint { 226 | font-size: 11px; 227 | font-style: italic; 228 | padding-left: 5px; 229 | } 230 | 231 | 232 | /*** Footer ***/ 233 | 234 | #footer { 235 | clear: both; 236 | font-size: 12px; 237 | margin-top: 25px; 238 | margin-bottom: 20px; 239 | text-align: center; 240 | color: #444; 241 | } 242 | 243 | #footer a { 244 | color: #333; 245 | } 246 | 247 | #footer img { 248 | vertical-align: bottom; 249 | padding-left: 5px; 250 | } 251 | 252 | 253 | /*** Games ***/ 254 | 255 | .column .game { 256 | position: relative; 257 | height: 80px; 258 | } 259 | 260 | .column .game .thumbnail { 261 | position: absolute; 262 | width: 80px; 263 | height: 80px; 264 | display: block; 265 | } 266 | 267 | .column .game .details { 268 | position: absolute; 269 | top: 8px; 270 | left: 92px; 271 | } 272 | 273 | .column .game .player { 274 | padding-top: 8px; 275 | padding-left: 22px; 276 | } 277 | 278 | .column .game .black { 279 | background: url(/images/game/small_black_stone.png) no-repeat 9px left; 280 | } 281 | 282 | .column .game .white { 283 | background: url(/images/game/small_white_stone.png) no-repeat 9px left; 284 | } 285 | 286 | .column .game .status { 287 | font-size: 10px; 288 | color: #888; 289 | padding-left: 22px; 290 | padding-top: 2px; 291 | } 292 | 293 | .column .item .go_button { 294 | padding: 5px 10px; 295 | text-decoration: none; 296 | font-weight: bold; 297 | color: #F2E9E4; 298 | background: -webkit-gradient( 299 | linear, 300 | left bottom, 301 | left top, 302 | color-stop(0, #3E282F), 303 | color-stop(0.25, #845765), 304 | color-stop(0.75, #9C7581), 305 | color-stop(1, #FDF1E9) 306 | ); 307 | background: -moz-linear-gradient( 308 | center bottom, 309 | #3E282F 0%, 310 | #845765 25%, 311 | #9C7581 75%, 312 | #FDF1E9 100% 313 | ); 314 | -webkit-box-shadow: 1px 2px 4px #444; 315 | -moz-box-shadow: 1px 2px 4px #444; 316 | box-shadow: 1px 2px 4px #444; 317 | } 318 | 319 | .column .game .go_button { 320 | position: absolute; 321 | right: 15px; 322 | top: 25px; 323 | } 324 | 325 | .user_offline, .user_online { 326 | padding-left: 3px; 327 | } 328 | 329 | .user_offline { 330 | color: #C61B19; 331 | } 332 | 333 | .user_online { 334 | color: #00A200; 335 | } 336 | 337 | .avatar { 338 | width: 48px; 339 | height: 48px; 340 | overflow: hidden; 341 | } 342 | 343 | 344 | #container .column h2 { 345 | font-size: 20px; 346 | background-color: #A3A4A5; 347 | color: #FFF; 348 | padding: 8px 10px; 349 | margin: 0; 350 | position: relative; 351 | } 352 | 353 | #container .column .item { 354 | background-color: #D4D4D8; 355 | margin-top: 8px; 356 | } 357 | 358 | #container .column .item .details { 359 | color: #696969; 360 | } 361 | 362 | 363 | /*** Buttons ***/ 364 | 365 | .button { 366 | text-decoration: none; 367 | padding: 5px 10px 6px 10px; 368 | border: 1px solid rgba(0,0,0,0.4); 369 | -moz-border-radius: 7px; 370 | -webkit-border-radius: 7px; 371 | text-shadow: 1px 1px 1px rgba(255,255,255,0.5); 372 | -moz-box-shadow: 0px 0px 2px rgba(0,0,0,0.5); 373 | -webkit-box-shadow: 0px 0px 2px rgba(0,0,0,0.5); 374 | color: #000; 375 | background: #D3D3D3; 376 | background: -webkit-gradient(linear, left top, left bottom, from(#F2F2F2), to(#CCC)); 377 | background: -moz-linear-gradient(top, #F2F2F2, #CCC); 378 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F2F2F2', endColorstr='#CCC'); 379 | } 380 | -------------------------------------------------------------------------------- /public/stylesheets/authentication.css: -------------------------------------------------------------------------------- 1 | #sign_up { 2 | margin-left: 100px; 3 | } 4 | 5 | #login_page { 6 | margin-left: 100px; 7 | padding-bottom: 20px; 8 | } 9 | 10 | #sign_in { 11 | padding-left: 50px; 12 | } 13 | 14 | #sign_in h2 { 15 | width: 340px; 16 | font-size: 22px; 17 | } 18 | 19 | #sign_in h3 { 20 | font-size: 18px; 21 | } 22 | 23 | 24 | /*** Update Profile ***/ 25 | 26 | #update_profile { 27 | float: left; 28 | margin-left: 50px; 29 | } 30 | 31 | #authentications { 32 | margin-left: 500px; 33 | } 34 | 35 | #container { 36 | padding: 30px 0; 37 | } 38 | 39 | 40 | 41 | /*** Authentications ***/ 42 | 43 | .authentications { 44 | margin-bottom: 30px; 45 | } 46 | 47 | .authentication { 48 | width: 240px; 49 | background-color: #A09B96; 50 | padding: 7px 15px; 51 | -moz-border-radius: 8px; 52 | -webkit-border-radius: 8px; 53 | position: relative; 54 | margin-bottom: 10px; 55 | color: #F7F7F7; 56 | } 57 | 58 | .authentication .remove { 59 | text-decoration: none; 60 | position: absolute; 61 | top: 3px; 62 | right: 3px; 63 | color: #FFF; 64 | padding: 2px 4px; 65 | font-size: 10px; 66 | } 67 | 68 | .authentication .remove:hover { 69 | color: #000; 70 | background-color: #DFDFDF; 71 | -moz-border-radius: 7px; 72 | -webkit-border-radius: 7px; 73 | } 74 | 75 | .authentication img { 76 | float: left; 77 | } 78 | 79 | .authentication .provider, .authentication .uid { 80 | margin-left: 38px; 81 | margin-right: 20px; 82 | } 83 | 84 | .authentication .provider { 85 | font-weight: bold; 86 | } 87 | 88 | .authentication .uid { 89 | color: #DDD; 90 | font-size: 11px; 91 | } 92 | 93 | .auth_provider img { 94 | display: block; 95 | } 96 | 97 | .auth_provider { 98 | float: left; 99 | text-decoration: none; 100 | margin-right: 20px; 101 | text-align: center; 102 | margin-bottom: 10px; 103 | } 104 | -------------------------------------------------------------------------------- /public/stylesheets/home.css: -------------------------------------------------------------------------------- 1 | #container { 2 | position: relative; 3 | } 4 | 5 | #welcome { 6 | line-height: 24px; 7 | position: absolute; 8 | top: 50px; 9 | left: 55px; 10 | width: 390px; 11 | } 12 | 13 | #welcome h2 { 14 | font-size: 25px; 15 | } 16 | 17 | .columns { 18 | padding-top: 350px; 19 | padding-bottom: 30px; 20 | margin-left: 50px; 21 | margin-right: 30px; 22 | } 23 | 24 | .column { 25 | float: left; 26 | width: 33.333%; 27 | } 28 | 29 | .column_padding { 30 | margin-right: 20px; 31 | } 32 | 33 | .column h2 a { 34 | position: absolute; 35 | display: block; 36 | right: 10px; 37 | top: 7px; 38 | font-size: 12px; 39 | text-decoration: none; 40 | padding: 5px 10px 6px 10px; 41 | -moz-border-radius: 7px; 42 | -webkit-border-radius: 7px; 43 | color: #000; 44 | background: #D3D3D3; 45 | background: -webkit-gradient(linear, left top, left bottom, from(#F2F2F2), to(#CCC)); 46 | background: -moz-linear-gradient(top, #F2F2F2, #CCC); 47 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F2F2F2', endColorstr='#CCC'); 48 | } 49 | 50 | .column #your_games h2 { 51 | background-color: #7B7DC7; 52 | } 53 | 54 | .column .pagination { 55 | margin: 10px 0; 56 | text-align: center; 57 | color: #777; 58 | } 59 | 60 | .column .pagination a { 61 | color: #777; 62 | text-decoration: none; 63 | background-color: #D4D4D8; 64 | padding: 4px 8px; 65 | margin: 0 8px; 66 | font-size: 14px; 67 | -moz-border-radius: 4px; 68 | -webkit-border-radius: 4px; 69 | } 70 | 71 | .column .pagination a:hover { 72 | color: #FFF; 73 | background-color: #444; 74 | } 75 | 76 | .no_games { 77 | background-color: #D4D4D8; 78 | color: #484848; 79 | margin-top: 8px; 80 | text-align: center; 81 | padding-top: 20px; 82 | padding-bottom: 30px; 83 | } 84 | 85 | .no_games .details { 86 | margin-bottom: 20px; 87 | line-height: 20px; 88 | } 89 | 90 | .column .buttons { 91 | text-align: center; 92 | margin-top: 15px; 93 | } 94 | 95 | 96 | /*** Users List ***/ 97 | 98 | .column .user { 99 | position: relative; 100 | height: 48px; 101 | } 102 | 103 | .user .avatar { 104 | position: absolute; 105 | display: block; 106 | } 107 | 108 | .user .details { 109 | position: absolute; 110 | top: 14px; 111 | left: 72px; 112 | font-size: 14px; 113 | } 114 | 115 | .user .go_button { 116 | position: absolute; 117 | right: 15px; 118 | top: 8px; 119 | font-size: 13px; 120 | } 121 | 122 | .user_offline, .user_online { 123 | padding-left: 3px; 124 | } 125 | 126 | .user_offline { 127 | color: #C61B19; 128 | } 129 | 130 | .user_online { 131 | color: #00A200; 132 | } 133 | -------------------------------------------------------------------------------- /public/stylesheets/user.css: -------------------------------------------------------------------------------- 1 | #container { 2 | padding: 30px 0; 3 | } 4 | 5 | #profile { 6 | margin-left: 180px; 7 | float: left; 8 | } 9 | 10 | #profile .avatar { 11 | float: left; 12 | } 13 | 14 | #profile .username { 15 | margin-left: 65px; 16 | } 17 | 18 | #profile .details { 19 | margin: 30px 0; 20 | } 21 | 22 | .column { 23 | margin-left: 450px; 24 | width: 350px; 25 | } -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /script/worker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | STDERR.sync = STDOUT.sync = true 3 | 4 | RAILS_ENV = ENV.fetch("RAILS_ENV", "development") 5 | RAILS_ROOT = File.expand_path("../..", __FILE__) 6 | 7 | ENV["BUNDLE_GEMFILE"] = "#{RAILS_ROOT}/Gemfile" 8 | require "bundler" 9 | Bundler.setup(:default, RAILS_ENV.to_sym) 10 | 11 | require "stalker" 12 | require "mysql2" 13 | require "go/gtp" 14 | require "oily_png" 15 | require "#{RAILS_ROOT}/lib/game_thumb" 16 | require "#{RAILS_ROOT}/lib/game_engine" 17 | 18 | mysql_config = Hash[YAML.load_file("#{RAILS_ROOT}/config/database.yml")[RAILS_ENV].map { |k, v| [k.to_sym, v] }] 19 | mysql = Mysql2::Client.new(mysql_config.merge(:reconnect => true)) 20 | 21 | Stalker.job("Game.move") do |args| 22 | id = args["id"] 23 | game = mysql.query("select * from games where id='#{id}' limit 1", :symbolize_keys => true).first 24 | update = GameEngine.update_game_attributes_with_move(game) 25 | update.each do |name, value| 26 | update[name] = value.utc.strftime("%Y-%m-%d %H:%M:%S") if value.kind_of? Time 27 | end 28 | values = update.map { |col, val| "#{col}='#{mysql.escape(val.to_s)}'" }.join(", ") 29 | mysql.query("UPDATE games SET #{values} WHERE id=#{id} AND current_player_id IS NULL AND finished_at IS NULL") 30 | GameThumb.generate(id, game[:board_size], update[:black_positions], update[:white_positions]) 31 | end 32 | 33 | Stalker.work 34 | -------------------------------------------------------------------------------- /spec/controllers/authentications_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe AuthenticationsController do 4 | fixtures :all 5 | render_views 6 | 7 | it "index action should render index template" do 8 | get :index 9 | response.should render_template(:index) 10 | end 11 | 12 | it "create action should redirect to root after logging in a user when authentication is found" do 13 | user = Factory(:user) 14 | user.authentications.create!(:provider => "foo", :uid => "123") 15 | request.env["omniauth.auth"] = {"provider" => "foo", "uid" => "123"} 16 | post :create 17 | response.should redirect_to(root_url) 18 | cookies["token"].should == user.token 19 | end 20 | 21 | it "create action should add authentication when logged in to a full user" do 22 | user = Factory(:user) 23 | @controller.stubs(:current_user).returns(user) 24 | request.env["omniauth.auth"] = {"provider" => "bar", "uid" => "456"} 25 | post :create 26 | user.authentications.first.uid.should == "456" 27 | response.should redirect_to(edit_current_user_url) 28 | end 29 | 30 | it "create action should make guest when not logged in and redirect to edit current user url when not valid" do 31 | request.env["omniauth.auth"] = {"provider" => "bar", "uid" => "789", "user_info" => {}} 32 | post :create 33 | response.should redirect_to(edit_current_user_url) 34 | cookies["token"].should_not be_nil 35 | session[:omniauth].should_not be_nil 36 | end 37 | 38 | it "create action should make guest when not logged in and redirect to root url when valid" do 39 | User.delete_all 40 | Authentication.delete_all 41 | request.env["omniauth.auth"] = {"provider" => "bar", "uid" => "123", "user_info" => {"email" => "foo@example.com", "nickname" => "foo"}} 42 | post :create 43 | response.should redirect_to(root_url) 44 | cookies["token"].should_not be_nil 45 | session[:omniauth].should be_nil 46 | end 47 | 48 | it "destroy action should destroy model and redirect edit user path" do 49 | user = Factory(:user) 50 | @controller.stubs(:current_user).returns(user) 51 | authentication = user.authentications.create!(:provider => 1, :uid => 2) 52 | delete :destroy, :id => authentication 53 | response.should redirect_to(edit_current_user_path) 54 | Authentication.exists?(authentication.id).should be_false 55 | end 56 | 57 | it "create action should log in user and merge guest games into user" do 58 | guest = User.create!(:guest => true) 59 | @controller.stubs(:current_user).returns(guest) 60 | game = Factory(:game, :black_player => guest) 61 | user = Factory(:user) 62 | user.authentications.create!(:provider => "foo", :uid => "123") 63 | request.env["omniauth.auth"] = {"provider" => "foo", "uid" => "123"} 64 | post :create 65 | response.should redirect_to(root_url) 66 | cookies["token"].should == user.token 67 | game.reload.black_player.should == user 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/controllers/games_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe GamesController do 4 | fixtures :all 5 | render_views 6 | 7 | it "index action should render index template" do 8 | get :index 9 | response.should render_template(:index) 10 | end 11 | 12 | it "show action should render show template" do 13 | get :show, :id => Game.first 14 | response.should render_template(:show) 15 | end 16 | 17 | it "new action should render new template" do 18 | get :new 19 | response.should render_template(:new) 20 | end 21 | 22 | it "new action should redirect user to signin when trying to challenge username" do 23 | get :new, :username => "foo" 24 | response.should redirect_to(signin_url) 25 | end 26 | 27 | it "create action should render new template when model is invalid" do 28 | Game.any_instance.stubs(:valid?).returns(false) 29 | post :create 30 | response.should render_template(:new) 31 | end 32 | 33 | it "create action should redirect when model is valid" do 34 | Game.any_instance.expects(:queue_computer_move) 35 | Game.any_instance.stubs(:valid?).returns(true) 36 | post :create 37 | response.should redirect_to(game_url(assigns[:game])) 38 | end 39 | 40 | it "sgf action should return SGF data" do 41 | Game.any_instance.stubs(:sgf).returns("sgf data") 42 | get :sgf, :id => Game.first 43 | response.content_type.should == "application/x-go-sgf" 44 | response.body.should == "sgf data" 45 | end 46 | end 47 | 48 | describe GamesController, "logged in" do 49 | fixtures :all 50 | render_views 51 | 52 | before(:each) do 53 | @user = Factory(:user) 54 | @controller.stubs(:current_user).returns(@user) 55 | end 56 | 57 | it "new action should render new template with custom username" do 58 | get :new, :username => "foo" 59 | response.should render_template(:new) 60 | end 61 | 62 | it "create action should send email to opponent user" do 63 | opponent = Factory(:user, :email_on_invitation => true) 64 | post :create, :game => { :chosen_opponent => "user", :opponent_username => opponent.username, :chosen_color => "black" } 65 | response.should redirect_to(game_url(assigns[:game])) 66 | Notifications.deliveries.size.should == 1 67 | Notifications.deliveries.first.subject.should == "[Go vs Go] Invitation from #{@user.username}" 68 | end 69 | 70 | it "create action should not send email to opponent user when unwanted" do 71 | opponent = Factory(:user, :email_on_invitation => false) 72 | post :create, :game => { :chosen_opponent => "user", :opponent_username => opponent.username, :chosen_color => "black" } 73 | response.should redirect_to(game_url(assigns[:game])) 74 | Notifications.deliveries.size.should == 0 75 | end 76 | 77 | it "create action should not send email to opponent user when playing against gnugo" do 78 | @user.update_attribute(:email_on_invitation, true) 79 | post :create, :game => { :chosen_opponent => "gnugo", :chosen_color => "black" } 80 | response.should redirect_to(game_url(assigns[:game])) 81 | Notifications.deliveries.size.should == 0 82 | end 83 | 84 | it "edit action should render edit javascript template and fill in user attributes" do 85 | game = Factory(:game, :white_player => @user, :current_player => @user) 86 | get :edit, :id => game, :format => :js 87 | response.should render_template(:edit) 88 | assigns(:game).chosen_opponent.should == "user" 89 | assigns(:game).opponent_username.should == game.black_player.username 90 | assigns(:game).chosen_color.should == "white" 91 | end 92 | 93 | it "update action should raise an error when already started" do 94 | lambda { 95 | put :update, :id => Factory(:game, :started_at => Time.now, :current_player => @user) 96 | }.should raise_error(RuntimeError) 97 | end 98 | 99 | it "update action should not allow user to update a game when he is not current player" do 100 | lambda { 101 | put :update, :id => Factory(:game, :started_at => nil, :current_player => nil) 102 | }.should raise_error(RuntimeError) 103 | end 104 | 105 | it "update action should mark game as started when accepting and adjust current player" do 106 | game = Factory(:game, :started_at => nil, :white_player => @user, :current_player => @user) 107 | put :update, :id => game, :invitation_button => "Accept", :format => :js 108 | game.reload.started_at.should_not be_nil 109 | game.current_player.should == game.black_player 110 | end 111 | 112 | it "update action should mark game as finished when declining" do 113 | game = Factory(:game, :started_at => nil, :white_player => @user, :current_player => @user) 114 | put :update, :id => game, :invitation_button => "Decline", :format => :js 115 | game.reload.started_at.should be_nil 116 | game.finished_at.should_not be_nil 117 | end 118 | 119 | it "update action should update game attributes" do 120 | game = Factory(:game, :started_at => nil, :white_player => @user, :current_player => @user, :board_size => 19) 121 | put :update, :id => game, :game => {:board_size => 9, :chosen_color => "white", :chosen_opponent => "user", :opponent_username => game.black_player.username}, :format => :js 122 | game.reload.started_at.should be_nil 123 | game.board_size.should == 9 124 | game.current_player.should == game.black_player 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/controllers/messages_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe MessagesController do 4 | fixtures :all 5 | render_views 6 | 7 | describe "as guest" do 8 | it "create action should redirect to signin url" do 9 | get :create 10 | response.should redirect_to(signin_url) 11 | end 12 | end 13 | 14 | describe "as user" do 15 | before(:each) do 16 | @user = Factory(:user) 17 | @controller.stubs(:current_user).returns(@user) 18 | end 19 | 20 | it "create action should render js template and send email if user requests" do 21 | game = Factory(:game, :black_player => @user) 22 | game.opponent(@user).update_attribute(:email_on_message, true) 23 | Message.any_instance.stubs(:valid?).returns(true) 24 | post :create, :format => "js", :message => {:game_id => game.id} 25 | response.should render_template("create") 26 | assigns(:message).user == @user 27 | assigns(:message).game == game 28 | Notifications.deliveries.size.should == 1 29 | Notifications.deliveries.first.subject.should == "[Go vs Go] Chat from #{@user.username}" 30 | end 31 | 32 | it "should not send message email when user doesn't want it" do 33 | game = Factory(:game, :black_player => @user) 34 | game.opponent(@user).update_attribute(:email_on_message, false) 35 | Message.any_instance.stubs(:valid?).returns(true) 36 | post :create, :format => "js", :message => {:game_id => game.id} 37 | response.should be_success 38 | Notifications.deliveries.size.should == 0 39 | end 40 | 41 | it "should not send message email when user is online" do 42 | game = Factory(:game, :black_player => @user) 43 | game.opponent(@user).update_attribute(:email_on_message, true) 44 | game.opponent(@user).update_attribute(:last_request_at, Time.now) 45 | Message.any_instance.stubs(:valid?).returns(true) 46 | post :create, :format => "js", :message => {:game_id => game.id} 47 | response.should be_success 48 | Notifications.deliveries.size.should == 0 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/controllers/moves_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe MovesController do 4 | fixtures :all 5 | render_views 6 | 7 | it "should show moves in game" do 8 | game = Factory(:game, :moves => "aa-bb-cc") 9 | get "index", :game_id => game.id, :format => "js", :after => 1 10 | response.body.should include("\"bb-cc\"") 11 | end 12 | 13 | it "should add a move and respond with javascript and send email if user requests" do 14 | game = Factory(:game) 15 | game.opponent.update_attribute(:email_on_move, true) 16 | @controller.stubs(:current_user).returns(game.current_player) 17 | post "create", :game_id => game.id, :format => "js", :move => "aa" 18 | response.should be_success 19 | game.reload 20 | Notifications.deliveries.size.should == 1 21 | Notifications.deliveries.first.subject.should == "[Go vs Go] Move by #{game.opponent.username}" 22 | end 23 | 24 | it "should not send move email when user doesn't want it" do 25 | game = Factory(:game) 26 | @controller.stubs(:current_user).returns(game.current_player) 27 | post "create", :game_id => game.id, :format => "js", :move => "aa" 28 | response.should be_success 29 | Notifications.deliveries.size.should == 0 30 | end 31 | 32 | # It used to not behave this way but it caused some confusion as to why it sometimes wouldn't send notifications 33 | it "should send move email even when user is online" do 34 | game = Factory(:game) 35 | game.opponent.update_attribute(:email_on_move, true) 36 | game.opponent.update_attribute(:last_request_at, Time.now) 37 | @controller.stubs(:current_user).returns(game.current_player) 38 | post "create", :game_id => game.id, :format => "js", :move => "aa" 39 | response.should be_success 40 | Notifications.deliveries.size.should == 1 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/controllers/sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe SessionsController do 4 | fixtures :all 5 | render_views 6 | 7 | it "new action should render new template" do 8 | get :new 9 | response.should render_template(:new) 10 | end 11 | 12 | it "create action should render new template when authentication is invalid" do 13 | User.stubs(:authenticate).returns(nil) 14 | post :create 15 | response.should render_template(:new) 16 | cookies["token"].should be_nil 17 | end 18 | 19 | it "create action should redirect when authentication is valid" do 20 | user = Factory(:user) 21 | User.stubs(:authenticate).returns(user) 22 | post :create 23 | response.should redirect_to("/") 24 | cookies["token"].should == user.token 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/controllers/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe UsersController do 4 | fixtures :all 5 | render_views 6 | 7 | it "show action should render show template" do 8 | user = Factory(:user) 9 | get :show, :id => user 10 | response.should render_template(:show) 11 | end 12 | 13 | it "show action should report 404 for guest user" do 14 | user = Factory(:user, :guest => true) 15 | lambda { 16 | get :show, :id => user 17 | }.should raise_error(ActiveRecord::RecordNotFound) 18 | end 19 | 20 | it "new action should render new template" do 21 | get :new 22 | response.should render_template(:new) 23 | end 24 | 25 | it "new action should redirect to login when email exists" do 26 | user = Factory(:user) 27 | get :new, :email => user.email 28 | response.should redirect_to(login_url(:login => user.email)) 29 | end 30 | 31 | it "new action should redirect to edit account page with email address when already logged in" do 32 | @controller.stubs(:current_user).returns(Factory(:user)) 33 | get :new, :email => "foo" 34 | response.should redirect_to(edit_current_user_url(:email => "foo")) 35 | end 36 | 37 | it "create action should render new template when model is invalid" do 38 | User.any_instance.stubs(:valid?).returns(false) 39 | post :create 40 | response.should render_template(:new) 41 | end 42 | 43 | it "create action should redirect when model is valid" do 44 | User.any_instance.stubs(:valid?).returns(true) 45 | post :create 46 | response.should redirect_to(root_url) 47 | cookies["token"].should == assigns["user"].token 48 | end 49 | 50 | it "edit action should redirect when not logged in" do 51 | get :edit, :id => "ignored" 52 | response.should redirect_to(signin_url) 53 | end 54 | 55 | it "edit action should render edit template" do 56 | @controller.stubs(:current_user).returns(User.first) 57 | get :edit, :id => "ignored" 58 | response.should render_template(:edit) 59 | end 60 | 61 | it "update action should redirect when not logged in" do 62 | put :update, :id => "ignored" 63 | response.should redirect_to(signin_url) 64 | end 65 | 66 | it "update action should render edit template when user is invalid" do 67 | @controller.stubs(:current_user).returns(User.first) 68 | User.any_instance.stubs(:valid?).returns(false) 69 | put :update, :id => "ignored" 70 | response.should render_template(:edit) 71 | end 72 | 73 | it "update action should redirect when user is valid" do 74 | @controller.stubs(:current_user).returns(User.first) 75 | User.any_instance.stubs(:valid?).returns(true) 76 | put :update, :id => "ignored" 77 | response.should redirect_to(root_url) 78 | end 79 | 80 | it "unsubscribe action should remove email options from user with matching token" do 81 | user = Factory(:user, :email_on_invitation => true, :email_on_move => true, :email_on_message => true) 82 | get :unsubscribe, :token => user.unsubscribe_token 83 | response.should redirect_to(root_url) 84 | user.reload 85 | user.email_on_invitation.should be_false 86 | user.email_on_move.should be_false 87 | user.email_on_message.should be_false 88 | end 89 | 90 | it "publicize action should redirect when not logged in" do 91 | put :publicize, :id => "ignored" 92 | response.should redirect_to(signin_url) 93 | end 94 | 95 | it "publicize action should redirect to root url and update publicized_at time" do 96 | user = Factory(:user, :publicized_at => nil) 97 | @controller.stubs(:current_user).returns(user) 98 | put :publicize, :id => "ignored" 99 | response.should redirect_to(root_url) 100 | user.reload.publicized_at.to_date.should == Time.zone.now.to_date 101 | end 102 | 103 | it "publicize action should not allow guest" do 104 | user = Factory(:user, :publicized_at => nil, :guest => true) 105 | @controller.stubs(:current_user).returns(user) 106 | put :publicize, :id => "ignored" 107 | response.should redirect_to(signin_url) 108 | user.reload.publicized_at.should be_nil 109 | flash[:alert].should_not be_nil 110 | end 111 | 112 | it "publicize action should remove publicized_at time when asked" do 113 | user = Factory(:user, :publicized_at => Time.now) 114 | @controller.stubs(:current_user).returns(user) 115 | put :publicize, :id => "ignored", :remove => true 116 | response.should redirect_to(root_url) 117 | user.reload.publicized_at.should be_nil 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | Factory.define :user do |f| 2 | f.sequence(:username) { |n| "foo#{n}" } 3 | f.sequence(:email) { |n| "foo#{n}@example.com" } 4 | f.password "foobar" 5 | f.password_confirmation { |u| u.password } 6 | end 7 | 8 | Factory.define :game do |f| 9 | f.association(:black_player, :factory => :user) 10 | f.association(:white_player, :factory => :user) 11 | f.current_player { |g| g.black_player } 12 | f.board_size 19 13 | f.started_at Time.now 14 | end 15 | 16 | Factory.define :message do |f| 17 | f.association(:game, :factory => :game) 18 | f.user { |m| m.game.black_player } 19 | f.content "Hello World" 20 | end 21 | -------------------------------------------------------------------------------- /spec/fixtures/authentications.yml: -------------------------------------------------------------------------------- 1 | one: 2 | user_id: 1 3 | provider: MyString 4 | uid: MyString 5 | 6 | two: 7 | user_id: 1 8 | provider: MyString 9 | uid: MyString 10 | -------------------------------------------------------------------------------- /spec/fixtures/games.yml: -------------------------------------------------------------------------------- 1 | one: 2 | black_player_id: 1 3 | white_player_id: 1 4 | current_player_id: 1 5 | black_score: 1.5 6 | white_score: 1.5 7 | black_positions: MyText 8 | white_positions: MyText 9 | moves: MyText 10 | komi: 1.5 11 | handicap: 1 12 | board_size: 1 13 | last_move_at: 2010-10-15 17:15:53 14 | started_at: 2010-10-15 17:15:53 15 | finished_at: 2010-10-15 17:15:53 16 | 17 | two: 18 | black_player_id: 1 19 | white_player_id: 1 20 | current_player_id: 1 21 | black_score: 1.5 22 | white_score: 1.5 23 | black_positions: MyText 24 | white_positions: MyText 25 | moves: MyText 26 | komi: 1.5 27 | handicap: 1 28 | board_size: 1 29 | last_move_at: 2010-10-15 17:15:53 30 | started_at: 2010-10-15 17:15:53 31 | finished_at: 2010-10-15 17:15:53 32 | -------------------------------------------------------------------------------- /spec/fixtures/messages.yml: -------------------------------------------------------------------------------- 1 | one: 2 | game_id: 1 3 | user_id: 1 4 | content: MyText 5 | move_index: 1 6 | 7 | two: 8 | game_id: 1 9 | user_id: 1 10 | content: MyText 11 | move_index: 1 12 | -------------------------------------------------------------------------------- /spec/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # password: "secret" 2 | foo: 3 | username: foo 4 | email: foo@example.com 5 | password_hash: 3488f5f7efecab14b91eb96169e5e1ee518a569f 6 | password_salt: bef65e058905c379436d80d1a32e7374b139e7b0 7 | 8 | bar: 9 | username: bar 10 | email: bar@example.com 11 | password_hash: 3488f5f7efecab14b91eb96169e5e1ee518a569f 12 | password_salt: bef65e058905c379436d80d1a32e7374b139e7b0 13 | -------------------------------------------------------------------------------- /spec/lib/game_engine_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe GameEngine do 4 | describe "without gtp stub" do 5 | before(:each) do 6 | @gtp = Go::GTP.run_gnugo 7 | @engine = GameEngine.new(@gtp) 8 | end 9 | 10 | after(:each) do 11 | @gtp.quit 12 | end 13 | 14 | it "should raise illegal suicide move when placing a stone in surrounding stones" do 15 | @engine.replay("ab-cc-ba") 16 | lambda { 17 | @engine.move("aa") 18 | }.should raise_error(GameEngine::IllegalMove) 19 | end 20 | 21 | it "should report back captured stones for black" do 22 | @engine.replay("dd-aa-ab-cc") 23 | @engine.move("ba").should == "baaa" 24 | end 25 | 26 | it "should report back captured stones for white" do 27 | @engine.replay("aa-ab-cc") 28 | @engine.move("ba").should == "baaa" 29 | end 30 | 31 | it "should allow black to place stone with surrounding black stones" do 32 | @engine.replay("ab-cc-ba-dd") 33 | @engine.move("aa").should == "aa" 34 | end 35 | 36 | it "should end game when resigning" do 37 | @engine.replay("ab-cc-ba-dd") 38 | @engine.move("RESIGN").should == "RESIGN" 39 | @engine.should be_over 40 | @engine.score(:black).should == 0 41 | @engine.score(:white).should == 1 42 | end 43 | 44 | it "should report illegal move when playing after resigning" do 45 | @engine.replay("aa-RESIGN") 46 | lambda { 47 | @engine.move("bb") 48 | }.should raise_error(GameEngine::IllegalMove) 49 | end 50 | end 51 | 52 | describe "with gtp stub" do 53 | before(:each) do 54 | @gtp = mock 55 | @gtp.stubs(:success?).returns(true) 56 | @engine = GameEngine.new(@gtp) 57 | end 58 | 59 | it "run should invoke GNU Go with the proper boardsize" do 60 | Go::GTP.expects(:run_gnugo).yields(@gtp) 61 | @gtp.expects(:boardsize).with(13) 62 | GameEngine.run(:board_size => 13) { } 63 | end 64 | 65 | it "run should invoke GNU Go with non-zero handicaps" do 66 | Go::GTP.expects(:run_gnugo).yields(@gtp) 67 | @gtp.expects(:fixed_handicap).with(2) 68 | GameEngine.run(:handicap => 2) { } 69 | end 70 | 71 | it "run should invoke GNU Go with the proper komi" do 72 | Go::GTP.expects(:run_gnugo).yields(@gtp) 73 | @gtp.expects(:komi).with(5.5) 74 | GameEngine.run(:komi => 5.5) { } 75 | end 76 | 77 | it "update_game_attributes_with_move should pass it on to engine" do 78 | GameEngine.expects(:run).with(:foo => 123).yields(@engine) 79 | @engine.stubs(:update_game_attributes_with_move).with({:foo => 123}, "ab").returns(:updated => true) 80 | GameEngine.update_game_attributes_with_move({:foo => 123}, "ab").should == {:updated => true} 81 | end 82 | 83 | it "should call play for each move on replay passing in GNU Go position" do 84 | @gtp.expects(:play).with(:white, "A16") 85 | @gtp.expects(:play).with(:black, "A17") 86 | @engine.replay("acff-ad") 87 | end 88 | 89 | it "should do nothing for replay when passing an nil for moves" do 90 | @engine.replay(nil) 91 | end 92 | 93 | it "should replay pass and resign correctly" do 94 | @gtp.expects(:play).with(:black, "PASS") 95 | @engine.replay("PASS-RESIGN") 96 | end 97 | 98 | it "should move stone and return point back" do 99 | @gtp.stubs(:over?).returns(false) 100 | @gtp.stubs(:list_stones).with(:white).returns(%w[A17 A16]) 101 | @gtp.expects(:play).with(:black, "A18") 102 | @engine.move("ab").should == "ab" 103 | end 104 | 105 | it "should move stone and return point back with captures" do 106 | @gtp.stubs(:over?).returns(false) 107 | @gtp.expects(:play).with(:black, "A18") 108 | @gtp.expects(:list_stones).with(:white).returns(%w[A16]) # it mocks them in reverse order 109 | @gtp.expects(:list_stones).with(:white).returns(%w[A17 A16]) 110 | @engine.move("ab").should == "abac" 111 | end 112 | 113 | it "should call genmove when move position isn't given" do 114 | @gtp.stubs(:over?).returns(false) 115 | @gtp.stubs(:list_stones).with(:white).returns(%w[A17 A16]) 116 | @gtp.expects(:genmove).with(:black).returns("A18") 117 | @engine.move.should == "ab" 118 | end 119 | 120 | it "should convert black positions to points" do 121 | @gtp.stubs(:list_stones).with(:black).returns(%w[A17 A16]) 122 | @engine.positions(:black).should == "acad" 123 | end 124 | 125 | it "should determine scores when black wins" do 126 | @gtp.stubs(:final_score).returns("B+70.5") 127 | @engine.score(:black).should == 70.5 128 | @engine.score(:white).should == 0 129 | end 130 | 131 | it "should determine scores when white wins" do 132 | @gtp.stubs(:final_score).returns("W+35.5") 133 | @engine.score(:black).should == 0 134 | @engine.score(:white).should == 35.5 135 | end 136 | 137 | it "should return PASS as move when passing" do 138 | @gtp.stubs(:over?).returns(false) 139 | @gtp.expects(:play).with(:black, "PASS") 140 | @engine.move("PASS").should == "PASS" 141 | end 142 | 143 | it "should be over when genmove returns resign" do 144 | @gtp.stubs(:over?).returns(false) 145 | @gtp.stubs(:list_stones).with(:white).returns(%w[A17 A16]) 146 | @gtp.expects(:genmove).with(:black).returns("resign") 147 | @engine.move.should == "RESIGN" 148 | @engine.should be_over 149 | end 150 | 151 | it "should be finished when gtp says game is over" do 152 | @gtp.stubs(:over?).returns(true) 153 | @engine.should be_over 154 | end 155 | 156 | it "should have attributes after move when game not over" do 157 | @engine.expects(:replay).with("") 158 | @engine.stubs(:move).with("aa").returns("bb") 159 | @engine.stubs(:positions).with(:black).returns("black") 160 | @engine.stubs(:positions).with(:white).returns("white") 161 | @engine.stubs(:over?).returns(false) 162 | @engine.stubs(:captures).with(:black).returns(1) 163 | @engine.stubs(:captures).with(:white).returns(2) 164 | result = @engine.update_game_attributes_with_move({:moves => "", :black_player_id => 123}, "aa") 165 | result[:moves].should == "bb" 166 | result[:black_positions].should == "black" 167 | result[:white_positions].should == "white" 168 | result[:current_player_id].should == 123 169 | result[:black_score].should == 1 170 | result[:white_score].should == 2 171 | end 172 | 173 | it "should have attributes after move when game is over" do 174 | @engine.expects(:replay).with("aa") 175 | @engine.stubs(:move).with(nil).returns("cc") 176 | @engine.stubs(:positions).with(:black).returns("black") 177 | @engine.stubs(:positions).with(:white).returns("white") 178 | @engine.stubs(:over?).returns(true) 179 | @engine.stubs(:score).with(:black).returns(1) 180 | @engine.stubs(:score).with(:white).returns(2) 181 | result = @engine.update_game_attributes_with_move(:moves => "aa") 182 | result[:moves].should == "aa-cc" 183 | result[:finished_at].should > 1.minute.ago 184 | result[:black_score].should == 1 185 | result[:white_score].should == 2 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /spec/mailers/notifications_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Notifications do 4 | it "invitation should send to current player of game" do 5 | game = Factory(:game) 6 | game.current_player = game.black_player 7 | mail = Notifications.invitation(game) 8 | mail.subject.should == "[Go vs Go] Invitation from #{game.white_player.username}" 9 | mail.to.should == [game.black_player.email] 10 | mail.from.should == ["noreply@govsgo.com"] 11 | mail.body.encoded.should include("unsubscribe") 12 | end 13 | 14 | it "move sends email about move" do 15 | game = Factory(:game) 16 | game.current_player = game.black_player 17 | mail = Notifications.move(game) 18 | mail.subject.should == "[Go vs Go] Move by #{game.white_player.username}" 19 | mail.to.should == [game.black_player.email] 20 | mail.from.should == ["noreply@govsgo.com"] 21 | mail.body.encoded.should include("unsubscribe") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/models/authentication_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe Authentication do 4 | it "should be valid" do 5 | Authentication.new.should be_valid 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/models/game_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe Game do 4 | before :each do 5 | @game = Game.new 6 | end 7 | 8 | it "should require a valid board_size" do 9 | @game.board_size = 50 10 | @game.should have(1).error_on(:board_size) 11 | @game.board_size = 9 12 | @game.should have(:no).errors_on(:board_size) 13 | end 14 | 15 | it "should require a valid handicap" do 16 | @game.handicap = 50 17 | @game.should have(1).error_on(:handicap) 18 | @game.handicap = 2 19 | @game.should have(:no).errors_on(:handicap) 20 | end 21 | 22 | it "should require a valid komi" do 23 | @game.komi = -3 24 | @game.should have(1).error_on(:komi) 25 | @game.komi = 6.5 26 | @game.should have(:no).errors_on(:komi) 27 | end 28 | 29 | it "should report moves after a position index" do 30 | @game.moves = "aa-bb-cc-dd" 31 | @game.moves_after(2).should == "cc-dd" 32 | end 33 | 34 | it "should report no moves when nil" do 35 | @game.moves_after(2).should == "" 36 | end 37 | 38 | it "should report no moves when out of range" do 39 | @game.moves = "aa-bb-cc-dd" 40 | @game.moves_after(8).should == "" 41 | end 42 | 43 | it "should set creator to black or white when choosing that color" do 44 | user = Factory(:user) 45 | @game.creator = user 46 | @game.chosen_color.should be_nil 47 | @game.chosen_color = "black" 48 | @game.prepare 49 | @game.black_player.should == user 50 | @game.chosen_color.should == "black" 51 | @game.current_player.should == user 52 | @game.black_player = nil 53 | @game.chosen_color = "white" 54 | @game.prepare 55 | @game.white_player.should == user 56 | @game.chosen_color.should == "white" 57 | end 58 | 59 | it "should raise an OutOfTurn exception when attempting to play when it's not your turn" do 60 | user = Factory(:user) 61 | @game.save! 62 | lambda { @game.move("aa", user) }.should raise_error(GameEngine::OutOfTurn) 63 | @game.update_attribute(:current_player_id, user.id) 64 | lambda { @game.move("bb", user) }.should_not raise_error(GameEngine::OutOfTurn) 65 | end 66 | 67 | it "should generate profile for black" do 68 | user = Factory(:user) 69 | game = Factory(:game, :current_player => user, :black_player => user, :handicap => 4, :black_score => 3) 70 | profile = game.profile_for(:black) 71 | profile.handicap_or_komi.should == "4 handicap" 72 | profile.score.should == 3 73 | profile.user.should == user 74 | profile.current.should be_true 75 | profile.last_status.should be_blank 76 | end 77 | 78 | it "should generate profile for white as GNU Go" do 79 | game = Factory(:game, :komi => 6.5, :white_player_id => nil, :white_score => 4, :current_player_id => Factory(:user).id, :moves => "PASS") 80 | profile = game.profile_for(:white) 81 | profile.handicap_or_komi.should == "6.5 komi" 82 | profile.score.should == 4 83 | profile.captured.should == 4 84 | profile.user.should be_nil 85 | profile.current.should be_false 86 | profile.last_status.should == "passed" 87 | end 88 | 89 | it "should have resigned as last status in profile" do 90 | game = Factory(:game, :white_player => Factory(:user), :moves => "RESIGN") 91 | game.profile_for(:white).last_status.should == "resigned" 92 | end 93 | 94 | it "should return SGF format for normal play" do 95 | game = Factory(:game, :moves => "aa-bbcc-PASS-dd", :handicap => 0, :board_size => 9, :komi => 6.5, :finished_at => Time.now, :white_score => 30.5, :black_score => 0) 96 | game.black_player.username = "foo" 97 | game.black_player.rank = "4k" 98 | game.white_player.username = "" 99 | game.white_player.rank = "" 100 | sgf = game.sgf 101 | sgf.should include("(;FF[4]GM[1]CA[utf-8]AP[govsgo:0.1]RU[Japanese]SZ[9]KM[6.5]HA[0]") 102 | sgf.should include("PB[foo]BR[4k]PW[Guest]WR[]") 103 | sgf.should include("RE[W+30.5]") 104 | sgf.should include(";B[aa];W[bb];B[];W[dd])") 105 | end 106 | 107 | it "should return SGF format for handicap game" do 108 | game = Factory(:game, :moves => "aa-RESIGN", :handicap => 2, :board_size => 19, :white_player => nil, :black_score => 0, :white_score => 1, :finished_at => Time.now) 109 | sgf = game.sgf 110 | sgf.should include("RE[W+R]") 111 | sgf.should include("HA[2]AB[pd][dp]") 112 | sgf.should include(";W[aa])") 113 | end 114 | 115 | it "should determine capture count from score when game hasn't ended" do 116 | game = Factory(:game, :black_score => 2, :white_score => 3) 117 | game.captured(:black).should == 2 118 | game.captured(:white).should == 3 119 | end 120 | 121 | it "should determine capture count from moves when game has ended" do 122 | game = Factory(:game, :moves => "aa-bb-PASS-ccdd-ffgghh-RESIGN", :finished_at => Time.now) 123 | game.captured(:black).should == 2 124 | game.captured(:white).should == 1 125 | end 126 | 127 | it "should determine capture count from moves when game with handicap has ended" do 128 | game = Factory(:game, :moves => "aa-bb-PASS-ccdd-ffgghh-RESIGN", :finished_at => Time.now, :handicap => 2) 129 | game.captured(:black).should == 1 130 | game.captured(:white).should == 2 131 | end 132 | 133 | it "black level is 10 when even" do 134 | game = Factory.build(:game, :handicap => 0, :komi => 6.5) 135 | game.level_for(game.black_player).should == 10 136 | end 137 | 138 | it "black level is 1 when 9 handicap and 6.5 komi" do 139 | game = Factory.build(:game, :handicap => 9, :komi => 6.5) 140 | game.level_for(game.black_player).should == 1 141 | end 142 | 143 | it "black level is 9 when 0.5 komi" do 144 | game = Factory.build(:game, :handicap => 0, :komi => 0.5) 145 | game.level_for(game.black_player).should == 9 146 | end 147 | 148 | it "white level is 11 when even" do 149 | game = Factory.build(:game, :handicap => 0, :komi => 6.5) 150 | game.level_for(game.white_player).should == 11 151 | end 152 | 153 | it "white level is 20 when 9 handicap and 6.5 komi" do 154 | game = Factory.build(:game, :handicap => 9, :komi => 6.5) 155 | game.level_for(game.white_player).should == 20 156 | end 157 | 158 | it "is even with black when setting level to 10" do 159 | game = Factory.build(:game) 160 | game.adjust_to_level(10) 161 | game.chosen_color.should == "black" 162 | game.handicap.should == 0 163 | game.komi.should == 6.5 164 | end 165 | 166 | it "chooses black with 9 handicap and 6.5 komi when setting level to 1" do 167 | game = Factory.build(:game) 168 | game.adjust_to_level(1) 169 | game.chosen_color.should == "black" 170 | game.handicap.should == 9 171 | game.komi.should == 6.5 172 | end 173 | 174 | it "chooses black with 0 handicap and 0.5 komi when setting level to 9" do 175 | game = Factory.build(:game) 176 | game.adjust_to_level(9) 177 | game.chosen_color.should == "black" 178 | game.handicap.should == 0 179 | game.komi.should == 0.5 180 | end 181 | 182 | it "is even with white when setting level to 11" do 183 | game = Factory.build(:game) 184 | game.adjust_to_level(11) 185 | game.chosen_color.should == "white" 186 | game.handicap.should == 0 187 | game.komi.should == 6.5 188 | end 189 | 190 | it "chooses white with 7 handicap and 6.5 komi when setting level to 18" do 191 | game = Factory.build(:game) 192 | game.adjust_to_level(18) 193 | game.chosen_color.should == "white" 194 | game.handicap.should == 7 195 | game.komi.should == 6.5 196 | end 197 | 198 | it "chooses white with 0 handicap and 0.5 komi when setting level to 12" do 199 | game = Factory.build(:game) 200 | game.adjust_to_level(12) 201 | game.chosen_color.should == "white" 202 | game.handicap.should == 0 203 | game.komi.should == 0.5 204 | end 205 | 206 | it "resulting level should be minus one for loss and plus one for win" do 207 | game = Factory.build(:game, :black_score => 0, :white_score => 1, :finished_at => Time.now, :handicap => 0, :komi => 6.5) 208 | game.level_for(game.black_player).should == 10 209 | game.resulting_level_for(game.black_player).should == 9 210 | game.level_for(game.white_player).should == 11 211 | game.resulting_level_for(game.white_player).should == 12 212 | end 213 | 214 | it "should be an active game when started and not finished" do 215 | Factory.build(:game, :started_at => Time.now, :finished_at => nil).should be_active 216 | Factory.build(:game, :started_at => nil, :finished_at => nil).should_not be_active 217 | Factory.build(:game, :started_at => nil, :finished_at => Time.now).should_not be_active 218 | Factory.build(:game, :started_at => Time.now, :finished_at => Time.now).should_not be_active 219 | end 220 | 221 | it "should know other player is opponent" do 222 | game = Factory.build(:game) 223 | game.opponent(game.black_player).should == game.white_player 224 | end 225 | 226 | it "should have nil winner/loser for game which hasn't started" do 227 | game = Factory.build(:game, :started_at => nil, :finished_at => Time.now) 228 | game.winner.should be_nil 229 | game.loser.should be_nil 230 | end 231 | 232 | it "should know if a user is a player in a game" do 233 | game = Factory.build(:game) 234 | game.should be_player(game.black_player) 235 | game.should be_player(game.white_player) 236 | game.should_not be_player(Factory.build(:user)) 237 | end 238 | end 239 | -------------------------------------------------------------------------------- /spec/models/message_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe Message do 4 | it "should validate that the user is a player in a game" do 5 | game = Factory(:game) 6 | Factory.build(:message, :game => game, :user => game.black_player).should be_valid 7 | Factory.build(:message, :game => game, :user => Factory(:user)).should have(1).error_on(:game_id) 8 | end 9 | 10 | it "should validate the presence of game, user and content" do 11 | message = Factory.build(:message, :game_id => "", :user_id => "", :content => "") 12 | message.should have(1).error_on(:game_id) 13 | message.should have(1).error_on(:user_id) 14 | message.should have(1).error_on(:content) 15 | end 16 | 17 | it "should set the move index to the last move position" do 18 | game = Factory(:game, :moves => "aa-bb-cc") 19 | Factory(:message, :game => game).move_index.should == 2 20 | Factory(:message).move_index.should be_nil 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe User do 4 | before(:each) do 5 | User.delete_all 6 | end 7 | 8 | it "should be valid" do 9 | Factory.build(:user).should be_valid 10 | end 11 | 12 | it "guest should be valid when empty" do 13 | User.new(:guest => true).should have(0).errors 14 | end 15 | 16 | it "guest should require password when updating" do 17 | user = User.create!(:guest => true) 18 | user.guest = false 19 | user.should have(1).error_on(:password) 20 | end 21 | 22 | it "should not require password when using other forms of authentication" do 23 | user = Factory(:user, :guest => true, :password => nil) 24 | user.authentications.create!(:provider => "foo", :uid => 123) 25 | user.guest = false 26 | user.should have(0).errors_on(:password) 27 | end 28 | 29 | it "should require username" do 30 | Factory.build(:user, :username => '').should have(1).error_on(:username) 31 | end 32 | 33 | it "should require password" do 34 | Factory.build(:user, :password => '').should have(1).error_on(:password) 35 | end 36 | 37 | it "should require well formed email" do 38 | Factory.build(:user, :email => 'foo@bar@example.com').should have(1).error_on(:email) 39 | end 40 | 41 | it "should validate uniqueness of email" do 42 | Factory(:user, :email => 'bar@example.com') 43 | Factory.build(:user, :email => 'bar@example.com').should have(1).error_on(:email) 44 | end 45 | 46 | it "should validate uniqueness of username" do 47 | Factory(:user, :username => 'uniquename') 48 | Factory.build(:user, :username => 'uniquename').should have(1).error_on(:username) 49 | end 50 | 51 | it "should not allow odd characters in username" do 52 | Factory.build(:user, :username => 'odd ^&(@)').should have(1).error_on(:username) 53 | end 54 | 55 | it "should validate password is longer than 3 characters" do 56 | Factory.build(:user, :password => 'bad').should have(1).error_on(:password) 57 | end 58 | 59 | it "should require matching password confirmation" do 60 | Factory.build(:user, :password_confirmation => 'nonmatching').should have(1).error_on(:password) 61 | end 62 | 63 | it "should generate password hash and salt on create" do 64 | user = Factory(:user) 65 | user.password_hash.should_not be_nil 66 | user.password_salt.should_not be_nil 67 | end 68 | 69 | it "should authenticate by username" do 70 | user = Factory(:user, :username => 'foobar', :password => 'secret') 71 | User.authenticate('foobar', 'secret').should == user 72 | end 73 | 74 | it "should authenticate by email" do 75 | user = Factory(:user, :email => 'foo@bar.com', :password => 'secret') 76 | User.authenticate('foo@bar.com', 'secret').should == user 77 | end 78 | 79 | it "should not authenticate bad username" do 80 | User.authenticate('nonexisting', 'secret').should be_nil 81 | end 82 | 83 | it "should not authenticate bad password" do 84 | Factory(:user, :username => 'foobar', :password => 'secret').save! 85 | User.authenticate('foobar', 'badpassword').should be_nil 86 | end 87 | 88 | it "should have games based on white or black" do 89 | user = Factory(:user) 90 | black_game = Factory(:game, :black_player => user) 91 | white_game = Factory(:game, :white_player => user) 92 | user.games.should == [black_game, white_game] 93 | end 94 | 95 | it "should separate games for your turn vs their turn" do 96 | user = Factory(:user) 97 | black_game = Factory(:game, :black_player => user, :current_player => user) 98 | white_game = Factory(:game, :white_player => user, :current_player => nil) 99 | Factory(:game, :black_player => user, :current_player => user, :finished_at => Time.now) 100 | Factory(:game, :white_player => user, :finished_at => Time.now) 101 | user.games_your_turn.should == [black_game] 102 | user.games_their_turn.should == [white_game] 103 | end 104 | 105 | it "should filter other games" do 106 | Game.delete_all 107 | user = Factory(:user) 108 | black_game = Factory(:game, :black_player => user) 109 | white_game = Factory(:game, :white_player => user) 110 | other_game = Factory(:game) 111 | user.other_games.should == [other_game] 112 | end 113 | 114 | it "should report user online when last request time within 5 minutes" do 115 | Factory.build(:user, :last_request_at => 4.minutes.ago).should be_online 116 | Factory.build(:user, :last_request_at => 6.minutes.ago).should_not be_online 117 | Factory.build(:user).should_not be_online 118 | end 119 | 120 | it "should update black, white, and current player id when moving games" do 121 | user = Factory(:user) 122 | game = Factory(:game, :black_player => user, :white_player => user) 123 | other_user = Factory(:user) 124 | user.move_games_to(other_user) 125 | game.reload.black_player.should == other_user 126 | game.black_player.should == other_user 127 | game.current_player.should == other_user 128 | end 129 | 130 | it "should generate a unique 16 digit token for each user" do 131 | User.create!(:guest => true).token.should_not == User.create!(:guest => true).token 132 | User.create!(:guest => true).token.length.should == 16 133 | User.create!(:guest => true).unsubscribe_token.length.should == 16 134 | end 135 | 136 | it "gnugo_level defaults to 6" do 137 | User.new.gnugo_level.should == 6 138 | end 139 | 140 | it "gnugo_level should average out resulting level for past games" do 141 | user = Factory(:user) 142 | Factory(:game, :black_player => user, :white_player => nil, :finished_at => Time.now, :black_score => 1, :white_score => 0, :handicap => 9, :komi => 6.5) # resulting level 2 143 | Factory(:game, :black_player => user, :white_player => nil, :finished_at => Time.now, :black_score => 0, :white_score => 1, :handicap => 5, :komi => 6.5) # resulting level 4 144 | Factory(:game, :black_player => user, :white_player => nil, :finished_at => Time.now, :black_score => 1, :white_score => 0, :handicap => 5, :komi => 6.5) # resulting level 6 145 | Factory(:game, :black_player => user, :white_player => nil, :finished_at => 2.days.ago, :black_score => 1, :white_score => 0, :handicap => 0, :komi => 6.5) # ignore since older 146 | Factory(:game, :black_player => user, :white_player => nil, :handicap => 5, :komi => 6.5) # ignore since not finished 147 | Factory(:game, :black_player => user, :handicap => 4, :komi => 6.5) # ignore since not against GNU Go 148 | user.gnugo_level.should == 4 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require File.expand_path("../../config/environment", __FILE__) 4 | require 'rspec/rails' 5 | 6 | # Requires supporting ruby files with custom matchers and macros, etc, 7 | # in spec/support/ and its subdirectories. 8 | Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} 9 | 10 | RSpec.configure do |config| 11 | # == Mock Framework 12 | # 13 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: 14 | # 15 | # config.mock_with :mocha 16 | # config.mock_with :flexmock 17 | # config.mock_with :rr 18 | config.mock_with :mocha 19 | 20 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 21 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 22 | 23 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 24 | # examples within a transaction, remove the following line or assign false 25 | # instead of true. 26 | config.use_transactional_fixtures = true 27 | 28 | config.before(:each) do 29 | Notifications.deliveries = [] 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/govsgo/c8d739d59ddd3239dc6a34cb11c9331a5874e632/vendor/plugins/.gitkeep --------------------------------------------------------------------------------