├── log └── .keep ├── tmp └── .keep ├── lib └── tasks │ └── .keep ├── test ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── game_test.rb │ └── players_game_test.rb ├── controllers │ ├── .keep │ └── api │ │ ├── games_controller_test.rb │ │ ├── players_games_controller_test.rb │ │ └── players_controller_test.rb ├── fixtures │ ├── .keep │ └── files │ │ └── .keep ├── integration │ └── .keep └── test_helper.rb ├── webpack ├── static │ └── .gitkeep ├── test │ ├── unit │ │ ├── .eslintrc │ │ ├── specs │ │ │ └── Hello.spec.js │ │ ├── index.js │ │ └── karma.conf.js │ └── e2e │ │ ├── specs │ │ └── test.js │ │ ├── custom-assertions │ │ └── elementCount.js │ │ ├── runner.js │ │ └── nightwatch.conf.js ├── config │ ├── prod.env.js │ ├── test.env.js │ ├── dev.env.js │ └── index.js ├── build │ ├── dev-client.js │ ├── build.js │ ├── webpack.dev.conf.js │ ├── check-versions.js │ ├── utils.js │ ├── dev-server.js │ ├── webpack.base.conf.js │ └── webpack.prod.conf.js ├── src │ ├── main.js │ ├── components │ │ ├── Paddle.vue │ │ ├── Ball.vue │ │ ├── LeaderBoard.vue │ │ ├── Table.vue │ │ └── GameList.vue │ └── App.vue └── index.html ├── app ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── player.rb │ ├── players_game.rb │ ├── stripe_wrapper.rb │ ├── game.rb │ └── ball.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── application_controller.rb │ └── api │ │ ├── players_controller.rb │ │ ├── players_games_controller.rb │ │ ├── games_controller.rb │ │ ├── donations_controller.rb │ │ └── leaders_controller.rb ├── views │ └── layouts │ │ ├── mailer.text.erb │ │ └── mailer.html.erb ├── jobs │ ├── application_job.rb │ └── pong_job.rb ├── channels │ ├── application_cable │ │ ├── channel.rb │ │ └── connection.rb │ ├── games_channel.rb │ ├── pong_channel.rb │ ├── left_paddle_channel.rb │ └── right_paddle_channel.rb └── mailers │ └── application_mailer.rb ├── .eslintignore ├── Pong Diagram.png ├── Procfile ├── .babelrc ├── bin ├── bundle ├── rake ├── rails ├── spring ├── update └── setup ├── cable └── config.ru ├── config ├── spring.rb ├── boot.rb ├── environment.rb ├── initializers │ ├── mime_types.rb │ ├── sidekiq.rb │ ├── application_controller_renderer.rb │ ├── stripe.rb │ ├── filter_parameter_logging.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── redis.rb │ ├── cors.rb │ ├── inflections.rb │ └── new_framework_defaults.rb ├── cable.yml ├── routes.rb ├── locales │ └── en.yml ├── database.yml ├── secrets.yml ├── application.rb ├── newrelic.yml ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── puma.rb ├── config.ru ├── .editorconfig ├── db ├── migrate │ ├── 20161228142607_create_players.rb │ ├── 20161229032611_create_games.rb │ ├── 20170812010300_add_stripe_customer_id_to_players_games.rb │ └── 20161229040917_create_players_games.rb ├── seeds.rb └── schema.rb ├── public └── robots.txt ├── Rakefile ├── .gitignore ├── .eslintrc.js ├── LICENSE ├── Gemfile ├── README.md ├── package.json └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webpack/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack/build/*.js 2 | webpack/config/*.js 3 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /Pong Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlafranchi/pong/HEAD/Pong Diagram.png -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | end 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p $PORT -e $RAILS_ENV 2 | worker: bundle exec sidekiq -c 5 -v -q default -e $RAILS_ENV 3 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } 6 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /cable/config.ru: -------------------------------------------------------------------------------- 1 | # cable/config.ru 2 | require_relative '../config/environment' 3 | Rails.application.eager_load! 4 | 5 | run ActionCable.server 6 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /app/channels/games_channel.rb: -------------------------------------------------------------------------------- 1 | class GamesChannel < ApplicationCable::Channel 2 | def subscribed 3 | stream_from "games_channel" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /webpack/test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/channels/pong_channel.rb: -------------------------------------------------------------------------------- 1 | class PongChannel < ApplicationCable::Channel 2 | def subscribed 3 | stream_from "pong_channel_#{params[:game_id]}" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/models/player.rb: -------------------------------------------------------------------------------- 1 | class Player < ActiveRecord::Base 2 | validates_presence_of :name 3 | 4 | has_many :players_games 5 | has_many :games, through: :players_games 6 | end 7 | -------------------------------------------------------------------------------- /webpack/config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"', 3 | RAILS_URL: '"https://vue-rails-pong.herokuapp.com"', 4 | STRIPE_PK: '"pk_live_iSkA2zFdDI7OWeYM7RZqhEHv"' 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | Sidekiq.configure_client do |config| 2 | config.redis = { :size => 1 } 3 | end 4 | 5 | Sidekiq.configure_server do |config| 6 | config.redis = { :size => 16 } 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20161228142607_create_players.rb: -------------------------------------------------------------------------------- 1 | class CreatePlayers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :players do |t| 4 | t.string :name 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: redis 3 | url: redis://localhost:6379 4 | 5 | test: 6 | adapter: async 7 | 8 | production: 9 | adapter: redis 10 | url: <%= ENV['REDISTOGO_URL'] %> 11 | -------------------------------------------------------------------------------- /db/migrate/20161229032611_create_games.rb: -------------------------------------------------------------------------------- 1 | class CreateGames < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :games do |t| 4 | t.integer :status, default: 0 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /db/migrate/20170812010300_add_stripe_customer_id_to_players_games.rb: -------------------------------------------------------------------------------- 1 | class AddStripeCustomerIdToPlayersGames < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :players_games, :stripe_customer_id, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.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 | -------------------------------------------------------------------------------- /config/initializers/stripe.rb: -------------------------------------------------------------------------------- 1 | Rails.configuration.stripe = { 2 | :publishable_key => ENV['STRIPE_PUBLISHABLE_KEY'], 3 | :secret_key => ENV['STRIPE_SECRET_KEY'] 4 | } 5 | 6 | Stripe.api_key = Rails.configuration.stripe[:secret_key] 7 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /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_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /webpack/config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"', 6 | RAILS_URL: '"http://api.example.com"', 7 | STRIPE_PK: '"pk_test_fI98pLJ5HlziNWAA56tW5QlT"' 8 | }) 9 | -------------------------------------------------------------------------------- /webpack/config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"', 6 | RAILS_URL: '"http://localhost:3000"', 7 | STRIPE_PK: '"pk_test_fI98pLJ5HlziNWAA56tW5QlT"' 8 | }) 9 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | identified_by :current_player 4 | 5 | def connect 6 | self.current_player = Player.find_by(name: 'left') 7 | end 8 | 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /webpack/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | webpack/test/unit/coverage 5 | webpack/test/e2e/reports 6 | selenium-debug.log 7 | 8 | # Rails 9 | /.bundle 10 | /db/*.sqlite3 11 | /db/*.sqlite3-journal 12 | /log/* 13 | /tmp/* 14 | !/log/.keep 15 | !/tmp/.keep 16 | .byebug_history 17 | 18 | *.swp 19 | dump.rdb 20 | .idea/ 21 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/api/players_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class PlayersController < ApplicationController 3 | def create 4 | player = Player.find_or_create_by(name: params[:player][:name]) 5 | if player.valid? 6 | render :json => player 7 | else 8 | render :json => {:errors => player.errors.full_messages}, :status => 422 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/channels/left_paddle_channel.rb: -------------------------------------------------------------------------------- 1 | class LeftPaddleChannel < ApplicationCable::Channel 2 | def subscribed 3 | stream_from "left_paddle_channel_#{params[:game_id]}" 4 | end 5 | 6 | def receive(data) 7 | $redis.with do |conn| 8 | conn.set("left:#{params[:game_id]}", data["y"]) 9 | end 10 | ActionCable.server.broadcast "right_paddle_channel_#{params[:game_id]}", data 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/channels/right_paddle_channel.rb: -------------------------------------------------------------------------------- 1 | class RightPaddleChannel < ApplicationCable::Channel 2 | def subscribed 3 | stream_from "right_paddle_channel_#{params[:game_id]}" 4 | end 5 | 6 | def receive(data) 7 | $redis.with do |conn| 8 | conn.set("right:#{params[:game_id]}", data["y"]) 9 | end 10 | ActionCable.server.broadcast "left_paddle_channel_#{params[:game_id]}", data 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/jobs/pong_job.rb: -------------------------------------------------------------------------------- 1 | class PongJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(game) 5 | next_hit = nil 6 | while game.playing? 7 | b = Ball.new(game, next_hit) 8 | b.serve 9 | next_hit = b.next_hit 10 | end 11 | $redis.with do |conn| 12 | conn.del("left:#{game.id}") 13 | end 14 | $redis.with do |conn| 15 | conn.del("right:#{game.id}") 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20161229040917_create_players_games.rb: -------------------------------------------------------------------------------- 1 | class CreatePlayersGames < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :players_games do |t| 4 | t.integer :game_id 5 | t.integer :player_id 6 | t.integer :position 7 | t.integer :score, :default => 0, :limit => 1 8 | end 9 | 10 | add_foreign_key :players_games, :players 11 | add_foreign_key :players_games, :games 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /webpack/test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Hello from 'src/components/Hello' 3 | 4 | describe('Hello.vue', () => { 5 | it('should render correct contents', () => { 6 | const vm = new Vue({ 7 | el: document.createElement('div'), 8 | render: (h) => h(Hello) 9 | }) 10 | expect(vm.$el.querySelector('.hello h1').textContent) 11 | .to.equal('Welcome to Your Vue.js App') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | 9 | Player.create(name: 'left') 10 | Player.create(name: 'right') 11 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 3 | namespace :api, :defaults => {format: 'json'} do 4 | resources :games, :except => [:show, :new, :edit] 5 | resources :players, :only => [:create] 6 | resources :players_games, :only => [:create] 7 | resources :leaders, :only => [:index] 8 | resources :donations, :only => [:create] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/controllers/api/games_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Api 4 | class GamesControllerTest < ActionDispatch::IntegrationTest 5 | test "GET #index" do 6 | get api_games_path, :xhr => true 7 | assert_response :success 8 | end 9 | 10 | test "POST #create" do 11 | assert_difference('Game.count', 1) do 12 | post api_games_path, :xhr => true 13 | end 14 | assert Game.last.waiting? 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/api/players_games_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class PlayersGamesController < ApplicationController 3 | def create 4 | players_game = PlayersGame.new(players_game_params) 5 | players_game.save! 6 | render :json => players_game 7 | end 8 | 9 | private 10 | 11 | def players_game_params 12 | params.require(:players_game).permit( 13 | :game_id, 14 | :player_id, 15 | :position, 16 | :score 17 | ) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/api/games_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class GamesController < ApplicationController 3 | def index 4 | render :json => Game.where("status != 2").order(id: :asc).limit(5).to_json({:methods => [:left_player, :right_player]}) 5 | end 6 | 7 | def create 8 | if Game.where("status != 2").count <= 5 9 | render :json => Game.create 10 | else 11 | render :json => {error: "Games are limited to 5 at a time, please join an existing game."}, status: 422 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /webpack/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App' 3 | import ActionCable from 'actioncable' 4 | import axios from 'axios' 5 | var webSocketProtocol = process.env.NODE_ENV === 'production' ? 'wss' : 'ws' 6 | const cable = ActionCable.createConsumer(webSocketProtocol + '://' + process.env.RAILS_URL.replace(/.*?:\/\//g, '') + '/cable') 7 | 8 | Vue.prototype.$http = axios 9 | Vue.prototype.$cable = cable 10 | 11 | /* eslint-disable no-new */ 12 | new Vue({ 13 | el: '#app', 14 | template: '', 15 | components: { App } 16 | }) 17 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | if spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 13 | gem 'spring', spring.version 14 | require 'spring/binstub' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/initializers/redis.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | require 'connection_pool' 3 | 4 | begin 5 | if Rails.env.production? 6 | uri = URI.parse(ENV["REDISTOGO_URL"]) 7 | $redis = ConnectionPool.new(:size => 6, :timeout => 3) do 8 | Redis.new(:host => uri.host, :port => uri.port, :password => uri.password) 9 | end 10 | else 11 | $redis = ConnectionPool.new(:size => 6, :timeout => 3) do 12 | Redis.new(:host => Rails.configuration.redis_host, :port => Rails.configuration.redis_port) 13 | end 14 | end 15 | rescue Exception => e 16 | Rails.logger.error e.backtrace 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/api/donations_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class DonationsController < ApplicationController 3 | def create 4 | charge = StripeWrapper::Charge.create(donation_params.to_h) 5 | if charge.successful? 6 | head :ok 7 | else 8 | render json: {error: charge.error_message}, status: :unprocessable_entity 9 | end 10 | end 11 | 12 | private 13 | 14 | def donation_params 15 | params.require(:donation).permit( 16 | :description, 17 | :amount, 18 | :currency, 19 | :source 20 | ) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /webpack/test/unit/index.js: -------------------------------------------------------------------------------- 1 | // Polyfill fn.bind() for PhantomJS 2 | /* eslint-disable no-extend-native */ 3 | Function.prototype.bind = require('function-bind') 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /app/models/players_game.rb: -------------------------------------------------------------------------------- 1 | class PlayersGame < ApplicationRecord 2 | enum position: [:left, :right] 3 | belongs_to :player 4 | belongs_to :game 5 | validates :game_id, uniqueness: {scope: [:position]} 6 | validates :player_id, uniqueness: {scope: [:game_id]} 7 | 8 | after_save :update_game 9 | after_commit :broadcast_game 10 | 11 | private 12 | 13 | def update_game 14 | if game.waiting? && game.players_games.count == 2 15 | game.playing! 16 | elsif game.playing? && score >= 10 17 | game.over! 18 | end 19 | end 20 | 21 | def broadcast_game 22 | game.broadcast 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/stripe_wrapper.rb: -------------------------------------------------------------------------------- 1 | module StripeWrapper 2 | class Charge 3 | attr_reader :response, :error_message 4 | def initialize(response, error_message) 5 | @response = response 6 | @error_message = error_message 7 | end 8 | def self.create(options={}) 9 | begin 10 | response = Stripe::Charge.create(options) 11 | new(response, nil) 12 | rescue Stripe::CardError => e 13 | new(nil, e.message) 14 | end 15 | end 16 | def successful? 17 | @response.present? 18 | end 19 | def error_message 20 | @error_message 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /webpack/src/components/Paddle.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 32 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 8 | extends: 'standard', 9 | // required to lint *.vue files 10 | plugins: [ 11 | 'html' 12 | ], 13 | // add your custom rules here 14 | 'rules': { 15 | // allow paren-less arrow functions 16 | 'arrow-parens': 0, 17 | // allow async-await 18 | 'generator-star-spacing': 0, 19 | // allow debugger during development 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webpack/test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | allow do 10 | if Rails.env.production? 11 | origins 'vue-rails-pong.herokuapp.com' 12 | else 13 | origins 'localhost:8080' 14 | end 15 | 16 | resource '*', 17 | headers: :any, 18 | methods: [:get, :post, :put, :patch, :delete, :options, :head] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /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. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /app/controllers/api/leaders_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class LeadersController < ApplicationController 3 | def index 4 | render :json => leader_board 5 | end 6 | 7 | private 8 | 9 | def leader_board 10 | Player.find_by_sql(%q{ 11 | SELECT p.name, 12 | sum(pg.score) points, 13 | count(pg.id) total_games, 14 | sum(case when pg.score = 10 then 1 else 0 end) games_won 15 | FROM players p 16 | LEFT JOIN players_games pg on pg.player_id = p.id 17 | GROUP BY p.name 18 | ORDER BY sum(case when pg.score = 10 then 1 else 0 end) DESC, count(pg.id) ASC, sum(pg.score) DESC 19 | LIMIT 5 20 | }) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /test/controllers/api/players_games_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Api 4 | class PlayersGamesControllerTest < ActionDispatch::IntegrationTest 5 | test "POST #create" do 6 | player = Player.create(name: 'bob') 7 | game = Game.create 8 | assert_nil game.left_player 9 | assert_difference('PlayersGame.count', 1) do 10 | post api_players_games_path, params: { 11 | players_game: { 12 | player_id: player.id, 13 | game_id: game.id, 14 | position: 'left' 15 | } 16 | } 17 | end 18 | assert_response :success 19 | assert_equal player, game.left_player 20 | assert_equal 0, PlayersGame.last.score 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | # url: <%= ENV["HEROKU_POSTGRESQL_YELLOW_URL"] %> 24 | production: 25 | url: <%= ENV["DATABASE_URL"] %> 26 | pool: <%= ENV["DB_POOL"] || ENV['RAILS_MAX_THREADS'] || 5 %> 27 | -------------------------------------------------------------------------------- /test/controllers/api/players_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PlayersControllerTest < ActionDispatch::IntegrationTest 4 | test "POST #create" do 5 | assert_difference('Player.count', 1) do 6 | post api_players_path, params: { 7 | player: { 8 | name: 'bob' 9 | } 10 | } 11 | end 12 | assert_response :success 13 | end 14 | 15 | test "POST #create twice" do 16 | assert_difference('Player.count', 1) do 17 | post api_players_path, params: { 18 | player: { 19 | name: 'bob' 20 | } 21 | } 22 | end 23 | assert_difference('Player.count', 0) do 24 | post api_players_path, params: { 25 | player: { 26 | name: 'bob' 27 | } 28 | } 29 | end 30 | assert_response :success 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Read the Rails 5.0 release notes for more info on each option. 6 | 7 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 8 | # Previous versions had false. 9 | ActiveSupport.to_time_preserves_timezone = true 10 | 11 | # Require `belongs_to` associations by default. Previous versions had false. 12 | Rails.application.config.active_record.belongs_to_required_by_default = true 13 | 14 | # Do not halt callback chains when a callback returns false. Previous versions had true. 15 | ActiveSupport.halt_callback_chains_on_return_false = false 16 | 17 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 18 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 19 | -------------------------------------------------------------------------------- /webpack/test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count 11 | this.expected = count 12 | this.pass = function (val) { 13 | return val === this.expected 14 | } 15 | this.value = function (res) { 16 | return res.value 17 | } 18 | this.command = function (cb) { 19 | var self = this 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length 22 | }, [selector], function (res) { 23 | cb.call(self, res) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/models/game_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class GameTest < ActionDispatch::IntegrationTest 4 | def setup 5 | @game = Game.create 6 | bob = Player.create(name: 'bob') 7 | alice = Player.create(name: 'alice') 8 | @pg_left = PlayersGame.create(game_id: @game.id, player_id: bob.id, position: 'left') 9 | @pg_right = PlayersGame.create(game_id: @game.id, player_id: alice.id, position: 'right') 10 | @game.reload 11 | assert @game.playing? 12 | end 13 | 14 | test "#score left" do 15 | @game.score('left') 16 | assert_equal 1, @pg_left.reload.score 17 | end 18 | 19 | test "#score right" do 20 | @game.score('right') 21 | assert_equal 1, @pg_right.reload.score 22 | end 23 | 24 | test "#score end game" do 25 | 10.times do 26 | @game.score('right') 27 | end 28 | assert_equal 10, @pg_right.reload.score 29 | assert @game.reload.over? 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /webpack/src/components/Ball.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 34 | 35 | 43 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 7c387c5e11460793741678f2085229d4177b8dcfb96ef64e5f4c4d470964551774cdbb8f8dcedde8a96a6dd86daacadcd96e235d5975316185b889ff95502fb3 15 | 16 | test: 17 | secret_key_base: e0fb80e12dde02a4904ad7ebbd49c342dfb9bb893950c4ba330c385030b0c121d4922fc3fc6e569fd1b808ae422149471b9efe546dbc9e710452078b9834061d 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /webpack/build/build.js: -------------------------------------------------------------------------------- 1 | // https://github.com/shelljs/shelljs 2 | require('./check-versions')() 3 | require('shelljs/global') 4 | env.NODE_ENV = 'production' 5 | 6 | var path = require('path') 7 | var config = require('../config') 8 | var ora = require('ora') 9 | var webpack = require('webpack') 10 | var webpackConfig = require('./webpack.prod.conf') 11 | 12 | console.log( 13 | ' Tip:\n' + 14 | ' Built files are meant to be served over an HTTP server.\n' + 15 | ' Opening index.html over file:// won\'t work.\n' 16 | ) 17 | 18 | var spinner = ora('building for production...') 19 | spinner.start() 20 | 21 | var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) 22 | rm('-rf', assetsPath) 23 | mkdir('-p', assetsPath) 24 | cp('-R', './webpack/src/static/*', assetsPath) 25 | 26 | webpack(webpackConfig, function (err, stats) { 27 | spinner.stop() 28 | if (err) throw err 29 | process.stdout.write(stats.toString({ 30 | colors: true, 31 | modules: false, 32 | children: false, 33 | chunks: false, 34 | chunkModules: false 35 | }) + '\n') 36 | }) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Richard LaFranchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /webpack/test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | var server = require('../../build/dev-server.js') 4 | 5 | // 2. run the nightwatch test suite against it 6 | // to run in additional browsers: 7 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 8 | // 2. add it to the --env flag below 9 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 10 | // For more information on Nightwatch's config file, see 11 | // http://nightwatchjs.org/guide#settings-file 12 | var opts = process.argv.slice(2) 13 | if (opts.indexOf('--config') === -1) { 14 | opts = opts.concat(['--config', 'webpack/test/e2e/nightwatch.conf.js']) 15 | } 16 | if (opts.indexOf('--env') === -1) { 17 | opts = opts.concat(['--env', 'chrome']) 18 | } 19 | 20 | var spawn = require('cross-spawn') 21 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 22 | 23 | runner.on('exit', function (code) { 24 | server.close() 25 | process.exit(code) 26 | }) 27 | 28 | runner.on('error', function (err) { 29 | server.close() 30 | throw err 31 | }) 32 | -------------------------------------------------------------------------------- /webpack/config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../../public/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../../public'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'] 18 | }, 19 | dev: { 20 | env: require('./dev.env'), 21 | port: 8080, 22 | assetsSubDirectory: 'static', 23 | assetsPublicPath: '/', 24 | proxyTable: {}, 25 | // CSS Sourcemaps off by default because relative paths are "buggy" 26 | // with this option, according to the CSS-Loader README 27 | // (https://github.com/webpack/css-loader#sourcemaps) 28 | // In our experience, they generally work as expected, 29 | // just be aware of this issue when enabling this option. 30 | cssSourceMap: false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/models/game.rb: -------------------------------------------------------------------------------- 1 | class Game < ApplicationRecord 2 | enum status: [:waiting, :playing, :over] 3 | 4 | has_many :players_games 5 | has_many :players, through: :players_games 6 | 7 | after_create :broadcast 8 | after_commit :play 9 | 10 | def left_player 11 | player(:left) 12 | end 13 | 14 | def right_player 15 | player(:right) 16 | end 17 | 18 | def score(position) 19 | pg = players_games.find_by(position: position) 20 | pg.increment(:score).save 21 | end 22 | 23 | def data 24 | game_data = self.attributes 25 | game_data["left_player"] = left_player 26 | game_data["right_player"] = right_player 27 | game_data 28 | end 29 | 30 | def broadcast 31 | ActionCable.server.broadcast "games_channel", data 32 | end 33 | 34 | private 35 | 36 | def select_stmt 37 | "players.id, players.name, players_games.score" 38 | end 39 | 40 | def player(position) 41 | Player 42 | .joins(:players_games) 43 | .select(select_stmt) 44 | .where( 45 | "players_games.game_id = ? AND players_games.position = ?", 46 | self.id, 47 | PlayersGame.positions[position] 48 | ).first 49 | end 50 | 51 | def play 52 | if playing? 53 | PongJob.perform_later(self) 54 | end 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20170812010300) do 14 | 15 | create_table "games", force: :cascade do |t| 16 | t.integer "status", default: 0 17 | end 18 | 19 | create_table "players", force: :cascade do |t| 20 | t.string "name" 21 | end 22 | 23 | create_table "players_games", force: :cascade do |t| 24 | t.integer "game_id" 25 | t.integer "player_id" 26 | t.integer "position" 27 | t.integer "score", limit: 1, default: 0 28 | t.string "stripe_customer_id" 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /test/models/players_game_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Api 4 | class PlayersGameTest < ActiveSupport::TestCase 5 | def setup 6 | @bob = Player.create(name: 'bob') 7 | @alice = Player.create(name: 'alice') 8 | @game = Game.create 9 | end 10 | 11 | test "should only have one left player" do 12 | pg_left = PlayersGame.create(game_id: @game.id, player_id: @bob.id, position: 'left') 13 | pg_double = PlayersGame.create(game_id: @game.id, player_id: @alice.id, position: 'left') 14 | assert_equal 1, PlayersGame.count 15 | assert_not_empty pg_double.errors 16 | end 17 | 18 | test "should only have one right player" do 19 | pg_right = PlayersGame.create(game_id: @game.id, player_id: @bob.id, position: 'right') 20 | pg_double = PlayersGame.create(game_id: @game.id, player_id: @alice.id, position: 'right') 21 | assert_equal 1, PlayersGame.count 22 | assert_not_empty pg_double.errors 23 | end 24 | 25 | test "should set game to playing when both have joined" do 26 | pg_right = PlayersGame.create(game_id: @game.id, player_id: @bob.id, position: 'right') 27 | pg_left = PlayersGame.create(game_id: @game.id, player_id: @alice.id, position: 'left') 28 | assert @game.reload.playing? 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /webpack/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var config = require('../config') 2 | var webpack = require('webpack') 3 | var merge = require('webpack-merge') 4 | var utils = require('./utils') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | 8 | // add hot-reload related code to entry chunks 9 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 10 | baseWebpackConfig.entry[name] = ['./webpack/build/dev-client'].concat(baseWebpackConfig.entry[name]) 11 | }) 12 | 13 | module.exports = merge(baseWebpackConfig, { 14 | module: { 15 | loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 16 | }, 17 | // eval-source-map is faster for development 18 | devtool: '#eval-source-map', 19 | plugins: [ 20 | new webpack.DefinePlugin({ 21 | 'process.env': config.dev.env 22 | }), 23 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 24 | new webpack.optimize.OccurenceOrderPlugin(), 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'webpack/index.html', 31 | inject: true 32 | }) 33 | ] 34 | }) 35 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "action_cable/engine" 12 | # require "sprockets/railtie" 13 | require "rails/test_unit/railtie" 14 | 15 | # Require the gems listed in Gemfile, including any gems 16 | # you've limited to :test, :development, or :production. 17 | Bundler.require(*Rails.groups) 18 | 19 | module Pong 20 | class Application < Rails::Application 21 | # Settings in config/environments/* take precedence over those specified here. 22 | # Application configuration should go into files in config/initializers 23 | # -- all .rb files in that directory are automatically loaded. 24 | 25 | # Only loads a smaller set of middleware suitable for API only apps. 26 | # Middleware like session, flash, cookies can be added back manually. 27 | # Skip views, helpers and assets when generating a new resource. 28 | config.active_job.queue_adapter = :sidekiq 29 | config.action_cable.mount_path = '/cable' 30 | config.action_cable.disable_request_forgery_protection = true 31 | config.api_only = true 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /webpack/test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/guide#settings-file 5 | module.exports = { 6 | "src_folders": ["webpack/test/e2e/specs"], 7 | "output_folder": "webpack/test/e2e/reports", 8 | "custom_assertions_path": ["webpack/test/e2e/custom-assertions"], 9 | 10 | "selenium": { 11 | "start_process": true, 12 | "server_path": "node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.1.jar", 13 | "host": "127.0.0.1", 14 | "port": 4444, 15 | "cli_args": { 16 | "webdriver.chrome.driver": require('chromedriver').path 17 | } 18 | }, 19 | 20 | "test_settings": { 21 | "default": { 22 | "selenium_port": 4444, 23 | "selenium_host": "localhost", 24 | "silent": true, 25 | "globals": { 26 | "devServerURL": "http://localhost:" + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | "chrome": { 31 | "desiredCapabilities": { 32 | "browserName": "chrome", 33 | "javascriptEnabled": true, 34 | "acceptSslCerts": true 35 | } 36 | }, 37 | 38 | "firefox": { 39 | "desiredCapabilities": { 40 | "browserName": "firefox", 41 | "javascriptEnabled": true, 42 | "acceptSslCerts": true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /webpack/build/check-versions.js: -------------------------------------------------------------------------------- 1 | var semver = require('semver') 2 | var chalk = require('chalk') 3 | var packageConfig = require('../../package.json') 4 | var exec = function (cmd) { 5 | return require('child_process') 6 | .execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | { 16 | name: 'npm', 17 | currentVersion: exec('npm --version'), 18 | versionRequirement: packageConfig.engines.npm 19 | } 20 | ] 21 | 22 | module.exports = function () { 23 | var warnings = [] 24 | for (var i = 0; i < versionRequirements.length; i++) { 25 | var mod = versionRequirements[i] 26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 27 | warnings.push(mod.name + ': ' + 28 | chalk.red(mod.currentVersion) + ' should be ' + 29 | chalk.green(mod.versionRequirement) 30 | ) 31 | } 32 | } 33 | 34 | if (warnings.length) { 35 | console.log('') 36 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 37 | console.log() 38 | for (var i = 0; i < warnings.length; i++) { 39 | var warning = warnings[i] 40 | console.log(' ' + warning) 41 | } 42 | console.log() 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | 4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 5 | gem 'rails', '~> 5.0.1', '>= 5.0.1' 6 | # Use Puma as the app server 7 | gem 'puma', '~> 3.0' 8 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 9 | # gem 'jbuilder', '~> 2.5' 10 | # Use Redis adapter to run Action Cable in production 11 | gem 'redis', '~> 3.0' 12 | # Use ActiveModel has_secure_password 13 | # gem 'bcrypt', '~> 3.1.7' 14 | 15 | # Use Capistrano for deployment 16 | # gem 'capistrano-rails', group: :development 17 | 18 | # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible 19 | gem 'rack-cors' 20 | 21 | group :development, :test do 22 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 23 | gem 'byebug', platform: :mri 24 | # Use sqlite3 as the database for Active Record 25 | gem 'sqlite3' 26 | end 27 | 28 | group :development do 29 | gem 'listen', '~> 3.0.5' 30 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 31 | gem 'spring' 32 | gem 'spring-watcher-listen', '~> 2.0.0' 33 | end 34 | 35 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 36 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 37 | group :production do 38 | gem 'pg' 39 | gem 'rails_12factor' 40 | end 41 | 42 | gem 'stripe' 43 | gem 'sidekiq' 44 | gem 'newrelic_rpm' 45 | -------------------------------------------------------------------------------- /webpack/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pong 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /webpack/src/components/LeaderBoard.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pong 2 | 3 | > Pong reinvented using Rails and Vue.js 4 | 5 | This purpose of this app was to demonstrate the use of a progressive JavaScript framework [Vue.js](https://vuejs.org) and Rails ActionCable to build a two player pong game that can be played over the web. 6 | 7 | ## Demo 8 | 9 | Just over 500 games have been played on the demo. I decided to request a $1 donation at the end of the game. If you found this helpful in any way, support the project by playing a game for $1.00 and invite your friends: 10 | 11 | [https://vue-rails-pong.herokuapp.com](https://vue-rails-pong.herokuapp.com) 12 | 13 | ## The Basic Architecture when Playing a Game 14 | 15 | ![Pong Architecture](Pong%20Diagram.png?raw=true) 16 | 17 | There also exists a GamesChannel which broadcasts every time a point is scored or a game changes state. 18 | 19 | ## Latency issue 20 | 21 | There exists a latency issue where a paddle's position may be behind to it's true position at a point in time. This may also exist with the ball's position as seen by the client. I'm sure this is a common issue among multiplayer games, but have no experience in that department so am open to suggestions. 22 | 23 | ## Rails Dev Setup 24 | ```bash 25 | # run the migration 26 | rake db:migrate 27 | 28 | # dev server 29 | rails server 30 | ``` 31 | 32 | ## Webpack Dev Setup 33 | 34 | ``` bash 35 | # install dependencies 36 | npm install 37 | 38 | # serve with hot reload at localhost:8080 39 | npm run dev 40 | ``` 41 | 42 | ## Production Setup 43 | 44 | The app is deployed to heroku using a nodejs buildpack and ruby buildpack. A configuartion in package.json for "heroku-postbuild" which runs `npm run build` before starting the Rails server. 45 | -------------------------------------------------------------------------------- /config/newrelic.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This file configures the New Relic Agent. New Relic monitors Ruby, Java, 3 | # .NET, PHP, Python and Node applications with deep visibility and low 4 | # overhead. For more information, visit www.newrelic.com. 5 | # 6 | # Generated August 12, 2017 7 | # 8 | # This configuration file is custom generated for app61839115@heroku.com 9 | # 10 | # For full documentation of agent configuration options, please refer to 11 | # https://docs.newrelic.com/docs/agents/ruby-agent/installation-configuration/ruby-agent-configuration 12 | 13 | common: &default_settings 14 | # Required license key associated with your New Relic account. 15 | license_key: 2f9d097d1bf8af3d8e884063b5ddb7b3a98d6696 16 | 17 | # Your application name. Renaming here affects where data displays in New 18 | # Relic. For more details, see https://docs.newrelic.com/docs/apm/new-relic-apm/maintenance/renaming-applications 19 | app_name: Pong 20 | 21 | # To disable the agent regardless of other settings, uncomment the following: 22 | # agent_enabled: false 23 | 24 | # Logging level for log/newrelic_agent.log 25 | log_level: info 26 | 27 | 28 | # Environment-specific settings are in this section. 29 | # RAILS_ENV or RACK_ENV (as appropriate) is used to determine the environment. 30 | # If your application has other named environments, configure them here. 31 | development: 32 | <<: *default_settings 33 | app_name: Pong (Development) 34 | 35 | test: 36 | <<: *default_settings 37 | # It doesn't make sense to report to New Relic from automated test runs. 38 | monitor_mode: false 39 | 40 | staging: 41 | <<: *default_settings 42 | app_name: Pong (Staging) 43 | 44 | production: 45 | <<: *default_settings 46 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.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 web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | 41 | # Raises error for missing translations 42 | # config.action_view.raise_on_missing_translations = true 43 | 44 | # Use an evented file watcher to asynchronously detect changes in source code, 45 | # routes, locales, etc. This feature depends on the listen gem. 46 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 47 | 48 | config.redis_host = 'localhost' 49 | config.redis_port = 6379 50 | end 51 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.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 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 16 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /webpack/build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | // generate loader string to be used with extract text plugin 15 | function generateLoaders (loaders) { 16 | var sourceLoader = loaders.map(function (loader) { 17 | var extraParamChar 18 | if (/\?/.test(loader)) { 19 | loader = loader.replace(/\?/, '-loader?') 20 | extraParamChar = '&' 21 | } else { 22 | loader = loader + '-loader' 23 | extraParamChar = '?' 24 | } 25 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 26 | }).join('!') 27 | 28 | // Extract CSS when that option is specified 29 | // (which is the case during production build) 30 | if (options.extract) { 31 | return ExtractTextPlugin.extract('vue-style-loader', sourceLoader) 32 | } else { 33 | return ['vue-style-loader', sourceLoader].join('!') 34 | } 35 | } 36 | 37 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html 38 | return { 39 | css: generateLoaders(['css']), 40 | postcss: generateLoaders(['css']), 41 | less: generateLoaders(['css', 'less']), 42 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 43 | scss: generateLoaders(['css', 'sass']), 44 | stylus: generateLoaders(['css', 'stylus']), 45 | styl: generateLoaders(['css', 'stylus']) 46 | } 47 | } 48 | 49 | // Generate loaders for standalone style files (outside of .vue) 50 | exports.styleLoaders = function (options) { 51 | var output = [] 52 | var loaders = exports.cssLoaders(options) 53 | for (var extension in loaders) { 54 | var loader = loaders[extension] 55 | output.push({ 56 | test: new RegExp('\\.' + extension + '$'), 57 | loader: loader 58 | }) 59 | } 60 | return output 61 | } 62 | -------------------------------------------------------------------------------- /webpack/test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var path = require('path') 7 | var merge = require('webpack-merge') 8 | var baseConfig = require('../../build/webpack.base.conf') 9 | var utils = require('../../build/utils') 10 | var webpack = require('webpack') 11 | var projectRoot = path.resolve(__dirname, '../../') 12 | 13 | var webpackConfig = merge(baseConfig, { 14 | // use inline sourcemap for karma-sourcemap-loader 15 | module: { 16 | loaders: utils.styleLoaders() 17 | }, 18 | devtool: '#inline-source-map', 19 | vue: { 20 | loaders: { 21 | js: 'isparta' 22 | } 23 | }, 24 | plugins: [ 25 | new webpack.DefinePlugin({ 26 | 'process.env': require('../../config/test.env') 27 | }) 28 | ] 29 | }) 30 | 31 | // no need for app entry during tests 32 | delete webpackConfig.entry 33 | 34 | // make sure isparta loader is applied before eslint 35 | webpackConfig.module.preLoaders = webpackConfig.module.preLoaders || [] 36 | webpackConfig.module.preLoaders.unshift({ 37 | test: /\.js$/, 38 | loader: 'isparta', 39 | include: path.resolve(projectRoot, 'src') 40 | }) 41 | 42 | // only apply babel for test files when using isparta 43 | webpackConfig.module.loaders.some(function (loader, i) { 44 | if (loader.loader === 'babel') { 45 | loader.include = path.resolve(projectRoot, 'test/unit') 46 | return true 47 | } 48 | }) 49 | 50 | module.exports = function (config) { 51 | config.set({ 52 | // to run in additional browsers: 53 | // 1. install corresponding karma launcher 54 | // http://karma-runner.github.io/0.13/config/browsers.html 55 | // 2. add it to the `browsers` array below. 56 | browsers: ['PhantomJS'], 57 | frameworks: ['mocha', 'sinon-chai'], 58 | reporters: ['spec', 'coverage'], 59 | files: ['./index.js'], 60 | preprocessors: { 61 | './index.js': ['webpack', 'sourcemap'] 62 | }, 63 | webpack: webpackConfig, 64 | webpackMiddleware: { 65 | noInfo: true 66 | }, 67 | coverageReporter: { 68 | dir: './coverage', 69 | reporters: [ 70 | { type: 'lcov', subdir: '.' }, 71 | { type: 'text-summary' } 72 | ] 73 | } 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /webpack/build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | var config = require('../config') 3 | if (!process.env.NODE_ENV) process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 4 | var path = require('path') 5 | var webpack = require('webpack') 6 | var express = require('express') 7 | var opn = require('opn') 8 | var webpackConfig = process.env.NODE_ENV === 'testing' 9 | ? require('./webpack.prod.conf') 10 | : require('./webpack.dev.conf') 11 | 12 | // default port where dev server listens for incoming traffic 13 | var port = process.env.PORT || config.dev.port 14 | // Define HTTP proxies to your custom API backend 15 | // https://github.com/chimurai/http-proxy-middleware 16 | var proxyTable = config.dev.proxyTable 17 | 18 | var app = express() 19 | var compiler = webpack(webpackConfig) 20 | 21 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 22 | publicPath: webpackConfig.output.publicPath, 23 | stats: { 24 | colors: true, 25 | chunks: false 26 | } 27 | }) 28 | 29 | var hotMiddleware = require('webpack-hot-middleware')(compiler) 30 | //// force page reload when html-webpack-plugin template changes 31 | compiler.plugin('compilation', function (compilation) { 32 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 33 | hotMiddleware.publish({ action: 'reload' }) 34 | cb() 35 | }) 36 | }) 37 | 38 | // proxy api requests 39 | Object.keys(proxyTable).forEach(function (context) { 40 | var options = proxyTable[context] 41 | if (typeof options === 'string') { 42 | options = { target: options } 43 | } 44 | app.use(proxyMiddleware(context, options)) 45 | }) 46 | 47 | // handle fallback for HTML5 history API 48 | app.use(require('connect-history-api-fallback')()) 49 | 50 | // serve webpack bundle output 51 | app.use(devMiddleware) 52 | 53 | // enable hot-reload and state-preserving 54 | // compilation error display 55 | app.use(hotMiddleware) 56 | 57 | // serve pure static assets 58 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 59 | app.use(staticPath, express.static('./static')) 60 | 61 | module.exports = app.listen(port, function (err) { 62 | if (err) { 63 | console.log(err) 64 | return 65 | } 66 | var uri = 'http://localhost:' + port 67 | console.log('Listening at ' + uri + '\n') 68 | 69 | // when env is testing, don't need open it 70 | if (process.env.NODE_ENV !== 'testing') { 71 | opn(uri) 72 | } 73 | }) 74 | -------------------------------------------------------------------------------- /webpack/src/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 65 | 66 | 75 | -------------------------------------------------------------------------------- /webpack/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var projectRoot = path.resolve(__dirname, '../') 5 | 6 | var env = process.env.NODE_ENV 7 | // check env & config/index.js to decide weither to enable CSS Sourcemaps for the 8 | // various preprocessor loaders added to vue-loader at the end of this file 9 | var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap) 10 | var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap) 11 | var useCssSourceMap = cssSourceMapDev || cssSourceMapProd 12 | 13 | module.exports = { 14 | entry: { 15 | app: ['babel-polyfill', './webpack/src/main.js'] 16 | }, 17 | output: { 18 | path: config.build.assetsRoot, 19 | publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, 20 | filename: '[name].js' 21 | }, 22 | resolve: { 23 | extensions: ['', '.js', '.vue'], 24 | fallback: [path.join(__dirname, '../../node_modules')], 25 | alias: { 26 | 'vue$': 'vue/dist/vue.common.js', 27 | 'src': path.resolve(__dirname, '../src'), 28 | 'assets': path.resolve(__dirname, '../src/assets'), 29 | 'components': path.resolve(__dirname, '../src/components') 30 | } 31 | }, 32 | resolveLoader: { 33 | fallback: [path.join(__dirname, '../../node_modules')] 34 | }, 35 | module: { 36 | preLoaders: [ 37 | { 38 | test: /\.vue$/, 39 | loader: 'eslint', 40 | include: projectRoot, 41 | exclude: /node_modules/ 42 | }, 43 | { 44 | test: /\.js$/, 45 | loader: 'eslint', 46 | include: projectRoot, 47 | exclude: /node_modules/ 48 | } 49 | ], 50 | loaders: [ 51 | { 52 | test: /\.vue$/, 53 | loader: 'vue' 54 | }, 55 | { 56 | test: /\.js$/, 57 | loader: 'babel', 58 | include: projectRoot, 59 | exclude: /node_modules/ 60 | }, 61 | { 62 | test: /\.json$/, 63 | loader: 'json' 64 | }, 65 | { 66 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 67 | loader: 'url', 68 | query: { 69 | limit: 10000, 70 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 71 | } 72 | }, 73 | { 74 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 75 | loader: 'url', 76 | query: { 77 | limit: 10000, 78 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 79 | } 80 | } 81 | ] 82 | }, 83 | eslint: { 84 | formatter: require('eslint-friendly-formatter') 85 | }, 86 | vue: { 87 | loaders: utils.cssLoaders({ sourceMap: useCssSourceMap }), 88 | postcss: [ 89 | require('autoprefixer')({ 90 | browsers: ['last 2 versions'] 91 | }) 92 | ] 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pong", 3 | "version": "1.0.0", 4 | "description": "Pong reinvented using Rails and Vue.js", 5 | "author": "Richard LaFranchi ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node webpack/build/dev-server.js", 9 | "build": "node webpack/build/build.js", 10 | "unit": "karma start webpack/test/unit/karma.conf.js --single-run", 11 | "e2e": "node webpack/test/e2e/runner.js", 12 | "test": "npm run unit && npm run e2e", 13 | "lint": "eslint --ext .js,.vue src webpack/test/unit/specs webpack/test/e2e/specs", 14 | "heroku-postbuild": "npm run build" 15 | }, 16 | "dependencies": { 17 | "actioncable": "^5.0.1", 18 | "autoprefixer": "^6.4.0", 19 | "axios": "^0.15.3", 20 | "babel-core": "^6.0.0", 21 | "babel-eslint": "^7.0.0", 22 | "babel-loader": "^6.0.0", 23 | "babel-plugin-transform-runtime": "^6.0.0", 24 | "babel-polyfill": "^6.23.0", 25 | "babel-preset-es2015": "^6.0.0", 26 | "babel-preset-stage-2": "^6.0.0", 27 | "babel-register": "^6.0.0", 28 | "chai": "^3.5.0", 29 | "chalk": "^1.1.3", 30 | "chromedriver": "^2.21.2", 31 | "connect-history-api-fallback": "^1.1.0", 32 | "cross-spawn": "^4.0.2", 33 | "css-loader": "^0.25.0", 34 | "eslint": "^3.7.1", 35 | "eslint-config-standard": "^6.1.0", 36 | "eslint-friendly-formatter": "^2.0.5", 37 | "eslint-loader": "^1.5.0", 38 | "eslint-plugin-html": "^1.3.0", 39 | "eslint-plugin-promise": "^2.0.1", 40 | "eslint-plugin-standard": "^2.0.1", 41 | "eventsource-polyfill": "^0.9.6", 42 | "express": "^4.13.3", 43 | "extract-text-webpack-plugin": "^1.0.1", 44 | "file-loader": "^0.9.0", 45 | "function-bind": "^1.0.2", 46 | "html-webpack-plugin": "^2.8.1", 47 | "http-proxy-middleware": "^0.17.2", 48 | "inject-loader": "^2.0.1", 49 | "isparta-loader": "^2.0.0", 50 | "json-loader": "^0.5.4", 51 | "karma": "^1.3.0", 52 | "karma-coverage": "^1.1.1", 53 | "karma-mocha": "^1.2.0", 54 | "karma-phantomjs-launcher": "^1.0.0", 55 | "karma-sinon-chai": "^1.2.0", 56 | "karma-sourcemap-loader": "^0.3.7", 57 | "karma-spec-reporter": "0.0.26", 58 | "karma-webpack": "^1.7.0", 59 | "lolex": "^1.4.0", 60 | "mocha": "^3.1.0", 61 | "nightwatch": "^0.9.8", 62 | "opn": "^4.0.2", 63 | "ora": "^0.3.0", 64 | "phantomjs-prebuilt": "^2.1.3", 65 | "selenium-server": "2.53.1", 66 | "semver": "^5.3.0", 67 | "shelljs": "^0.7.4", 68 | "sinon": "^1.17.3", 69 | "sinon-chai": "^2.8.0", 70 | "underscore": "^1.8.3", 71 | "url-loader": "^0.5.7", 72 | "vue": "^2.1.0", 73 | "vue-loader": "^10.0.0", 74 | "vue-style-loader": "^1.0.0", 75 | "vue-template-compiler": "^2.1.0", 76 | "webpack": "^1.13.2", 77 | "webpack-dev-middleware": "^1.8.3", 78 | "webpack-hot-middleware": "^2.12.2", 79 | "webpack-merge": "^0.14.1" 80 | }, 81 | "engines": { 82 | "node": ">= 4.0.0", 83 | "npm": ">= 3.0.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | 22 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 23 | # config.action_controller.asset_host = 'http://assets.example.com' 24 | 25 | # Specifies the header that your server uses for sending files. 26 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 27 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 28 | 29 | # Mount Action Cable outside main process or domain 30 | # config.action_cable.mount_path = nil 31 | # config.action_cable.url = 'wss://example.com/cable' 32 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 33 | 34 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 35 | config.force_ssl = true 36 | 37 | # Use the lowest log level to ensure availability of diagnostic information 38 | # when problems arise. 39 | config.log_level = :debug 40 | 41 | # Prepend all log lines with the following tags. 42 | config.log_tags = [ :request_id ] 43 | 44 | # Use a different cache store in production. 45 | # config.cache_store = :mem_cache_store 46 | 47 | # Use a real queuing backend for Active Job (and separate queues per environment) 48 | # config.active_job.queue_adapter = :resque 49 | # config.active_job.queue_name_prefix = "pong_#{Rails.env}" 50 | config.action_mailer.perform_caching = false 51 | 52 | # Ignore bad email addresses and do not raise email delivery errors. 53 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 54 | # config.action_mailer.raise_delivery_errors = false 55 | 56 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 57 | # the I18n.default_locale when a translation cannot be found). 58 | config.i18n.fallbacks = true 59 | 60 | # Send deprecation notices to registered listeners. 61 | config.active_support.deprecation = :notify 62 | 63 | # Use default logging formatter so that PID and timestamp are not suppressed. 64 | config.log_formatter = ::Logger::Formatter.new 65 | 66 | # Use a different logger for distributed setups. 67 | # require 'syslog/logger' 68 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 69 | 70 | if ENV["RAILS_LOG_TO_STDOUT"].present? 71 | logger = ActiveSupport::Logger.new(STDOUT) 72 | logger.formatter = config.log_formatter 73 | config.logger = ActiveSupport::TaggedLogging.new(logger) 74 | end 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | end 79 | -------------------------------------------------------------------------------- /webpack/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var env = process.env.NODE_ENV === 'testing' 10 | ? require('../config/test.env') 11 | : config.build.env 12 | 13 | var webpackConfig = merge(baseWebpackConfig, { 14 | module: { 15 | loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }) 16 | }, 17 | devtool: config.build.productionSourceMap ? '#source-map' : false, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 21 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 22 | }, 23 | vue: { 24 | loaders: utils.cssLoaders({ 25 | sourceMap: config.build.productionSourceMap, 26 | extract: true 27 | }) 28 | }, 29 | plugins: [ 30 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 31 | new webpack.DefinePlugin({ 32 | 'process.env': env 33 | }), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | } 38 | }), 39 | new webpack.optimize.OccurrenceOrderPlugin(), 40 | // extract css into its own file 41 | new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')), 42 | // generate dist index.html with correct asset hash for caching. 43 | // you can customize output by editing /index.html 44 | // see https://github.com/ampedandwired/html-webpack-plugin 45 | new HtmlWebpackPlugin({ 46 | filename: process.env.NODE_ENV === 'testing' 47 | ? 'index.html' 48 | : config.build.index, 49 | template: 'webpack/index.html', 50 | inject: true, 51 | minify: { 52 | removeComments: true, 53 | collapseWhitespace: true, 54 | removeAttributeQuotes: true 55 | // more options: 56 | // https://github.com/kangax/html-minifier#options-quick-reference 57 | }, 58 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 59 | chunksSortMode: 'dependency' 60 | }), 61 | // split vendor js into its own file 62 | new webpack.optimize.CommonsChunkPlugin({ 63 | name: 'vendor', 64 | minChunks: function (module, count) { 65 | // any required modules inside node_modules are extracted to vendor 66 | return ( 67 | module.resource && 68 | /\.js$/.test(module.resource) && 69 | module.resource.indexOf( 70 | path.join(__dirname, '../../node_modules') 71 | ) === 0 72 | ) 73 | } 74 | }), 75 | // extract webpack runtime and module manifest to its own file in order to 76 | // prevent vendor hash from being updated whenever app bundle is updated 77 | new webpack.optimize.CommonsChunkPlugin({ 78 | name: 'manifest', 79 | chunks: ['vendor'] 80 | }) 81 | ] 82 | }) 83 | 84 | if (config.build.productionGzip) { 85 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 86 | 87 | webpackConfig.plugins.push( 88 | new CompressionWebpackPlugin({ 89 | asset: '[path].gz[query]', 90 | algorithm: 'gzip', 91 | test: new RegExp( 92 | '\\.(' + 93 | config.build.productionGzipExtensions.join('|') + 94 | ')$' 95 | ), 96 | threshold: 10240, 97 | minRatio: 0.8 98 | }) 99 | ) 100 | } 101 | 102 | module.exports = webpackConfig 103 | -------------------------------------------------------------------------------- /app/models/ball.rb: -------------------------------------------------------------------------------- 1 | class Ball 2 | attr_reader :next_hit 3 | TABLE_WIDTH = 650 4 | TABLE_HEIGHT = 480 5 | BALL_HEIGHT = 15 6 | BALL_WIDTH = 15 7 | PADDLE_HEIGHT = 80 8 | VALID_SLOPES = [0, 0.0875, 0.2679, 0.5773, 1] 9 | 10 | def initialize(game, next_hit=nil) 11 | @game = game 12 | @difficulty = 5 13 | @x = 318 14 | @y = 232 15 | next_hit ||= 'left' 16 | @next_hit = next_hit 17 | @direction_x = nil 18 | @direction_y = nil 19 | end 20 | 21 | def serve 22 | @direction_x = @next_hit == 'left' ? -1 : 1 23 | @direction_y = VALID_SLOPES.sample 24 | Rails.logger.debug "DIRECTION: #{@direction_y}" 25 | if [true, false].sample 26 | @direction_y = -@direction_y 27 | end 28 | loop do 29 | broadcast 30 | update 31 | break if point_scored? 32 | sleep 0.01 33 | end 34 | tally 35 | end 36 | 37 | private 38 | 39 | def update 40 | if next_y >= top_y || next_y <= bottom_y 41 | @direction_y = -@direction_y 42 | end 43 | deflect if will_hit? 44 | @x += (@difficulty * @direction_x) 45 | @y += (@difficulty * @direction_y) 46 | end 47 | 48 | def point_scored? 49 | @x <= left_goal_x || @x >= right_goal_x 50 | end 51 | 52 | def will_hit? 53 | upper_paddle = next_paddle_y + PADDLE_HEIGHT + BALL_HEIGHT 54 | top_of = point_of_contact + BALL_HEIGHT 55 | ((next_x <= left_deflection_x && @x > left_deflection_x) || 56 | (next_x >= right_deflection_x && @x < right_deflection_x)) && 57 | (top_of >= next_paddle_y && top_of <= upper_paddle) 58 | end 59 | 60 | def point_of_contact 61 | delta_x = paddle_x - @x 62 | delta_y = @direction_y * delta_x 63 | @y + delta_y 64 | end 65 | 66 | def deflect 67 | poc = point_of_contact 68 | @x = paddle_x 69 | @y = poc 70 | relative_point = poc + (BALL_HEIGHT / 2) - (PADDLE_HEIGHT / 2) - next_paddle_y 71 | slope_index = (relative_point).abs.ceil 72 | slope_index = slope_index > 4 ? 4 : slope_index 73 | @direction_y = VALID_SLOPES[slope_index] 74 | @direction_y = relative_point < 0 ? -@direction_y : @direction_y 75 | @direction_x = -@direction_x 76 | Rails.logger.debug "DEFLECT...." 77 | Rails.logger.debug "contact: #{poc}" 78 | Rails.logger.debug "relative: #{relative_point}" 79 | Rails.logger.debug "paddle: #{next_paddle_y}" 80 | toggle_next_hit 81 | end 82 | 83 | def next_y 84 | @y + (@difficulty * @direction_y) 85 | end 86 | 87 | def next_x 88 | @x + (@difficulty * @direction_x) 89 | end 90 | 91 | def toggle_next_hit 92 | @next_hit = @next_hit == 'left' ? 'right' : 'left' 93 | end 94 | 95 | def next_paddle_y 96 | @next_hit == 'left' ? left_paddle_y : right_paddle_y 97 | end 98 | 99 | def left_paddle_y 100 | result = 200 101 | $redis.with do |conn| 102 | result = conn.get("left:#{@game.id}").to_i || result 103 | end 104 | return result 105 | end 106 | 107 | def right_paddle_y 108 | result = 200 109 | $redis.with do |conn| 110 | result = conn.get("right:#{@game.id}").to_i || result 111 | end 112 | return result 113 | end 114 | 115 | def paddle_x 116 | @next_hit == 'left' ? left_deflection_x : right_deflection_x 117 | end 118 | 119 | def left_goal_x 120 | 0 - BALL_WIDTH 121 | end 122 | 123 | def right_goal_x 124 | TABLE_WIDTH 125 | end 126 | 127 | def left_deflection_x 128 | BALL_WIDTH * 3 129 | end 130 | 131 | def right_deflection_x 132 | TABLE_WIDTH - (BALL_WIDTH * 4) 133 | end 134 | 135 | def bottom_y 136 | 0 137 | end 138 | 139 | def top_y 140 | TABLE_HEIGHT - BALL_HEIGHT 141 | end 142 | 143 | def tally 144 | toggle_next_hit 145 | @game.score(@next_hit) 146 | @game.reload 147 | end 148 | 149 | def broadcast 150 | ActionCable.server.broadcast "pong_channel_#{@game.id}", { x: @x, y: @y } 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (5.0.5) 5 | actionpack (= 5.0.5) 6 | nio4r (>= 1.2, < 3.0) 7 | websocket-driver (~> 0.6.1) 8 | actionmailer (5.0.5) 9 | actionpack (= 5.0.5) 10 | actionview (= 5.0.5) 11 | activejob (= 5.0.5) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.0.5) 15 | actionview (= 5.0.5) 16 | activesupport (= 5.0.5) 17 | rack (~> 2.0) 18 | rack-test (~> 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.0.5) 22 | activesupport (= 5.0.5) 23 | builder (~> 3.1) 24 | erubis (~> 2.7.0) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 | activejob (5.0.5) 28 | activesupport (= 5.0.5) 29 | globalid (>= 0.3.6) 30 | activemodel (5.0.5) 31 | activesupport (= 5.0.5) 32 | activerecord (5.0.5) 33 | activemodel (= 5.0.5) 34 | activesupport (= 5.0.5) 35 | arel (~> 7.0) 36 | activesupport (5.0.5) 37 | concurrent-ruby (~> 1.0, >= 1.0.2) 38 | i18n (~> 0.7) 39 | minitest (~> 5.1) 40 | tzinfo (~> 1.1) 41 | arel (7.1.4) 42 | builder (3.2.3) 43 | byebug (9.0.6) 44 | concurrent-ruby (1.0.5) 45 | connection_pool (2.2.1) 46 | erubis (2.7.0) 47 | faraday (0.12.2) 48 | multipart-post (>= 1.2, < 3) 49 | ffi (1.9.18) 50 | globalid (0.4.0) 51 | activesupport (>= 4.2.0) 52 | i18n (0.8.6) 53 | listen (3.0.8) 54 | rb-fsevent (~> 0.9, >= 0.9.4) 55 | rb-inotify (~> 0.9, >= 0.9.7) 56 | loofah (2.0.3) 57 | nokogiri (>= 1.5.9) 58 | mail (2.6.6) 59 | mime-types (>= 1.16, < 4) 60 | method_source (0.8.2) 61 | mime-types (3.1) 62 | mime-types-data (~> 3.2015) 63 | mime-types-data (3.2016.0521) 64 | mini_portile2 (2.2.0) 65 | minitest (5.10.3) 66 | multipart-post (2.0.0) 67 | newrelic_rpm (4.3.0.335) 68 | nio4r (2.1.0) 69 | nokogiri (1.8.0) 70 | mini_portile2 (~> 2.2.0) 71 | pg (0.21.0) 72 | puma (3.9.1) 73 | rack (2.0.3) 74 | rack-cors (1.0.1) 75 | rack-protection (2.0.0) 76 | rack 77 | rack-test (0.6.3) 78 | rack (>= 1.0) 79 | rails (5.0.5) 80 | actioncable (= 5.0.5) 81 | actionmailer (= 5.0.5) 82 | actionpack (= 5.0.5) 83 | actionview (= 5.0.5) 84 | activejob (= 5.0.5) 85 | activemodel (= 5.0.5) 86 | activerecord (= 5.0.5) 87 | activesupport (= 5.0.5) 88 | bundler (>= 1.3.0) 89 | railties (= 5.0.5) 90 | sprockets-rails (>= 2.0.0) 91 | rails-dom-testing (2.0.3) 92 | activesupport (>= 4.2.0) 93 | nokogiri (>= 1.6) 94 | rails-html-sanitizer (1.0.3) 95 | loofah (~> 2.0) 96 | rails_12factor (0.0.3) 97 | rails_serve_static_assets 98 | rails_stdout_logging 99 | rails_serve_static_assets (0.0.5) 100 | rails_stdout_logging (0.0.5) 101 | railties (5.0.5) 102 | actionpack (= 5.0.5) 103 | activesupport (= 5.0.5) 104 | method_source 105 | rake (>= 0.8.7) 106 | thor (>= 0.18.1, < 2.0) 107 | rake (12.0.0) 108 | rb-fsevent (0.10.2) 109 | rb-inotify (0.9.10) 110 | ffi (>= 0.5.0, < 2) 111 | redis (3.3.3) 112 | sidekiq (5.0.4) 113 | concurrent-ruby (~> 1.0) 114 | connection_pool (~> 2.2, >= 2.2.0) 115 | rack-protection (>= 1.5.0) 116 | redis (~> 3.3, >= 3.3.3) 117 | spring (2.0.2) 118 | activesupport (>= 4.2) 119 | spring-watcher-listen (2.0.1) 120 | listen (>= 2.7, < 4.0) 121 | spring (>= 1.2, < 3.0) 122 | sprockets (3.7.1) 123 | concurrent-ruby (~> 1.0) 124 | rack (> 1, < 3) 125 | sprockets-rails (3.2.0) 126 | actionpack (>= 4.0) 127 | activesupport (>= 4.0) 128 | sprockets (>= 3.0.0) 129 | sqlite3 (1.3.13) 130 | stripe (3.2.0) 131 | faraday (~> 0.9) 132 | thor (0.19.4) 133 | thread_safe (0.3.6) 134 | tzinfo (1.2.3) 135 | thread_safe (~> 0.1) 136 | websocket-driver (0.6.5) 137 | websocket-extensions (>= 0.1.0) 138 | websocket-extensions (0.1.2) 139 | 140 | PLATFORMS 141 | ruby 142 | 143 | DEPENDENCIES 144 | byebug 145 | listen (~> 3.0.5) 146 | newrelic_rpm 147 | pg 148 | puma (~> 3.0) 149 | rack-cors 150 | rails (~> 5.0.1, >= 5.0.1) 151 | rails_12factor 152 | redis (~> 3.0) 153 | sidekiq 154 | spring 155 | spring-watcher-listen (~> 2.0.0) 156 | sqlite3 157 | stripe 158 | tzinfo-data 159 | 160 | BUNDLED WITH 161 | 1.15.1 162 | -------------------------------------------------------------------------------- /webpack/src/components/Table.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 117 | 118 | 119 | 182 | -------------------------------------------------------------------------------- /webpack/src/components/GameList.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 208 | --------------------------------------------------------------------------------