Add games to seats and implement tables

This commit is contained in:
Jesse C. Fisher 2016-02-20 02:19:28 -08:00
parent 32194b11b9
commit 92f2806e31
47 changed files with 905 additions and 191 deletions

View File

@ -34,12 +34,10 @@ GEM
minitest (~> 5.1)
thread_safe (~> 0.1)
tzinfo (~> 1.1)
addressable (2.3.8)
addressable (2.4.0)
arel (5.0.1.20140414130214)
ast (2.1.0)
astrolabe (1.3.1)
parser (~> 2.2)
babel-source (5.8.26)
ast (2.2.0)
babel-source (5.8.35)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
execjs (~> 2.0)
@ -62,25 +60,26 @@ GEM
capistrano (>= 3.0.0)
capistrano-postgresql (4.2.1)
capistrano (>= 3.0)
capistrano-rails (1.1.5)
capistrano-rails (1.1.6)
capistrano (~> 3.1)
capistrano-bundler (~> 1.1)
capistrano-rails-console (1.0.0)
capistrano-rails-console (1.0.2)
capistrano (>= 3.1.0, < 4.0.0)
capistrano-rbenv (2.0.3)
capistrano-rbenv (2.0.4)
capistrano (~> 3.1)
sshkit (~> 1.3)
capistrano3-puma (1.2.1)
capistrano (~> 3.0)
puma (>= 2.6)
capybara (2.5.0)
capybara (2.6.2)
addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
chartkick (1.4.1)
childprocess (0.5.6)
childprocess (0.5.9)
ffi (~> 1.0, >= 1.0.11)
cliver (0.3.2)
coderay (1.1.0)
@ -90,38 +89,37 @@ GEM
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.9.1.1)
colorize (0.7.7)
coffee-script-source (1.10.0)
connection_pool (2.2.0)
database_cleaner (1.5.1)
debug_inspector (0.0.2)
devise (3.5.2)
devise (3.5.6)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 3.2.6, < 5)
responders
thread_safe (~> 0.1)
warden (~> 1.2.3)
devise_invitable (1.5.3)
devise_invitable (1.5.5)
actionmailer (>= 3.2.6, < 5)
devise (>= 3.2.0)
diff-lcs (1.2.5)
dotenv (2.0.2)
dotenv-rails (2.0.2)
dotenv (= 2.0.2)
railties (~> 4.0)
dotenv (2.1.0)
dotenv-rails (2.1.0)
dotenv (= 2.1.0)
railties (>= 4.0, < 5.1)
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
erubis (2.7.0)
eventmachine (1.0.8)
eventmachine (1.0.9.1)
execjs (2.6.0)
factory_girl (4.5.0)
activesupport (>= 3.0.0)
factory_girl_rails (4.5.0)
factory_girl_rails (4.6.0)
factory_girl (~> 4.5.0)
railties (>= 3.0.0)
faker (1.5.0)
faker (1.6.1)
i18n (~> 0.5)
ffi (1.9.10)
formatador (0.2.5)
@ -142,7 +140,7 @@ GEM
guard-ctags-bundler (1.4.0)
guard (>= 2.0)
guard-compat (>= 0.1.0)
guard-livereload (2.5.1)
guard-livereload (2.5.2)
em-websocket (~> 0.5)
guard (~> 2.8)
guard-compat (~> 1.0)
@ -177,8 +175,8 @@ GEM
hub (1.12.4)
i18n (0.7.0)
interception (0.5)
jbuilder (2.3.2)
activesupport (>= 3.0.0, < 5)
jbuilder (2.4.1)
activesupport (>= 3.0.0, < 5.1)
multi_json (~> 1.2)
jquery-rails (3.1.4)
railties (>= 3.0, < 5.0)
@ -189,37 +187,37 @@ GEM
activesupport (>= 3.0.0)
launchy (2.4.3)
addressable (~> 2.3)
listen (3.0.3)
listen (3.0.6)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
lumberjack (1.0.9)
rb-inotify (>= 0.9.7)
lumberjack (1.0.10)
mail (2.6.3)
mime-types (>= 1.16, < 3)
method_source (0.8.2)
mime-types (2.6.2)
mini_portile (0.6.2)
minitest (5.8.1)
mime-types (2.99)
mini_portile2 (2.0.0)
minitest (5.8.4)
multi_json (1.11.2)
nenv (0.2.0)
nenv (0.3.0)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (3.0.1)
newrelic_rpm (3.14.1.311)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
net-ssh (3.0.2)
newrelic_rpm (3.14.3.313)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
notiffany (0.0.8)
nenv (~> 0.1)
shellany (~> 0.0)
orm_adapter (0.5.0)
parser (2.2.3.0)
ast (>= 1.1, < 3.0)
pg (0.18.3)
poltergeist (1.7.0)
parser (2.3.0.5)
ast (~> 2.2)
pg (0.18.4)
poltergeist (1.9.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
polyamorous (1.2.0)
polyamorous (1.3.0)
activerecord (>= 3.0)
powerpack (0.1.1)
pry (0.10.3)
@ -231,8 +229,8 @@ GEM
pry-rescue (1.4.2)
interception (>= 0.5)
pry
puma (2.14.0)
pundit (1.0.1)
puma (2.16.0)
pundit (1.1.0)
activesupport (>= 3.0.0)
quiet_assets (1.1.0)
railties (>= 3.1, < 5.0)
@ -251,14 +249,14 @@ GEM
bundler (>= 1.3.0, < 2.0)
railties (= 4.1.8)
sprockets-rails (~> 2.0)
rails_layout (1.0.26)
rails_layout (1.0.29)
railties (4.1.8)
actionpack (= 4.1.8)
activesupport (= 4.1.8)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.0.0)
rake (10.4.2)
rainbow (2.1.0)
rake (10.5.0)
ransack (1.7.0)
actionpack (>= 3.0)
activerecord (>= 3.0)
@ -267,53 +265,53 @@ GEM
polyamorous (~> 1.2)
rb-fchange (0.0.6)
ffi
rb-fsevent (0.9.6)
rb-inotify (0.9.5)
rb-fsevent (0.9.7)
rb-inotify (0.9.7)
ffi (>= 0.5.0)
rdoc (4.2.0)
rdoc (4.2.2)
json (~> 1.4)
react-rails (1.3.2)
react-rails (1.6.0)
babel-transpiler (>= 0.7.0)
coffee-script-source (~> 1.8)
connection_pool
execjs
rails (>= 3.2)
tilt
react-rails-hot-loader (0.4.0)
react-rails-hot-loader (0.5.0)
em-websocket
rails
react-rails
responders (1.1.2)
railties (>= 3.2, < 4.2)
rspec (3.3.0)
rspec-core (~> 3.3.0)
rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0)
rspec-core (3.3.2)
rspec-support (~> 3.3.0)
rspec-expectations (3.3.1)
rspec (3.4.0)
rspec-core (~> 3.4.0)
rspec-expectations (~> 3.4.0)
rspec-mocks (~> 3.4.0)
rspec-core (3.4.2)
rspec-support (~> 3.4.0)
rspec-expectations (3.4.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.3.0)
rspec-mocks (3.3.2)
rspec-support (~> 3.4.0)
rspec-mocks (3.4.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.3.0)
rspec-rails (3.3.3)
rspec-support (~> 3.4.0)
rspec-rails (3.4.2)
actionpack (>= 3.0, < 4.3)
activesupport (>= 3.0, < 4.3)
railties (>= 3.0, < 4.3)
rspec-core (~> 3.3.0)
rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0)
rspec-support (~> 3.3.0)
rspec-support (3.3.0)
rubocop (0.34.2)
astrolabe (~> 1.3)
parser (>= 2.2.2.5, < 3.0)
rspec-core (~> 3.4.0)
rspec-expectations (~> 3.4.0)
rspec-mocks (~> 3.4.0)
rspec-support (~> 3.4.0)
rspec-support (3.4.1)
rubocop (0.37.2)
parser (>= 2.3.0.4, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.4)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 0.3)
ruby-progressbar (1.7.5)
ruby_parser (3.7.1)
ruby_parser (3.7.3)
sexp_processor (~> 4.1)
rubyzip (1.1.7)
sass (3.2.19)
@ -325,18 +323,18 @@ GEM
sdoc (0.4.1)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
selenium-webdriver (2.48.1)
selenium-webdriver (2.52.0)
childprocess (~> 0.5)
multi_json (~> 1.0)
rubyzip (~> 1.0)
websocket (~> 1.0)
sexp_processor (4.6.0)
sexp_processor (4.6.1)
shellany (0.0.1)
simple_form (3.2.0)
actionpack (~> 4.0)
activemodel (~> 4.0)
simple_form (3.2.1)
actionpack (> 4, < 5.1)
activemodel (> 4, < 5.1)
slop (3.6.0)
spring (1.4.0)
spring (1.6.3)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
sprockets (2.12.4)
@ -348,8 +346,7 @@ GEM
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
sshkit (1.7.1)
colorize (>= 0.7.0)
sshkit (1.8.1)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
sysexits (1.2.0)
@ -363,6 +360,7 @@ GEM
uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
unicode-display_width (0.3.1)
upmin-admin (0.1.3)
chartkick
haml (>= 3.0.0)
@ -371,10 +369,10 @@ GEM
rails
ransack
sass-rails
warden (1.2.3)
warden (1.2.6)
rack (>= 1.0)
websocket (1.2.2)
websocket-driver (0.6.2)
websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
xpath (2.0.0)

View File

@ -50,9 +50,10 @@ end
# * zeus: 'zeus rspec' (requires the server to be started separately)
# * 'just' rspec: 'rspec'
guard :rspec, cmd: "bundle exec spring rspec", all_after_pass: false, failed_mode: :focus do #
guard :rspec, cmd: "bundle exec spring rspec", all_after_pass: false do#, failed_mode: :focus
require "guard/rspec/dsl"
dsl = Guard::RSpec::Dsl.new(self)
notification :off
# Feel free to open issues for suggestions and improvements

View File

@ -38,6 +38,7 @@ end
guard 'rails', host: '0.0.0.0' do
watch('Gemfile.lock')
watch(%r{^(config|lib)/.*})
notification :off
end

View File

@ -4,6 +4,8 @@ Thirteen Tien Len
TODO
----
- Refactor seat.jsx buttons in to a <form>.
- 1st 2nd 3rd place messages. End game message.
- Tables to contain the games
@ -36,6 +38,12 @@ first play
- Review attributions and license compliance
- TODO: Refactor guardfiles down to one file using `groups`
https://github.com/guard/guard/wiki/Guardfile-examples
- TODO: Move game logic out of game model / controller to module in
/lib/game_logic.rb e.g. https://github.com/adamof/rails-poker/tree/master/poker
## Proposed Schema
Table

View File

@ -16,5 +16,5 @@
//= require react
//= require react_ujs
//= require components
//= require react-rails-hot-loader
// require react-rails-hot-loader
//= require_tree .

View File

@ -57,7 +57,7 @@ function handleFlashMessagesHeader(node, xhr) {
$(document).ready(function() {
var dummy = new Array();
var flashDiv = React.render(<FlashMessages messages={dummy} />, $('#flash_messages')[0]);
var flashDiv = ReactDOM.render(<FlashMessages messages={dummy} />, $('#flash_messages')[0]);
$(document).ajaxComplete(function(event, xhr, settings) {
handleFlashMessagesHeader(flashDiv, xhr);

View File

@ -5,10 +5,14 @@ var Game = React.createClass({
title: React.PropTypes.string,
players: React.PropTypes.array,
id: React.PropTypes.number,
url: React.PropTypes.string,
play_to_beat_string: React.PropTypes.string
},
url: function() {
var url = "/games/" + this.props.id;
return url;
},
getInitialState: function() {
return {
data: null,
@ -22,7 +26,7 @@ var Game = React.createClass({
},
loadGameFromServer: function() {
$.getJSON(this.props.url, function(result) {
$.getJSON(this.url(), function(result) {
if (this.isMounted()) {
this.setState({ data: result });
}
@ -35,8 +39,9 @@ var Game = React.createClass({
},
handlePlayHandSubmit: function(card_ids) {
console.log("Hello from handlePlayHandSubmit()");
$.ajax({
url: this.props.url + '/play_hand',
url: this.url() + '/play_hand',
type: 'PATCH',
dataType: 'json',
data: card_ids,
@ -45,14 +50,14 @@ var Game = React.createClass({
this.loadGameFromServer();
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
console.error(this.url(), status, err.toString());
}.bind(this)
});
},
handleStart: function() {
$.ajax({
url: this.props.url + '/start',
url: this.url() + '/start',
type: 'POST',
dataType: 'json',
complete: function (jqXHR, textStatus) {
@ -69,7 +74,7 @@ var Game = React.createClass({
handleJoin: function() {
$.ajax({
url: this.props.url + '/join',
url: this.url() + '/join',
type: 'POST',
dataType: 'json',
complete: function (jqXHR, textStatus) {
@ -98,7 +103,7 @@ var Game = React.createClass({
:
<section id='game-controls'>
{/* TODO: If not joinable, show why. i.e. game full*/ }
{this.state.data.is_joinable ? <button onClick={this.handleJoin}>Join</button> : null }
{/* TODO: DEPRECATED use seat sit instead this.state.data.is_joinable ? <button onClick={this.handleJoin}>Join</button> : null */}
{/* TODO: If not startable, show why. i.e. need more players */}
<div id='start'>
{this.state.data.is_started ? null : <button onClick={this.handleStart} disabled={this.state.data.is_startable ? null : "disabled" }>Start</button>}
@ -108,6 +113,8 @@ var Game = React.createClass({
<PlayerList
game={this.state.data}
current_user={this.props.current_user}
onSeatSubmit={this.props.onSeatSubmit}
onPlayHandSubmit={this.handlePlayHandSubmit} />
{this.state.data.winner_player_id ? "Winner: " + this.state.data.winner_player_id : null}

View File

@ -8,10 +8,10 @@ var Player = React.createClass({
return (
<div id={"player-" + this.props.id} className={"player " + (this.props.game.controlling_player_id == this.props.id ? 'has-control' : '')}>
<div className='user-name'>{this.props.email || "Guest" + this.props.soft_token.substr(0,6)}</div>
{this.props.game.is_started
?
{this.props.game.is_started ?
<div>Inventory count: {this.props.inventory_count}</div>
: null }
: null }
{/* Show player controls if player is owned by current_user */}
{(this.props.soft_token == this.props.game.current_user_soft_token) && (this.props.game.is_started == true)

View File

@ -4,6 +4,7 @@ var PlayerList = React.createClass({
game_id: React.PropTypes.number
},
/* TODO: DEPRECATED playerNodes */
playerNodes: function() {
var playerList = this;
var playerNodes = this.props.game.players.map(function (player) {
@ -23,15 +24,45 @@ var PlayerList = React.createClass({
return (
<div id="playerList">
<div id="top">
{this.playerNodes()[0]}
<div className="game-seat position-1">
{console.log("Hello from player_list.jsx")}
{console.log(this.props.onPlayHandSubmit)}
<Seat
{...this.props.game.seats[0]}
game={this.props.game}
onPlayHandSubmit={this.props.onPlayHandSubmit}
current_user={this.props.current_user}
onSeatSubmit={this.props.onSeatSubmit} />
</div>
</div>
<div id="mid">
{this.playerNodes()[1]}
<div className="game-seat position-2">
<Seat
{...this.props.game.seats[1]}
game={this.props.game}
onPlayHandSubmit={this.props.onPlayHandSubmit}
current_user={this.props.current_user}
onSeatSubmit={this.props.onSeatSubmit} />
</div>
<div id="play-to-beat">Hand to beat: {this.props.game.play_to_beat_string}</div>
{this.playerNodes()[2]}
<div className="game-seat position-3">
<Seat
{...this.props.game.seats[2]}
game={this.props.game}
onPlayHandSubmit={this.props.onPlayHandSubmit}
current_user={this.props.current_user}
onSeatSubmit={this.props.onSeatSubmit} />
</div>
</div>
<div id="bot">
{this.playerNodes()[3]}
<div className="game-seat position-4">
<Seat
{...this.props.game.seats[3]}
game={this.props.game}
onPlayHandSubmit={this.props.onPlayHandSubmit}
current_user={this.props.current_user}
onSeatSubmit={this.props.onSeatSubmit} />
</div>
</div>
</div>
);

View File

@ -1,29 +1,68 @@
var Seat = React.createClass({
onClick: function(e) {
// TODO: Sit at table
$.ajax({
url: window.location.pathname + '/sit',
type: 'POST',
dataType: 'json',
complete: function (jqXHR, textStatus) {
// callback
},
success: function (data, textStatus, jqXHR) {
// success callback
},
error: function (jqXHR, textStatus, errorThrown) {
// error callback
}
});
isOccupied: function() {
//TODO: create isOccupied logic
return false;
},
url: function() {
var url = "/seats/" + this.props.id + ".json"
return url;
},
sitClick: function(e) {
this.props.onSeatSubmit(this.props.id, 'sit');
},
standClick: function(e) {
this.props.onSeatSubmit(this.props.id, 'stand');
},
isAvailable: function() {
return this.props.user_display_name == null
},
sitButtonStyle: function() {
var style = {};
if(null != this.props.user_id == this.props.current_user.id)
style = { display: 'none'};
if(this.props.user_soft_token == this.props.current_user.soft_token)
style = { display: 'none'};
return style;
},
standButtonStyle: function() {
var style = { display: 'none'};
if(null != this.props.user_id == this.props.current_user.id)
style = {};
if(this.props.user_soft_token == this.props.current_user.soft_token)
style = {};
return style;
},
render: function() {
return (
<button onClick={this.onClick}>
Sit
</button>
<div className='seat'>
<div className='seat-controls'>
<button
onClick={this.sitClick}
style={ this.sitButtonStyle() }
disabled={!this.isAvailable()} >
Sit
</button>
<button
onClick={this.standClick}
style={ this.standButtonStyle() }>
Stand
</button>
</div>
{this.props.player ?
<Player
game={this.props.game}
onPlayHandSubmit={this.props.onPlayHandSubmit}
{...this.props.player} />
: "Empty" }
</div>
);
}
});

View File

@ -0,0 +1,35 @@
var SeatList = React.createClass({
propTypes: {
seats: React.PropTypes.array
},
seatNodes: function() {
var onSeatSubmit = this.props.onSeatSubmit;
var current_user = this.props.current_user;
var seatNodes = this.props.seats.map(function(seat){
return (
<Seat
key={seat.id}
id={seat.id}
user_id={seat.user_id}
user_soft_token={seat.user_soft_token}
user_display_name={seat.user_display_name}
current_user={current_user}
onSeatSubmit={onSeatSubmit} />
)
})
return seatNodes;
},
render: function() {
return (
<div id="seatList">
{this.seatNodes()[0]}
{this.seatNodes()[1]}
{this.seatNodes()[2]}
{this.seatNodes()[3]}
</div>
);
}
});

View File

@ -14,7 +14,7 @@ var Table = React.createClass({
};
},
componentDidMount: function() {
loadTableFromServer: function() {
$.getJSON(this.props.url, function(result) {
if (this.isMounted()) {
this.setState({ data: result });
@ -22,6 +22,29 @@ var Table = React.createClass({
}.bind(this));
},
componentDidMount: function() {
this.loadTableFromServer();
},
handleSeatSubmit: function(seat_id, action) {
console.log("handleSeatSubmit");
$.ajax({
url: window.location.pathname + '/seats/' + seat_id + '/' + action,
type: 'PATCH',
dataType: 'json',
success: function (data, textStatus, jqXHR) {
// success callback
console.log(data.message);
this.loadTableFromServer();
}.bind(this),
error: function (jqXHR, textStatus, errorThrown) {
// error callback
console.log(data.message);
}
});
},
render: function() {
if (!this.state.data) {
return <div>Loading...</div>;
@ -29,10 +52,8 @@ var Table = React.createClass({
return (
<div id={"table-" + this.state.data.id}>
<h1>{this.state.data.title}</h1>
<Seat />
<Seat />
<Seat />
<Seat />
{/* TODO: DEPRECATED seats are now rendered within games <SeatList current_user={this.state.data.current_user} seats={this.state.data.seats} onSeatSubmit={this.handleSeatSubmit} /> */}
<Game id={this.state.data.current_game_id} current_user={this.state.data.current_user} onSeatSubmit={this.handleSeatSubmit} />
</div>
);
}

View File

@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@ -0,0 +1,3 @@
// Place all the styles related to the Seat controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@ -4,11 +4,18 @@ class GamesController < ApplicationController
respond_to :html, :json
def index
@games = Game.all.order :id
if params[:table_id]
@table = Table.find(params[:table_id])
@games = @table.games
else
@games = Game.all.order :id
end
respond_with(@games)
end
def show
#TODO: Implement standalone games, without a table?
#redirect_to @game.table
respond_with(@game)
end
@ -98,7 +105,8 @@ class GamesController < ApplicationController
def play_now
# First joinable game or create new game
@game = Game.play_now(current_user)
@game.add_player_from_user current_user
@seats = @game.seats.order(:position).map {|s| s.occupied? ? nil : s}
@seats.compact.last.sit(current_user)
redirect_to @game
end

View File

@ -0,0 +1,85 @@
class SeatsController < ApplicationController
before_action :set_seat, only: [:show, :edit, :update, :destroy, :sit, :stand]
respond_to :html, :json
# GET /seats
# GET /seats.json
def index
@seats = Seat.all
end
# GET /seats/1
# GET /seats/1.json
def show
end
# GET /seats/new
def new
@seat = Seat.new
end
# GET /seats/1/edit
def edit
end
# POST /seats
# POST /seats.json
def create
@seat = Seat.new(seat_params)
respond_to do |format|
if @seat.save
format.html { redirect_to @seat, notice: 'Seat was successfully created.' }
format.json { render :show, status: :created, location: @seat }
else
format.html { render :new }
format.json { render json: @seat.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /seats/1
# PATCH/PUT /seats/1.json
def update
respond_to do |format|
if @seat.update(seat_params)
format.html { redirect_to @seat, notice: 'Seat was successfully updated.' }
format.json { render :show, status: :ok, location: @seat }
else
format.html { render :edit }
format.json { render json: @seat.errors, status: :unprocessable_entity }
end
end
end
# DELETE /seats/1
# DELETE /seats/1.json
def destroy
@seat.destroy
respond_to do |format|
format.html { redirect_to seats_url, notice: 'Seat was successfully destroyed.' }
format.json { head :no_content }
end
end
# POST /seats/1
def sit
@message = @seat.sit current_user
end
def stand
@message = @seat.stand current_user
end
private
# Use callbacks to share common setup or constraints between actions.
def set_seat
@seat = Seat.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def seat_params
params.require(:seat).permit(:position, :table_id, :user_id)
end
end

View File

@ -0,0 +1,2 @@
module SeatHelper
end

View File

@ -1,6 +1,7 @@
# The game class
class Game < ActiveRecord::Base
belongs_to :table
has_many :seats
has_many :players, dependent: :destroy
has_many :users, through: :players
has_many :player_cards, through: :players
@ -8,10 +9,12 @@ class Game < ActiveRecord::Base
has_one :controlling_player
belongs_to :play_to_beat
accepts_nested_attributes_for :players
validates :title, presence: true
#validates :title, presence: true
after_create :add_table
# Returns the newest joinable game or creates a new game if none are found
scope :play_now, lambda { |user|
where(status:nil).joins("LEFT OUTER JOIN players ON players.game_id = games.id").group("games.id").having("count(players) < 4").first || Game.create(title: "Game " + user.soft_token[0..4])
where(status:nil).joins("LEFT OUTER JOIN players ON players.game_id = games.id").group("games.id").having("count(players) < 4").last || Game.create(title: "Game " + user.soft_token[0..4])
}
def start
@ -72,14 +75,37 @@ class Game < ActiveRecord::Base
new_player = self.players.create(user: user, soft_token: user.soft_token)
end
def remove_player_from_user(user)
return false unless self.has_user? user
if self.players.exists?(user_id: user.id)
@player = self.players.find_by(user_id: user.id)
elsif self.players.exists?(soft_token: user.soft_token)
@player = self.players.find_by(soft_token: user.soft_token)
end
if @player
@player.destroy
end
end
def can_accomodate(user)
!(already_has?(user) || full?)
end
#TODO: Which has_user? method is better? Probably has_user?.
def already_has?(user)
self.players.pluck(:user_id, :soft_token).flatten.compact.to_set.intersect? ([user.id, user.soft_token].to_set)
end
def has_user? user
return true if self.players.exists?(user_id: user.id)
return true if self.players.exists?(soft_token: user.soft_token)
return false
end
def full?
players.count >= 4
end
@ -130,4 +156,12 @@ class Game < ActiveRecord::Base
@inventory_counts.include? 0
end
private
def add_table
self.table ||= Table.create current_game_id: self.id
self.seats = self.table.seats
self.save
end
end

View File

@ -1,4 +1,52 @@
class Seat < ActiveRecord::Base
belongs_to :table
belongs_to :game
belongs_to :user
def sit(current_user)
if self.occupied?
return "Error. Seat is occupied."
else
# Stand up from any other seats at this table
current_user.stand_from(self.table)
self.user_id = current_user.id
self.user_soft_token = current_user.soft_token
self.save
self.table.current_game.add_player_from_user(current_user)
return "Successfully sat."
end
end
def stand(current_user)
unless self.occupied_by?(current_user)
return "Error. Seat not occupied by you."
else
self.user_id = nil
self.user_soft_token = nil
self.save
self.table.current_game.remove_player_from_user(current_user)
return "Successfully stood up."
end
end
def occupied?
self.user_id.present? || self.user_soft_token.present?
end
def occupied_by? user
if user.id.present?
self.user_id == user.id
end
if user.soft_token.present?
self.user_soft_token == user.soft_token
end
end
def user_display_name
return nil unless self.occupied?
self.user.try(:email) || "Guest" + self.user_soft_token.to_s[0..4]
end
end

View File

@ -3,5 +3,28 @@ class Table < ActiveRecord::Base
has_many :seats
has_many :users, through: :seats
has_many :games
has_one :current_game, :class_name => "Game", :primary_key => "current_game_id"
#TODO: Deprecated current_game_id from database.
# Instead calculate with self.current_game
#
#has_one :current_game, :class_name => "Game", :primary_key => "current_game_id"
#TODO: Should these be part of Game initialize instead of callback?
after_create :add_seats
after_create :add_game
def current_game
self.games.last
end
private
def add_seats
1.upto(4) do |i|
self.seats.create(position: i)
end
end
def add_game
self.games.create if self.current_game_id.blank?
end
end

View File

@ -16,6 +16,25 @@ class User < ActiveRecord::Base
!soft_user?
end
def display_name
self.email || "Guest#{self.soft_token[0..5]}"
end
def stand_from(table)
@seats = []
if self.id
@seats << table.seats.where(user_id: self.id)
end
if self.soft_token
@seats << table.seats.where(user_soft_token: self.soft_token)
end
@seats.flatten.each do |seat|
seat.user_id = nil
seat.user_soft_token = nil
seat.save
end
end
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :invitable, :database_authenticatable, :registerable, # :confirmable,

View File

@ -1,4 +1,4 @@
#game-component-container-container= react_component 'Game', props = {url: "#{game_path @game}"}, html_options = {id: "game-component-container"}
#game-component-container-container= react_component 'Game', props = {id: @game.id, current_user: current_user}, html_options = {id: "game-component-container"}
-# TODO: Remove deprecated commented out code
-#%p

View File

@ -13,14 +13,40 @@ json.is_startable @game.startable?
json.current_user_id current_user.try(:id)
json.current_user_soft_token current_user.try(:soft_token)
json.players @game.players do |player|
json.id player.id
json.email player.user.try(:email)
json.inventory_count player.inventory.count
json.user_id player.user.try(:id)
json.soft_token player.soft_token
#json.players @game.players do |player|
# json.id player.id
# json.email player.user.try(:email)
# json.inventory_count player.inventory.count
# json.user_id player.user.try(:id)
# json.soft_token player.soft_token
#
# if player.soft_token == current_user.soft_token
# json.inventory player.inventory
# end
#end
if player.soft_token == current_user.soft_token
json.inventory player.inventory
json.seats @game.seats.order(:position) do |seat|
json.id seat.id
json.table_id seat.table_id
json.position seat.position
json.user_id seat.user_id
json.user_soft_token seat.user_soft_token
json.user_display_name seat.user_display_name
json.set! :player do
@player = nil
if !seat.user_id.blank?
@player = @game.players.find_by(user_id: seat.user_id)
elsif !seat.user_soft_token.blank?
@player = @game.players.find_by(soft_token: seat.user_soft_token)
end
if @player
json.id @player.id
json.email @player.user.try(:email)
json.inventory_count @player.inventory.count
json.user_id @player.user_id
json.soft_token @player.soft_token
json.inventory @player.inventory if @player.soft_token == current_user.soft_token
end
end
end

View File

@ -0,0 +1 @@
json.extract! @seat, :id, :table_id, :user_id, :user_soft_token

View File

@ -0,0 +1 @@
json.message @message

View File

@ -0,0 +1 @@
json.message @message

View File

@ -1 +1,13 @@
json.extract! @table, :id, :title, :created_at, :updated_at
json.current_game_id @table.current_game.id
json.current_user current_user
json.seats @table.seats.order(:position) do |seat|
json.id seat.id
json.table_id seat.table_id
json.position seat.position
json.user_id seat.user_id
json.user_soft_token seat.user_soft_token
json.user_display_name seat.user_display_name
end

View File

@ -1,3 +1,3 @@
if Rails.env.development?
React::Rails::HotLoader.start()
#React::Rails::HotLoader.start()
end

View File

@ -1,7 +1,12 @@
Rails.application.routes.draw do
resources :players
resources :tables do
resources :seats
resources :seats do
member do
patch 'sit'
patch 'stand'
end
end
resources :games
end

View File

@ -0,0 +1,5 @@
class AddPositionToSeats < ActiveRecord::Migration
def change
add_column :seats, :position, :integer
end
end

View File

@ -0,0 +1,5 @@
class AddUserSoftTokenToSeats < ActiveRecord::Migration
def change
add_column :seats, :user_soft_token, :string
end
end

View File

@ -0,0 +1,5 @@
class AddGameIdToSeats < ActiveRecord::Migration
def change
add_column :seats, :game_id, :integer
end
end

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20151231035240) do
ActiveRecord::Schema.define(version: 20160219012611) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -76,6 +76,9 @@ ActiveRecord::Schema.define(version: 20151231035240) do
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "position"
t.string "user_soft_token"
t.integer "game_id"
end
add_index "seats", ["table_id"], name: "index_seats_on_table_id", using: :btree

View File

@ -1,7 +1,10 @@
FactoryGirl.define do
factory :seat do
table { FactoryGirl.create(:table) }
user { FactoryGirl.create(:user) }
end
trait :occupied do
user { FactoryGirl.create(:user) }
user_soft_token { user.soft_token }
end
end
end

View File

@ -1,7 +1,6 @@
FactoryGirl.define do
factory :table do
sequence(:title) { |n| "Test Table #{n}" }
current_game { FactoryGirl.create(:game) }
end
end

View File

@ -3,7 +3,7 @@ FactoryGirl.define do
confirmed_at Time.now
name 'Test User'
sequence(:email) { |n| "test#{n}@example.com" }
password 'please123'
password 'password'
sequence(:soft_token) { |n| "soft_token#{n}" }
trait :admin do

View File

@ -2,31 +2,68 @@
# As a visitor
# I want to quickly join a game
# So I can play
feature 'Play now', :devise do
before { @game = FactoryGirl.create :game }
feature 'Play now', :devise, js: true do
# Scenario: Visitor can play now
# Given I am not signed in
# When I click Play Now
# Then I join a game
scenario 'visitor can join a game', js: true do
visit root_path
click_link 'Play Now'
expect(page).to have_content "Test Game"
expect(current_path).to eq game_path(@game)
@player = @game.players.last
@playerComponent = page.find("#player-1")
expect(@playerComponent.text).to have_content("Guest#{@player.soft_token[0..5]}")
describe 'visitor can Play Now' do
context 'there are no joinable games' do
before do
expect(Game.all.count).to eq 0
visit root_path
click_link 'Play Now'
end
it 'creates and joins a new game' do
#TODO: Fix race condition
expect(page).to have_content "Hand to beat"
#Game title derived from visitor soft_token
expect(page).to have_content "#{Game.last.players.first.soft_token[0..4]}"
#Game count increased by 1
expect(Game.all.count).to eq 1
end
end
# Add another player
@game.add_player_from_user FactoryGirl.create :user
#TODO: Remove race condition
visit current_path
click_button 'Start'
#TODO: Remove race condition
visit current_path
visit current_path
visit current_path
visit current_path
expect(page).to have_content 'Inventory'
context 'visitor joins an existing game' do
before do
FactoryGirl.create :game
@game = Game.last
visit root_path
expect(Game.all.count).to eq 1
click_link 'Play Now'
end
it 'shows and starts the game' do
#TODO: Deprecated it shows the game title
#expect(page).to have_content "Test Game"
expect(page).to have_content "Hand to beat"
#it 'is located at the correct url' do
expect(current_path).to eq game_path(@game)
#it 'shows the visitor as a player' do
@player = @game.players.last
@playerComponent = page.find("#player-1")
#TODO: have_content @player.display_name
expect(@playerComponent.text).to have_content("Guest#{@player.soft_token[0..5]}")
#it 'shows other players' do
# Add another player
@user2 = FactoryGirl.create :user
@seats = @game.table.seats.map { |s| s.occupied? ? nil : s}
@seats.compact.first.sit(@user2)
#TODO: Remove race condition
visit current_path
expect(page).to have_content @user2.display_name
#it 'the game is startable' do
click_button 'Start'
#TODO: Remove race condition
visit current_path
visit current_path
visit current_path
visit current_path
expect(page).to have_content 'Inventory'
expect(Game.all.count).to eq 1
end
end
end
end

View File

@ -7,7 +7,7 @@ feature 'Play a hand', type: :feature, js: true do
@game = setup_game
@game.reload
@player = @game.controlling_player
signin(@player.user.email,'please123')
signin(@player.user.email,'password')
#login_as(@player.user, scope: :user)
visit game_path @game
end
@ -228,4 +228,17 @@ feature 'Play a hand', type: :feature, js: true do
expect(page).to have_content 'Hand to beat: None'
end
# Scenario: Player wins the game
# Given I am the active player
# And I play my last card(s)
# And I am the first player to do so
# Then I see a 1st place winner message
scenario 'first place winner' do
# play last card
pending "TBD"
expect(@player.inventory.count).to eq 0
expect(page).to have_content "Winner: #{@player.display_name}"
end
end

View File

@ -0,0 +1,108 @@
# Feature: Seats
# As a visitor
# I want to sit in a seat
# So I can be at a table and play a game
feature 'Seats', :devise, js: true do
context "As a visitor" do
before(:each) do
@table = FactoryGirl.create :table
visit table_path @table
end
it 'visitor can sit in an empty seat' do
click_button 'Sit', match: :first
expect(page).to have_content "Guest"
end
it 'sit button is disabled for unavailable seats' do
@button = page.find_button('Sit', match: :first)
@button.click
expect(page).to have_content "Guest"
expect(@button.disabled?).to eq true
end
it 'has a stand button if current user is seated' do
@button = page.find_button('Sit', match: :first)
@button.click
expect(page).to have_content "Guest"
@buttons = page.find_all('button', text: 'Stand')
expect(@buttons.length).to eq 1
end
scenario 'table seats match up with game seats' do
click_button 'Sit', match: :first
expect(page.find(:css, ".game-seat.position-2")).to have_content "Guest"
end
#TODO: User can only occupy one seat per table
scenario 'visitor changes seats' do
click_button 'Sit', match: :first
expect(page).to have_content "Guest"
#TODO: Fix race condition
sleep 3
click_button 'Sit', match: :first
expect(page).to have_content "Guest"
expect(page.text.scan('Guest').count).to eq 2
end
scenario 'visitor can join the current game' do
click_button 'Sit', match: :first
#TODO: Fix race condition. Trigger game react component reload when player joins a table
sleep 4
expect(page).to have_content "Guest"
expect(page.text.scan('Guest').count).to eq 2
end
scenario 'visitor can leave the current game' do
click_button 'Sit', match: :first
#TODO: Fix race condition
sleep 4
expect(page.text.scan('Guest').count).to eq 2
click_button 'Stand', match: :first
expect(page).to have_content "Sit"
#TODO: Fix race condition
sleep 4
expect(page.text.scan('Guest').count).to eq 0
end
end
context "As a user" do
before(:each) do
@table = FactoryGirl.create :table
@user = FactoryGirl.create :user
signin(@user.email, @user.password)
visit table_path @table
end
scenario 'user can sit at the table' do
#TODO: Fix race condition
expect(page).to have_content "Sit"
click_button 'Sit', match: :first
expect(page).to have_content @user.email
end
scenario 'user can join the current game' do
expect(page).to have_content "Sit"
click_button 'Sit', match: :first
#TODO: Fix race condition. Trigger game react component reload when player joins a table
sleep 4
expect(page).to have_content @user.email
expect(page.text.scan(@user.email).count).to eq 2
end
scenario 'user can leave the current game' do
expect(page).to have_content "Sit"
click_button 'Sit', match: :first
#TODO: Fix race condition
sleep 4
expect(page.text.scan(@user.email).count).to eq 2
click_button 'Stand', match: :first
expect(page).to have_content "Sit"
#TODO: Fix race condition
sleep 4
expect(page.text.scan(@user.email).count).to eq 0
end
end
end

View File

@ -26,7 +26,7 @@ feature 'Table index', :devise do
# Then I see an index of tables
scenario 'I can see all the tables', js: true do
@user = FactoryGirl.create :user
signin(@user.email,'please123')
signin(@user.email,'password')
5.times { FactoryGirl.create :table }
visit root_path

View File

@ -9,33 +9,70 @@ feature 'Table show', :devise do
visit tables_path
click_link @table.title
end
# Scenario: Visitor can view a table
# Given I am not signed in
# When I visit a table
# Then I see the table attributes
scenario 'visitor can see the table', js: true do
expect(page).to have_selector "#table-#{@table.id} > h1", @table.title
end
scenario 'visitor can sit at the table', js: true do
click_button 'Sit'
click_button 'Sit', match: :first
expect(page).to have_content "Guest"
end
scenario 'visitor can join the current game', js: true do
click_button 'Sit', match: :first
#TODO: Fix race condition. Trigger game react component reload when player joins a table
sleep 4
expect(page).to have_content "Guest"
expect(page.text.scan('Guest').count).to eq 2
end
scenario 'visitor can leave the current game', js: true do
click_button 'Sit', match: :first
#TODO: Fix race condition
sleep 4
expect(page.text.scan('Guest').count).to eq 2
click_button 'Stand', match: :first
expect(page).to have_content "Sit"
#TODO: Fix race condition
sleep 4
expect(page.text.scan('Guest').count).to eq 0
end
end
context "As a user" do
# Scenario: User can view a table
# Given I am signed in
# When I click a table
# Then I see an index of tables
scenario 'visitor can see the table', js: true do
before(:each) do
@table = FactoryGirl.create :table
# TODO: sign in
visit tables_path
click_link @table.title
expect(page).to have_button "Sit"
expect(page).to have_selector "#table-#{@table.id} > .title", @table.title
expect(current_path).to eq table_path(@table)
@user = FactoryGirl.create :user
signin(@user.email, @user.password)
visit table_path @table
end
scenario 'user can sit at the table', js: true do
#TODO: Fix race condition
expect(page).to have_content "Sit"
click_button 'Sit', match: :first
expect(page).to have_content @user.email
end
scenario 'user can join the current game', js: true do
expect(page).to have_content "Sit"
click_button 'Sit', match: :first
#TODO: Fix race condition. Trigger game react component reload when player joins a table
sleep 4
expect(page).to have_content @user.email
expect(page.text.scan(@user.email).count).to eq 2
end
scenario 'user can leave the current game', js: true do
expect(page).to have_content "Sit"
click_button 'Sit', match: :first
#TODO: Fix race condition
sleep 4
expect(page.text.scan(@user.email).count).to eq 2
click_button 'Stand', match: :first
expect(page).to have_content "Sit"
#TODO: Fix race condition
sleep 4
expect(page.text.scan(@user.email).count).to eq 0
end
end
end

View File

@ -0,0 +1,60 @@
# Feature: Table show
# As a visitor
# I want to view a table
# So I can sit and play a game
feature 'Table show', :devise do
context "As a visitor" do
before(:each) do
@table = FactoryGirl.create :table
visit tables_path
click_link @table.title
end
# Scenario: Visitor can view a table
# Given I am not signed in
# When I visit a table
# Then I see the table attributes
scenario 'visitor can see the table', js: true do
expect(page).to have_selector "#table-#{@table.id} > h1", @table.title
end
scenario 'visitor can see the seats', js: true do
#TODO: Race condition exists without have_content
expect(page).to have_content "Sit"
@seat_count = page.find_all(".seat").length
expect(@seat_count).to eq @table.seats.count
end
scenario 'visitor can see the current game', js: true do
#TODO: Write a better expectation
expect(page).to have_content "Hand to beat"
end
end
context "As a user" do
before(:each) do
@table = FactoryGirl.create :table
@user = FactoryGirl.create :user
signin(@user.email, @user.password)
end
# Scenario: User can view a table
# Given I am signed in
# When I click a table
# Then I see the table attributes
scenario 'user can see the table', js: true do
visit tables_path
click_link @table.title
expect(page).to have_content "Sign out"
expect(page).to have_selector "#table-#{@table.id} > h1", @table.title
expect(current_path).to eq table_path(@table)
end
scenario 'user can see the current game', js: true do
#TODO: Fix race condition
visit table_path @table
#TODO: Write a better expectation
expect(page).to have_content "Hand to beat"
end
end
end

View File

@ -8,7 +8,7 @@ feature 'Sign in', :devise do
# When I sign in with valid credentials
# Then I see an invalid credentials message
scenario 'user cannot sign in if not registered' do
signin('test@example.com', 'please123')
signin('test@example.com', 'password')
expect(page).to have_content
I18n.t 'devise.failure.not_found_in_database', authentication_keys: 'email'
end

View File

@ -20,4 +20,9 @@ feature 'Join game', :devise do
@playerComponent = page.find("#player-1")
expect(@playerComponent.text).to have_content("Guest#{@player.soft_token[0..5]}")
end
scenario 'visitor can join a game in progress' do
pending "To Be Implemented"
expect(page).to have_content "Waiting for next game."
end
end

View File

@ -9,13 +9,31 @@ RSpec.describe Seat, type: :model do
it { should respond_to :table }
it { should respond_to :user }
it { should respond_to :user_id }
it { should respond_to :user_soft_token }
it '#table returns a Table' do
expect(@seat.table.class).to match Table
end
it '#user returns a User' do
@seat = FactoryGirl.create(:seat, :occupied)
expect(@seat.user.class).to match User
end
it '#user_id returns a Fixnum' do
@seat = FactoryGirl.create(:seat, :occupied)
expect(@seat.user_id.class).to match Fixnum
end
it '#user_soft_token returns a String' do
@seat = FactoryGirl.create(:seat, :occupied)
expect(@seat.user_soft_token.class).to match String
end
it "#occupied? returns a Boolean" do
expect(@seat.occupied?).to eq false
@seat.user_id = 1
expect(@seat.occupied?).to eq true
end
end

View File

@ -33,4 +33,8 @@ RSpec.describe Table, type: :model do
expect(@table.current_game.class).to match Game
end
it "#seats returns 4 seats by default" do
expect(@table.seats.count).to eq 4
end
end

View File

@ -7,10 +7,10 @@ module Features
user2 = FactoryGirl.create :user
user3 = FactoryGirl.create :user
user4 = FactoryGirl.create :user
game.add_player_from_user user1
game.add_player_from_user user2
game.add_player_from_user user3
game.add_player_from_user user4
game.seats[0].sit user1
game.seats[1].sit user2
game.seats[2].sit user3
game.seats[3].sit user4
game.start
return game
end