├── Procfile ├── .gitignore ├── README.md ├── server ├── poker │ ├── sidePot.js │ ├── player.js │ ├── __test__ │ │ ├── deck.test.js │ │ ├── winner.test.js │ │ ├── betting.test.js │ │ ├── seat.test.js │ │ ├── multiplayer.test.js │ │ ├── table.test.js │ │ └── raise.test.js │ ├── seat.js │ └── deck.js ├── db │ └── models │ │ ├── hand.js │ │ ├── userHand.js │ │ ├── group.js │ │ ├── groupMember.js │ │ ├── groupInvite.js │ │ ├── user.js │ │ ├── index.js │ │ └── __test__ │ │ └── user.test.js ├── index.html ├── utils │ └── index.js ├── routes │ ├── groupInvites.js │ ├── searchUsers.js │ ├── index.js │ ├── hands.js │ ├── auth.js │ └── groups.js ├── index.js └── socket │ └── index.js ├── client ├── modules │ ├── utils │ │ └── theme.js │ └── components │ │ ├── index.js │ │ ├── Text.js │ │ ├── Modal.js │ │ ├── Link.js │ │ ├── Button.js │ │ ├── Panel.js │ │ └── UserSearchModal.js ├── app │ ├── components │ │ ├── Playground │ │ │ └── index.js │ │ ├── NoMatch.jsx │ │ ├── Game │ │ │ ├── Pieces │ │ │ │ ├── Background.jsx │ │ │ │ ├── ChipStack.jsx │ │ │ │ ├── Board.jsx │ │ │ │ ├── Card.jsx │ │ │ │ └── ChipPile.jsx │ │ │ ├── Seats │ │ │ │ ├── Bet.jsx │ │ │ │ ├── EmptySeat.jsx │ │ │ │ ├── Hand.jsx │ │ │ │ ├── ShotClock.jsx │ │ │ │ ├── SeatedPlayer.jsx │ │ │ │ └── index.jsx │ │ │ ├── ChatAndInfo │ │ │ │ ├── Message.jsx │ │ │ │ ├── SitOutCheckbox.js │ │ │ │ ├── Spectators.jsx │ │ │ │ ├── Chat.jsx │ │ │ │ └── index.jsx │ │ │ ├── Actions │ │ │ │ ├── PotSizeButton.jsx │ │ │ │ ├── RaiseSlider.jsx │ │ │ │ ├── ActionButtons.jsx │ │ │ │ └── index.jsx │ │ │ ├── TableControls │ │ │ │ └── index.js │ │ │ ├── Game.jsx │ │ │ └── BuyinModal.js │ │ ├── lobby │ │ │ └── MainMenu │ │ │ │ ├── Player.jsx │ │ │ │ ├── PlayerList.jsx │ │ │ │ ├── Table.jsx │ │ │ │ ├── TableList.jsx │ │ │ │ └── index.jsx │ │ ├── Lobby │ │ │ ├── TopNav.js │ │ │ ├── BottomNav.js │ │ │ └── index.jsx │ │ ├── App.jsx │ │ ├── Groups │ │ │ ├── CreateGroupModal.jsx │ │ │ ├── Group.jsx │ │ │ └── index.js │ │ ├── Signup.jsx │ │ ├── Login.jsx │ │ └── HandHistory │ │ │ ├── index.js │ │ │ └── Hand.jsx │ ├── actions │ │ ├── ui.js │ │ ├── lobby.js │ │ ├── hands.js │ │ ├── searchUsers.js │ │ ├── groups.js │ │ └── user.js │ ├── reducers │ │ ├── ui.js │ │ ├── searchUsers.js │ │ ├── hands.js │ │ ├── groups.js │ │ ├── lobby.js │ │ └── user.js │ ├── store │ │ └── store.js │ └── index.js └── scss │ ├── card.scss │ ├── main.scss │ ├── table.scss │ ├── seats.scss │ └── small_table.scss ├── .eslintrc.js ├── .flowconfig ├── .gitattributes ├── .vscode └── launch.json ├── webpack.config.js └── package.json /Procfile: -------------------------------------------------------------------------------- 1 | server -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | npm-debug.log 3 | .vscode 4 | .env 5 | /server/db/config -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # poker_playhouse 2 | Realtime multiplayer poker 3 | Node.js, React, Redux, Socket.IO, Express, Sequelize, PostgreSQL -------------------------------------------------------------------------------- /server/poker/sidePot.js: -------------------------------------------------------------------------------- 1 | class SidePot { 2 | constructor() { 3 | this.amount = 0 4 | this.players = [] 5 | } 6 | } 7 | 8 | module.exports = SidePot; -------------------------------------------------------------------------------- /client/modules/utils/theme.js: -------------------------------------------------------------------------------- 1 | export default { 2 | colors: { 3 | blue: '#2196f3', 4 | green: '#5cb85c', 5 | lightblue: '#5bc0de', 6 | orange: '#f0ad4e', 7 | red: '##d9534f', 8 | }, 9 | } -------------------------------------------------------------------------------- /client/app/components/Playground/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | 4 | export default function Playground() { 5 | return ( 6 |
7 | hello world 8 |
9 | ) 10 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }] 5 | }, 6 | "extends": ['plugin:react/recommended'] 7 | } -------------------------------------------------------------------------------- /server/poker/player.js: -------------------------------------------------------------------------------- 1 | class Player { 2 | constructor(socketId, id, name, bankroll) { 3 | this.socketId = socketId, 4 | this.id = id, 5 | this.name = name, 6 | this.bankroll = bankroll 7 | } 8 | } 9 | 10 | module.exports = Player -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ./.gitignore 3 | ./.gitattributes 4 | ./node_modules/ 5 | 6 | [include] 7 | ./client/app/components/*.js 8 | 9 | [libs] 10 | 11 | [lints] 12 | 13 | [options] 14 | module.name_mapper='^app/\(.*\)' -> '/client/modules/\1' 15 | 16 | [strict] 17 | -------------------------------------------------------------------------------- /client/app/components/NoMatch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class NoMatch extends React.Component { 4 | render() { 5 | return ( 6 |
7 |

Oops, you must be in the wrong place...

8 |
9 | ) 10 | } 11 | } 12 | 13 | export default NoMatch 14 | -------------------------------------------------------------------------------- /client/modules/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Button } from './Button'; 2 | export { default as Link } from './Link'; 3 | export { default as Modal } from './Modal'; 4 | export { default as Panel } from './Panel'; 5 | export { default as Text } from './Text'; 6 | export { default as UserSearchModal } from './UserSearchModal'; -------------------------------------------------------------------------------- /server/db/models/hand.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | const Hand = sequelize.define('Hand', { 3 | history: { 4 | type: DataTypes.JSON, 5 | allowNull: false, 6 | }, 7 | }); 8 | 9 | Hand.associate = models => { 10 | Hand.hasMany(models.UserHand, { foreignKey: 'hand_id' }); 11 | }; 12 | 13 | return Hand; 14 | }; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /client/app/actions/ui.js: -------------------------------------------------------------------------------- 1 | export const TOGGLE_LEFT_COLUMN = 'TOGGLE_LEFT_COLUMN' 2 | export const TOGGLE_RIGHT_COLUMN = 'TOGGLE_RIGHT_COLUMN' 3 | export const TOGGLE_GRID_VIEW = 'TOGGLE_GRID_VIEW' 4 | 5 | export function toggleLeftColumn() { 6 | return { 7 | type: TOGGLE_LEFT_COLUMN 8 | } 9 | } 10 | 11 | export function toggleRightColumn() { 12 | return { 13 | type: TOGGLE_RIGHT_COLUMN 14 | } 15 | } 16 | 17 | export function toggleGridView() { 18 | return { 19 | type: TOGGLE_GRID_VIEW 20 | } 21 | } -------------------------------------------------------------------------------- /server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Poker Playhouse 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/modules/components/Text.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { css } from 'emotion' 4 | 5 | type Props = { 6 | children: React.Node, 7 | small?: boolean, 8 | large?: boolean, 9 | bold?: boolean, 10 | } 11 | 12 | const Text = ({ children, small, large, bold }: Props) => ( 13 | 18 | {children} 19 | 20 | ) 21 | 22 | export default Text -------------------------------------------------------------------------------- /client/app/components/Game/Pieces/Background.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import { blue } from 'material-ui/styles/colors' 4 | 5 | const styles = { 6 | outer: { 7 | background: blue[700] 8 | } 9 | } 10 | function Background() { 11 | return ( 12 |
13 | {[1, 2, 3].map(num => ( 14 |
15 |
16 |
17 | ))} 18 |
19 | ) 20 | } 21 | 22 | export default Background -------------------------------------------------------------------------------- /client/app/components/Game/Seats/Bet.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ChipPile from '../Pieces/ChipPile' 3 | 4 | type Props = { 5 | seat: { 6 | bet: number, 7 | turn: boolean, 8 | lastAction: ?string, 9 | } 10 | } 11 | const Bet = ({ seat }: Props) => ( 12 |
13 | 14 | 15 | {seat.lastAction && !seat.turn && 16 | {seat.lastAction + ' - '} 17 | } 18 | ${seat.bet.toFixed(2)} 19 | 20 |
21 | ) 22 | 23 | export default Bet -------------------------------------------------------------------------------- /client/modules/components/Modal.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { css } from 'emotion' 3 | 4 | import Dialog from 'material-ui/Dialog'; 5 | 6 | const container = css` 7 | padding: 24px; 8 | `; 9 | 10 | type Props = { 11 | open: boolean, 12 | children: React.Node, 13 | } 14 | class Modal extends React.Component { 15 | render() { 16 | return ( 17 | 18 |
19 | {this.props.children} 20 |
21 |
22 | ) 23 | } 24 | } 25 | 26 | export default Modal; -------------------------------------------------------------------------------- /client/app/components/lobby/MainMenu/Player.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | 4 | type Props = { 5 | player: { 6 | name?: ?string, 7 | }, 8 | active: boolean, 9 | } 10 | class Player extends React.Component { 11 | render() { 12 | const { player, active } = this.props 13 | const style = { 14 | fontWeight: active ? 'bold' : 'normal' 15 | } 16 | 17 | return ( 18 |
  • 19 | {player.name} 20 | {active && (you)} 21 |
  • 22 | ) 23 | } 24 | } 25 | 26 | export default Player -------------------------------------------------------------------------------- /client/app/components/Game/ChatAndInfo/Message.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | 4 | const styles = { 5 | timeStamp: { 6 | color: '#ccc', 7 | }, 8 | } 9 | 10 | type Props = { 11 | message: { 12 | message: string, 13 | timestamp: string, 14 | from: string, 15 | }, 16 | } 17 | const Message = ({ message }: Props) => ( 18 |
    19 | {message.timestamp} 20 | {message.from && 21 | [{message.from}] 22 | } 23 | : {message.message} 24 |
    25 | ) 26 | 27 | export default Message -------------------------------------------------------------------------------- /server/poker/__test__/deck.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | var Deck = require('../deck') 3 | 4 | describe('Deck', () => { 5 | describe('#count()', () => { 6 | it('should initialize with 52 cards', () => { 7 | let deck = new Deck() 8 | 9 | expect(deck.count()).to.be.equal(52) 10 | }) 11 | }) 12 | 13 | describe('#draw()', () => { 14 | it('should return a card', () => { 15 | let deck = new Deck() 16 | let card = deck.draw() 17 | 18 | expect(card).to.have.property('suit') 19 | expect(card).to.have.property('rank') 20 | }) 21 | }) 22 | }) -------------------------------------------------------------------------------- /server/db/models/userHand.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | const UserHand = sequelize.define('UserHand', { 3 | id: { 4 | type: DataTypes.INTEGER, 5 | primaryKey: true, 6 | autoIncrement: true, 7 | }, 8 | user_id: { 9 | type: DataTypes.INTEGER, 10 | }, 11 | hand_id: { 12 | type: DataTypes.INTEGER, 13 | }, 14 | }); 15 | 16 | UserHand.associate = models => { 17 | UserHand.belongsTo(models.User, { foreignKey: 'user_id' }) 18 | UserHand.belongsTo(models.Hand, { foreignKey: 'hand_id' }) 19 | }; 20 | 21 | return UserHand; 22 | }; -------------------------------------------------------------------------------- /server/db/models/group.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | const Group = sequelize.define('Group', { 3 | creator_id: { 4 | type: DataTypes.INTEGER, 5 | }, 6 | name: { 7 | type: DataTypes.STRING, 8 | allowNull: false 9 | }, 10 | code: { 11 | type: DataTypes.STRING, 12 | allowNull: false 13 | }, 14 | }); 15 | 16 | Group.associate = models => { 17 | Group.hasMany(models.GroupMember, { foreignKey: 'group_id' }); 18 | Group.hasMany(models.GroupInvite, { foreignKey: 'group_id', as: 'group_invites' }); 19 | };; 20 | 21 | return Group; 22 | }; 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}\\index.js", 12 | "cwd": "${workspaceRoot}" 13 | }, 14 | { 15 | "type": "node", 16 | "request": "attach", 17 | "name": "Attach to Process", 18 | "port": 5858 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /client/app/components/Game/Actions/PotSizeButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from 'material-ui/Button' 3 | import { withStyles, createStyleSheet } from 'material-ui/styles' 4 | 5 | const styleSheet = createStyleSheet('Actions', theme => ({ 6 | button: { 7 | flex: 1, 8 | margin: '3px', 9 | padding: '0px', 10 | minWidth: '0px', 11 | fontSize: '12px', 12 | fontFamily: 'Montserrat, sans-serif', 13 | } 14 | })) 15 | 16 | const PotSizeButton = ({ text, onRaiseClick, classes }) => ( 17 | 24 | ) 25 | 26 | export default withStyles(styleSheet)(PotSizeButton) 27 | 28 | -------------------------------------------------------------------------------- /client/modules/components/Link.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import { css } from 'emotion'; 4 | import { IndexLink } from 'react-router'; 5 | 6 | const linkStyle = css` 7 | text-decoration: none; 8 | color: #aaa; 9 | `; 10 | const activeClass = css` 11 | margin-right: 20px; 12 | font-weight: bold; 13 | color: #000; 14 | `; 15 | 16 | type Props = { 17 | children?: ?React.Node, 18 | to: string, 19 | style: Object, 20 | }; 21 | 22 | const MyLink = ({ children, to, style }: Props) => ( 23 | 29 | {children} 30 | 31 | ); 32 | 33 | MyLink.defaultProps = { 34 | style: {}, 35 | } 36 | 37 | export default MyLink; -------------------------------------------------------------------------------- /server/db/models/groupMember.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | const GroupMember = sequelize.define('GroupMember', { 3 | group_id: { 4 | type: DataTypes.INTEGER, 5 | }, 6 | user_id: { 7 | type: DataTypes.INTEGER, 8 | allowNull: false 9 | }, 10 | is_admin: { 11 | type: DataTypes.BOOLEAN, 12 | allowNull: false, 13 | defaultValue: false, 14 | }, 15 | bankroll: { 16 | type: DataTypes.FLOAT, 17 | allowNull: true, 18 | defaultValue: 0.00, 19 | } 20 | }); 21 | 22 | GroupMember.associate = models => { 23 | GroupMember.belongsTo(models.User, { foreignKey: 'user_id' }); 24 | GroupMember.belongsTo(models.Group, { foreignKey: 'group_id' }); 25 | }; 26 | 27 | return GroupMember; 28 | }; 29 | -------------------------------------------------------------------------------- /client/app/components/Game/Seats/EmptySeat.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import Paper from 'material-ui/Paper' 4 | import { blueGrey, cyan } from 'material-ui/styles/colors' 5 | 6 | type Props = { 7 | seatId: string, 8 | onSeatClick: () => void 9 | } 10 | const EmptySeat = ({ seatId, onSeatClick }: Props) => ( 11 | 12 |
    13 | SIT HERE 14 |
    15 | 16 |
    17 |
    {seatId}
    18 |
    $0.00
    19 |
    20 |
    21 | ) 22 | 23 | export default EmptySeat 24 | -------------------------------------------------------------------------------- /server/utils/index.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | 3 | function generateToken(user) { 4 | var u = { 5 | id: user.id, 6 | username: user.username, 7 | location: user.location, 8 | bankroll: user.bankroll, 9 | createdAt: user.createdAt, 10 | updatedAt: user.updatedAt 11 | } 12 | 13 | return token = jwt.sign(u, process.env.JWT_SECRET, { 14 | expiresIn: 60 * 60 * 24 // expires in 24 hours 15 | }) 16 | } 17 | 18 | function getCleanUser(user) { 19 | if (!user) return {} 20 | 21 | return { 22 | id: user.id, 23 | username: user.username, 24 | location: user.location, 25 | bankroll: user.bankroll, 26 | createdAt: user.createdAt, 27 | updatedAt: user.updatedAt 28 | } 29 | } 30 | 31 | module.exports = { 32 | generateToken, 33 | getCleanUser 34 | } -------------------------------------------------------------------------------- /server/routes/groupInvites.js: -------------------------------------------------------------------------------- 1 | const db = require('../db/models') 2 | const jwt = require('jsonwebtoken'); 3 | 4 | module.exports = { 5 | fetchInvites(req, res) { 6 | jwt.verify(req.body.accessToken, process.env.JWT_SECRET, async (err, decoded) => { 7 | if (err) { 8 | return res.status(404).json({ 9 | error: true, 10 | message: 'Invalid access token' 11 | }) 12 | } 13 | 14 | const user = await db.User.findById(decoded.id) 15 | if (!user) throw new Error('User not found'); 16 | 17 | const invites = await db.GroupInvite.findAll({ 18 | where: { invited_id: user.id }, 19 | }); 20 | 21 | res.send({ invites }); 22 | }) 23 | }, 24 | // createInvite(req, res) { 25 | 26 | // }, 27 | // deleteInvite(req, res) { 28 | 29 | // }, 30 | } -------------------------------------------------------------------------------- /client/app/components/Lobby/TopNav.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import { Link } from 'app/components'; 4 | import { css } from 'emotion'; 5 | 6 | const outer = css` 7 | position: absolute; 8 | top: 0; 9 | width: 100%; 10 | background: #fff; 11 | box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05); 12 | ` 13 | const inner = css` 14 | display: flex; 15 | padding: 20px; 16 | align-items: center; 17 | ` 18 | const linkStyle = { marginRight: '20px' }; 19 | 20 | const TopNav = () => ( 21 |
    22 |
    23 | Lobby 24 | Hand history 25 | Groups 26 |
    27 |
    28 | ) 29 | 30 | export default TopNav; -------------------------------------------------------------------------------- /client/app/reducers/ui.js: -------------------------------------------------------------------------------- 1 | import { 2 | TOGGLE_LEFT_COLUMN, 3 | TOGGLE_RIGHT_COLUMN, 4 | TOGGLE_GRID_VIEW 5 | } from '../actions/ui' 6 | 7 | const initialState = { 8 | leftColumnShowing: true, 9 | rightColumnShowing: true, 10 | gridViewOn: false 11 | } 12 | 13 | function ui(state = initialState, action) { 14 | switch (action.type) { 15 | case TOGGLE_LEFT_COLUMN: 16 | return { 17 | ...state, 18 | leftColumnShowing: !state.leftColumnShowing 19 | } 20 | case TOGGLE_RIGHT_COLUMN: 21 | return { 22 | ...state, 23 | rightColumnShowing: !state.rightColumnShowing 24 | } 25 | case TOGGLE_GRID_VIEW: 26 | return { 27 | ...state, 28 | gridViewOn: !state.gridViewOn 29 | } 30 | default: 31 | return state 32 | } 33 | } 34 | 35 | export default ui -------------------------------------------------------------------------------- /server/routes/searchUsers.js: -------------------------------------------------------------------------------- 1 | const db = require('../db/models'); 2 | const jwt = require('jsonwebtoken'); 3 | 4 | module.exports = { 5 | search(req, res) { 6 | jwt.verify(req.query.accessToken, process.env.JWT_SECRET, async (err, decoded) => { 7 | if (err) { 8 | return res.status(404).json({ 9 | error: true, 10 | message: 'JWT invalid' 11 | }) 12 | } 13 | 14 | const user = await db.User.findById(decoded.id) 15 | if (!user) throw new Error('User not found'); 16 | 17 | const query = req.query.searchQuery; 18 | if (!query) res.send({ searchResults: [] }) 19 | 20 | const searchResults = await db.User.findAll({ 21 | where: { username: { $iLike: `%${query}%` } }, 22 | limit: 20, 23 | }); 24 | 25 | res.send({ searchResults }); 26 | }); 27 | }, 28 | } -------------------------------------------------------------------------------- /server/db/models/groupInvite.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | const GroupInvite = sequelize.define('GroupInvite', { 3 | group_id: { 4 | type: DataTypes.INTEGER, 5 | }, 6 | inviter_id: { 7 | type: DataTypes.INTEGER, 8 | allowNull: false 9 | }, 10 | invited_id: { 11 | type: DataTypes.INTEGER, 12 | allowNull: false 13 | }, 14 | accepted_at: { 15 | type: DataTypes.DATE, 16 | allowNull: true, 17 | }, 18 | }); 19 | 20 | GroupInvite.associate = models => { 21 | GroupInvite.belongsTo(models.User, { foreignKey: 'inviter_id', as: 'inviter' }); 22 | GroupInvite.belongsTo(models.User, { foreignKey: 'invited_id', as: 'invited' }); 23 | GroupInvite.belongsTo(models.Group, { foreignKey: 'group_id', as: 'group' }); 24 | }; 25 | 26 | return GroupInvite; 27 | }; 28 | -------------------------------------------------------------------------------- /client/app/components/Game/Pieces/ChipStack.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withStyles, createStyleSheet } from 'material-ui/styles' 3 | 4 | const styleSheet = createStyleSheet('ChipStack', () => ({ 5 | chip: { 6 | width: '20px', 7 | height: '3px', 8 | margin: '1px 3px', 9 | } 10 | })) 11 | 12 | type Props = { 13 | number: number, 14 | color: string, 15 | classes: Object, 16 | } 17 | const ChipStack = ({ 18 | number, 19 | color, 20 | classes 21 | }: Props) => { 22 | if (number === 0) { 23 | return null 24 | } 25 | 26 | const chips = Array.from(Array(parseInt(number)).keys()).map(num => ( 27 |
    31 |
    32 | )) 33 | 34 | return
    {chips}
    35 | } 36 | 37 | export default withStyles(styleSheet)(ChipStack) -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const authRoute = require('./auth'); 4 | const handsRoute = require('./hands'); 5 | const groupsRoute = require('./groups'); 6 | const searchUsersRoute = require('./searchUsers'); 7 | 8 | if (!process.env.JWT_SECRET) { 9 | console.error('ERROR!: Please set JWT_SECRET before running the app. \n run: export JWT_SECRET=...') 10 | process.exit(); 11 | } 12 | 13 | router.post('/signup', authRoute.signup); 14 | router.post('/login', authRoute.login); 15 | router.post('/verify_jwt', authRoute.verifyToken); 16 | 17 | router.post('/hand-history/:page', handsRoute.handHistory); 18 | 19 | router.get('/groups', groupsRoute.fetchGroups); 20 | router.post('/groups', groupsRoute.createGroup); 21 | router.delete('/groups/:groupId', groupsRoute.deleteGroup); 22 | 23 | router.get('/users/search', searchUsersRoute.search); 24 | 25 | module.exports = router; -------------------------------------------------------------------------------- /server/db/models/user.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | const User = sequelize.define('User', { 3 | username: { 4 | type: DataTypes.STRING, 5 | unique: true, 6 | allowNull: false 7 | }, 8 | password: { 9 | type: DataTypes.STRING, 10 | allowNull: false 11 | }, 12 | location: { 13 | type: DataTypes.STRING, 14 | allowNull: true 15 | }, 16 | bankroll: { 17 | type: DataTypes.FLOAT, 18 | allowNull: true, 19 | defaultValue: 100.00 20 | } 21 | }); 22 | 23 | User.associate = models => { 24 | User.hasMany(models.UserHand, { foreignKey: 'user_id' }); 25 | User.hasMany(models.GroupMember, { foreignKey: 'user_id' }); 26 | User.hasMany(models.GroupInvite, { foreignKey: 'inviter_id', as: 'sent_invites' }) 27 | User.hasMany(models.GroupInvite, { foreignKey: 'invited_id', as: 'received_invites' }) 28 | }; 29 | 30 | return User; 31 | }; -------------------------------------------------------------------------------- /client/app/reducers/searchUsers.js: -------------------------------------------------------------------------------- 1 | import { 2 | SEARCH_USERS_REQUEST, 3 | SEARCH_USERS_SUCCESS, 4 | SEARCH_USERS_FAILURE, 5 | } from '../actions/searchUsers'; 6 | 7 | const initialState = { 8 | isFetching: false, 9 | searchResults: [], 10 | errorMessage: '', 11 | } 12 | 13 | function searchUsers(state = initialState, action) { 14 | switch (action.type) { 15 | case SEARCH_USERS_REQUEST: 16 | return { 17 | ...state, 18 | isFetching: true, 19 | errorMessage: '', 20 | } 21 | case SEARCH_USERS_SUCCESS: 22 | return { 23 | searchResults: action.searchResults, 24 | isFetching: false, 25 | errorMessage: '', 26 | } 27 | case SEARCH_USERS_FAILURE: 28 | return { 29 | ...state, 30 | isFetching: false, 31 | errorMessage: action.message.response.data.message, 32 | } 33 | default: 34 | return state 35 | } 36 | } 37 | 38 | export default searchUsers -------------------------------------------------------------------------------- /client/app/components/Lobby/BottomNav.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import { css } from 'emotion'; 4 | 5 | import { Button } from 'app/components' 6 | 7 | const outer = css` 8 | position: absolute; 9 | bottom: 0; 10 | width: 100%; 11 | background: #2c2e3e; 12 | ` 13 | const inner = css` 14 | display: flex; 15 | padding: 20px; 16 | align-items: center; 17 | justify-content: space-between; 18 | color: #eee; 19 | ` 20 | 21 | type Props = { 22 | name: string, 23 | bankroll: number, 24 | logout: () => void, 25 | } 26 | const BottomNav = ({ name, bankroll, logout }: Props) => ( 27 |
    28 |
    29 |
    30 | Logged in as {name} 31 | Balance: ${bankroll.toFixed(2)} 32 |
    33 | 34 |
    35 |
    36 | ) 37 | 38 | export default BottomNav; -------------------------------------------------------------------------------- /client/app/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from 'redux' 2 | import { hashHistory } from 'react-router' 3 | import { routerMiddleware, routerReducer } from 'react-router-redux' 4 | import { createLogger } from 'redux-logger' 5 | import thunkMiddleware from 'redux-thunk' 6 | 7 | import user from '../reducers/user' 8 | import lobby from '../reducers/lobby' 9 | import ui from '../reducers/ui' 10 | import hands from '../reducers/hands' 11 | import groups from '../reducers/groups' 12 | import searchUsers from '../reducers/searchUsers' 13 | 14 | const rootReducer = combineReducers({ 15 | user, 16 | lobby, 17 | ui, 18 | hands, 19 | groups, 20 | searchUsers, 21 | routing: routerReducer 22 | }) 23 | 24 | const masterMiddleware = applyMiddleware( 25 | thunkMiddleware, 26 | createLogger({ collapsed: true }), 27 | routerMiddleware(hashHistory) 28 | ) 29 | 30 | const store = createStore( 31 | rootReducer, 32 | masterMiddleware 33 | ) 34 | 35 | export default store -------------------------------------------------------------------------------- /client/app/reducers/hands.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_HANDS, 3 | REQUEST_HANDS_SUCCESS, 4 | REQUEST_HANDS_FAILURE, 5 | } from '../actions/hands' 6 | 7 | const initialState = { 8 | isFetching: false, 9 | hands: [], 10 | pages: 0, 11 | count: 0, 12 | errorMessage: '', 13 | } 14 | 15 | function hands(state = initialState, action) { 16 | switch (action.type) { 17 | case REQUEST_HANDS: 18 | return { 19 | ...state, 20 | isFetching: true, 21 | errorMessage: '', 22 | } 23 | case REQUEST_HANDS_SUCCESS: 24 | return { 25 | ...state, 26 | isFetching: false, 27 | hands: action.hands, 28 | pages: action.pages, 29 | count: action.count, 30 | errorMessage: '', 31 | } 32 | case REQUEST_HANDS_FAILURE: 33 | return { 34 | ...state, 35 | isFetching: false, 36 | errorMessage: action.message.response.data.message, 37 | } 38 | default: 39 | return state 40 | } 41 | } 42 | 43 | export default hands -------------------------------------------------------------------------------- /client/app/components/Game/Seats/Hand.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import { css } from 'emotion' 4 | import Card from '../Pieces/Card' 5 | 6 | const hand = css` 7 | position: absolute; 8 | text-align: center; 9 | width: 100%; 10 | bottom: 128px; 11 | 12 | .card { 13 | position: absolute; 14 | transition: 0.2s all; 15 | } 16 | .card:first-child { 17 | top: 0px; 18 | left: 38px; 19 | } 20 | .card:last-child { 21 | top: 0px; 22 | right: 38px; 23 | } 24 | 25 | &:hover { 26 | .card:first-child { 27 | top: -40px; 28 | left: 10px; 29 | } 30 | .card:last-child { 31 | top: -40px; 32 | right: 10px; 33 | } 34 | } 35 | ` 36 | 37 | type Props = { 38 | seat: { 39 | hand: Array<{ 40 | rank: string, 41 | suit: string, 42 | }>, 43 | }, 44 | } 45 | const Hand = (props: Props) => ( 46 |
    47 | 48 | 49 |
    50 | ); 51 | 52 | export default Hand -------------------------------------------------------------------------------- /client/app/components/lobby/MainMenu/PlayerList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Player from './Player' 3 | 4 | const styles = { 5 | playerList: { 6 | margin: 0, 7 | padding: 0, 8 | listStyleType: 'none', 9 | }, 10 | } 11 | 12 | class PlayerList extends React.Component { 13 | render() { 14 | const { user, players } = this.props 15 | 16 | if (Object.keys(players).length > 0) { 17 | return ( 18 |
    19 |
      20 | {Object.keys(players).map(id => { 21 | let active = user.username === players[id].name ? true : false 22 | 23 | return ( 24 | 29 | ) 30 | })} 31 |
    32 |
    33 | ) 34 | } else { 35 | return

    Players

    36 | } 37 | } 38 | } 39 | 40 | export default PlayerList -------------------------------------------------------------------------------- /client/modules/components/Button.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import Button from 'material-ui/Button' 4 | 5 | const buttonStyle = { 6 | fontFamily: 'Montserrat, sans-serif', 7 | color: '#fff', 8 | fontSize: '16px', 9 | padding: '6px 18px', 10 | minWidth: '0', 11 | minHeight: '0', 12 | } 13 | 14 | type Props = { 15 | onClick: () => void, 16 | className: any, 17 | disabled: boolean, 18 | children: React.Node, 19 | style?: Object, 20 | flat?: boolean, 21 | secondary?: boolean, 22 | className?: string, 23 | } 24 | 25 | const MyButton = ({ onClick, disabled, children, style, flat, secondary, className }: Props) => { 26 | const xStyle = {...style}; 27 | if (flat) { 28 | xStyle.boxShadow = 'none'; 29 | } 30 | 31 | return ( 32 | 42 | ) 43 | } 44 | 45 | export default MyButton -------------------------------------------------------------------------------- /server/db/models/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Sequelize = require('sequelize'); 4 | const basename = path.basename(__filename); 5 | const env = process.env.NODE_ENV || 'development'; 6 | const config = require(__dirname + '/../config/config.json')[env]; 7 | const db = {}; 8 | 9 | let sequelize; 10 | if (config.use_env_variable) { 11 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 12 | } else { 13 | sequelize = new Sequelize(config.database, config.username, config.password, config); 14 | } 15 | 16 | fs 17 | .readdirSync(__dirname) 18 | .filter(file => { 19 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 20 | }) 21 | .forEach(file => { 22 | const model = sequelize['import'](path.join(__dirname, file)); 23 | db[model.name] = model; 24 | }); 25 | 26 | Object.keys(db).forEach(modelName => { 27 | if (db[modelName].associate) { 28 | db[modelName].associate(db); 29 | } 30 | }); 31 | 32 | db.sequelize = sequelize; 33 | db.Sequelize = Sequelize; 34 | 35 | module.exports = db; 36 | -------------------------------------------------------------------------------- /client/scss/card.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | display: inline-block; 3 | font-weight: bold; 4 | // background: #eee; 5 | width: 75px; 6 | height: 105px; 7 | overflow: hidden; 8 | margin: 3px; 9 | vertical-align: top; 10 | box-sizing: border-box; 11 | // box-shadow: $box-shadow; 12 | font-family: 'Oswald', sans-serif; 13 | border-radius: 4px; 14 | 15 | .card-back { 16 | width: 69px; 17 | height: 99px; 18 | margin: 3px; 19 | background-color: #295dc3; 20 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23ffffff' fill-opacity='0.4' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E"); 21 | } 22 | > div { 23 | display: inline-block; 24 | } 25 | .small-picture { 26 | vertical-align: top; 27 | padding-left: 5px; 28 | float: left; 29 | 30 | div:first-child { 31 | font-size: 1.2em; 32 | } 33 | div:last-child { 34 | margin-top: -15px; 35 | font-size: 1.5em; 36 | } 37 | } 38 | .big-picture { 39 | font-size: 5em; 40 | float: right; 41 | padding-right: 5px; 42 | } 43 | } -------------------------------------------------------------------------------- /client/app/components/Game/ChatAndInfo/SitOutCheckbox.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import Checkbox from 'material-ui/Checkbox'; 4 | 5 | const styles = { 6 | container: { 7 | position: 'absolute', 8 | top: '-42px', 9 | right: '-8px', 10 | display: 'flex', 11 | alignItems: 'center', 12 | } 13 | } 14 | 15 | type Props = { 16 | socket: Object, 17 | table: { 18 | id: string, 19 | }, 20 | seat: { 21 | id: string, 22 | sittingOut: boolean, 23 | }, 24 | }; 25 | 26 | class SitOutCheckbox extends React.Component { 27 | handleClick = () => { 28 | const { socket, table, seat } = this.props 29 | const socketMessage = seat.sittingOut ? 'sitting_in' : 'sitting_out' 30 | 31 | socket.emit(socketMessage, { 32 | tableId: table.id, 33 | seatId: seat.id 34 | }) 35 | } 36 | 37 | render () { 38 | const { seat } = this.props 39 | if (!seat) return null 40 | 41 | return ( 42 |
    43 | Sit out next hand 44 | this.handleClick()} /> 45 |
    46 | ) 47 | } 48 | } 49 | 50 | export default SitOutCheckbox -------------------------------------------------------------------------------- /client/app/actions/lobby.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_LOBBY_INFO = 'RECEIVE_LOBBY_INFO' 2 | export const TABLES_UPDATED = 'TABLES_UPDATED' 3 | export const PLAYERS_UPDATED = 'PLAYERS_UPDATED' 4 | export const TABLE_JOINED = 'TABLE_JOINED' 5 | export const TABLE_LEFT = 'TABLE_LEFT' 6 | export const TABLE_UPDATED = 'TABLE_UPDATED' 7 | 8 | export function receiveLobbyInfo(tables, players, socketId) { 9 | return { 10 | type: RECEIVE_LOBBY_INFO, 11 | tables, 12 | players, 13 | socketId 14 | } 15 | } 16 | 17 | export function tablesUpdated(tables) { 18 | return { 19 | type: TABLES_UPDATED, 20 | tables 21 | } 22 | } 23 | 24 | export function playersUpdated(players) { 25 | return { 26 | type: PLAYERS_UPDATED, 27 | players 28 | } 29 | } 30 | 31 | export function tableJoined(tables, tableId) { 32 | return { 33 | type: TABLE_JOINED, 34 | tables, 35 | tableId 36 | } 37 | } 38 | 39 | export function tableLeft(tables, tableId) { 40 | return { 41 | type: TABLE_LEFT, 42 | tables, 43 | tableId 44 | } 45 | } 46 | 47 | export function tableUpdated(table, message, from) { 48 | return { 49 | type: TABLE_UPDATED, 50 | table, 51 | message, 52 | from 53 | } 54 | } -------------------------------------------------------------------------------- /client/app/components/Game/Pieces/Board.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import Card from './Card' 4 | import ChipPile from './ChipPile' 5 | 6 | type Props = { 7 | table: { 8 | board: Array<{ 9 | rank: string, 10 | suit: string, 11 | }>, 12 | mainPot: number, 13 | pot: number, 14 | }, 15 | } 16 | class Board extends React.Component { 17 | render() { 18 | let renderedCards = this.props.table.board.slice(0) 19 | let { table } = this.props 20 | 21 | while (renderedCards.length < 5) { 22 | renderedCards.push({ 23 | rank: '0', 24 | suit: '0' 25 | }) 26 | } 27 | 28 | return ( 29 |
    30 | {table.mainPot > 0 && 31 |
    32 | 33 |
    Main pot: ${table.mainPot.toFixed(2)}
    34 |
    35 | } 36 | 37 | {renderedCards.map((card, index) => 38 | 39 | )} 40 | 41 |
    42 | Total pot: ${table.pot.toFixed(2)} 43 |
    44 |
    45 | ) 46 | } 47 | } 48 | 49 | export default Board 50 | -------------------------------------------------------------------------------- /client/app/components/Game/ChatAndInfo/Spectators.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import Player from '../../Lobby/MainMenu/Player' 4 | 5 | const styles = { 6 | playerList: { 7 | margin: 0, 8 | padding: 0, 9 | listStyleType: 'none', 10 | }, 11 | } 12 | 13 | type Props = { 14 | user: { 15 | username: string, 16 | }, 17 | table: { 18 | players: Array<{ 19 | name?: ?string, 20 | socketId: string, 21 | }>, 22 | }, 23 | } 24 | class Spectators extends React.Component { 25 | render() { 26 | const { user, table } = this.props 27 | 28 | if (table.players.length > 0) { 29 | return ( 30 |
    31 |
      32 | {table.players.map(player => { 33 | let active = user.username === player.name ? true : false 34 | 35 | return ( 36 | 41 | ) 42 | })} 43 |
    44 |
    45 | ) 46 | } else { 47 | return
    48 | } 49 | } 50 | } 51 | 52 | export default Spectators 53 | -------------------------------------------------------------------------------- /client/modules/components/Panel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { css } from 'emotion' 4 | import cx from 'classnames'; 5 | 6 | const container = css` 7 | border: 1px solid #eee; 8 | background: #fff; 9 | box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05); 10 | border-radius: 3px; 11 | ` 12 | const darkStyle = css` 13 | background: #eee; 14 | ` 15 | const headerStyle = css` 16 | padding: 16px 24px; 17 | border-bottom: 1px solid #eee; 18 | ` 19 | const darkHeader = css` 20 | padding: 16px 24px; 21 | border-bottom: 1px solid #ddd; 22 | ` 23 | const body = css` 24 | padding: 16px 24px; 25 | ` 26 | 27 | type Props = { 28 | dark?: true, 29 | header?: ?React.Node, 30 | children: React.Node, 31 | className?: string, 32 | } 33 | 34 | const Panel = ({ dark, header, children, className }: Props) => { 35 | return ( 36 |
    37 | {header && ( 38 |
    39 | {typeof header === 'string' 40 | ?

    {header}

    41 | : header} 42 |
    43 | )} 44 |
    45 | {children} 46 |
    47 |
    48 | ) 49 | } 50 | 51 | export default Panel -------------------------------------------------------------------------------- /client/app/components/App.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { hashHistory } from 'react-router' 4 | import { connect } from 'react-redux' 5 | import { css } from 'emotion' 6 | 7 | import { tokenLogin } from '../actions/user' 8 | 9 | import io from 'socket.io-client' 10 | const socket = io('/') 11 | 12 | const container = css` 13 | height: 100vh; 14 | background: #fafafa; 15 | ` 16 | 17 | type Props = { 18 | token: string, 19 | tokenLogin: (token: string) => void, 20 | children?: React.Element, 21 | } 22 | class App extends React.Component { 23 | componentDidMount() { 24 | const { token, tokenLogin } = this.props 25 | 26 | if (token) { 27 | tokenLogin(token) 28 | } else { 29 | if (hashHistory.getCurrentLocation().pathname !== '/login') { 30 | hashHistory.push('/login') 31 | } 32 | } 33 | } 34 | 35 | render() { 36 | const { children } = this.props; 37 | if (!children) return null; 38 | 39 | return
    {React.cloneElement(children, { socket })}
    ; 40 | } 41 | } 42 | 43 | function mapStateToProps(state) { 44 | return { token: state.user.token } 45 | } 46 | 47 | const mapDispatchToProps = ({ tokenLogin }) 48 | 49 | export default connect( 50 | mapStateToProps, 51 | mapDispatchToProps 52 | )(App) 53 | -------------------------------------------------------------------------------- /client/app/actions/hands.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded' 3 | 4 | export const REQUEST_HANDS = 'REQUEST_HANDS' 5 | export const REQUEST_HANDS_SUCCESS = 'REQUEST_HANDS_SUCCESS' 6 | export const REQUEST_HANDS_FAILURE = 'REQUEST_HANDS_FAILURE' 7 | 8 | const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:9000/api' : '/api' 9 | 10 | export function requestHands() { 11 | return { 12 | type: REQUEST_HANDS, 13 | } 14 | } 15 | 16 | export function requestHandsSuccess(hands, count, pages) { 17 | return { 18 | type: REQUEST_HANDS_SUCCESS, 19 | hands, 20 | count, 21 | pages, 22 | } 23 | } 24 | 25 | export function requestHandsFailure(message) { 26 | return { 27 | type: REQUEST_HANDS_FAILURE, 28 | message, 29 | } 30 | } 31 | 32 | export function fetchHandHistory(token, page) { 33 | return function(dispatch) { 34 | dispatch(requestHands()) 35 | 36 | return axios.post(`${ROOT_URL}/hand-history/${page}`, { token }) 37 | .then(res => { 38 | const { hands, count, pages } = res.data 39 | dispatch(requestHandsSuccess(hands, count, pages)) 40 | }) 41 | .catch(err => { 42 | console.log(err) 43 | dispatch(requestHandsFailure(err)) 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/app/actions/searchUsers.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded' 3 | 4 | export const SEARCH_USERS_REQUEST = 'SEARCH_USERS_REQUEST'; 5 | export const SEARCH_USERS_SUCCESS = 'SEARCH_USERS_SUCCESS'; 6 | export const SEARCH_USERS_FAILURE = 'SEARCH_USERS_FAILURE'; 7 | 8 | const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:9000/api' : '/api' 9 | 10 | export function searchUsersRequest() { 11 | return { 12 | type: SEARCH_USERS_REQUEST, 13 | } 14 | } 15 | 16 | export function searchUsersSuccess(searchResults) { 17 | return { 18 | type: SEARCH_USERS_SUCCESS, 19 | searchResults, 20 | } 21 | } 22 | 23 | export function searchUsersFailure(message) { 24 | return { 25 | type: SEARCH_USERS_FAILURE, 26 | message, 27 | } 28 | } 29 | 30 | export function searchUsers(token, searchQuery) { 31 | return function (dispatch) { 32 | dispatch(searchUsersRequest()) 33 | 34 | return axios.get(`${ROOT_URL}/users/search?accessToken=${token}&searchQuery=${searchQuery}`) 35 | .then(res => { 36 | const { searchResults } = res.data; 37 | dispatch(searchUsersSuccess(searchResults)) 38 | }) 39 | .catch(err => { 40 | console.log(err) 41 | dispatch(searchUsersFailure(err)) 42 | }) 43 | } 44 | } -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const http = require('http') 3 | const bodyParser = require('body-parser') 4 | const socketIo = require('socket.io') 5 | const webpack = require('webpack') 6 | const path = require('path') 7 | const webpackConfig = require('../webpack.config.js') 8 | const routes = require('./routes/index') 9 | const db = require('./db/models') 10 | const gameSocket = require('./socket'); 11 | 12 | const app = express() 13 | const server = http.createServer(app) 14 | const io = socketIo(server) 15 | const compiler = webpack(webpackConfig) 16 | 17 | app.use(express.static(__dirname)) 18 | app.use(bodyParser.json()) 19 | app.use(bodyParser.urlencoded({ extended: true })) 20 | 21 | if (process.env.NODE_ENV === 'development') { 22 | const webpackDevMiddleware = require('webpack-dev-middleware') 23 | const webpackHotMiddleware = require('webpack-hot-middleware') 24 | app.use(webpackDevMiddleware(compiler, { publicPath: webpackConfig.output.publicPath })) 25 | app.use(webpackHotMiddleware(compiler)) 26 | } else if (process.env.NODE_ENV === 'production') { 27 | app.get('/', (res) => { 28 | res.sendFile(path.join(__dirname + 'index.html')) 29 | }) 30 | } 31 | 32 | app.use('/api', routes); 33 | io.on('connection', socket => gameSocket.init(socket, io)); 34 | db.sequelize.sync(); 35 | 36 | server.listen(process.env.PORT) -------------------------------------------------------------------------------- /client/app/components/lobby/MainMenu/Table.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import { css } from 'emotion' 4 | import { Button } from 'app/components'; 5 | 6 | const activeStyle = css`font-weight: bold;`; 7 | const disabledStyle = css`color: #aaa;`; 8 | 9 | type Props = { 10 | table: { 11 | id: number, 12 | name: string, 13 | minBet: number, 14 | players: Array<{}>, 15 | maxPlayers: number, 16 | }, 17 | onTableClick: (tableId: number) => void, 18 | active: boolean, 19 | hasTableOpen: boolean, 20 | } 21 | class Table extends React.Component { 22 | render() { 23 | const { table, onTableClick, active, hasTableOpen } = this.props 24 | let className; 25 | if (active) { 26 | className = activeStyle; 27 | } else { 28 | className = hasTableOpen ? disabledStyle : ''; 29 | } 30 | return ( 31 | 32 | {table.name} 33 | ${table.minBet.toFixed(2)} / ${(table.minBet * 2).toFixed(2)} 34 | {table.players.length} / {table.maxPlayers} 35 | 36 | 42 | 43 | 44 | ) 45 | } 46 | } 47 | 48 | export default Table -------------------------------------------------------------------------------- /server/poker/seat.js: -------------------------------------------------------------------------------- 1 | class Seat { 2 | constructor(id, player, buyin, stack) { 3 | this.id = id 4 | this.player = player 5 | this.buyin = buyin 6 | this.stack = stack 7 | this.hand = [] 8 | this.bet = 0 9 | this.turn = false 10 | this.checked = true 11 | this.folded = true 12 | this.lastAction = null 13 | this.sittingOut = false 14 | } 15 | fold() { 16 | this.bet = 0 17 | this.folded = true 18 | this.lastAction = 'FOLD' 19 | this.turn = false 20 | } 21 | check() { 22 | this.checked = true 23 | this.lastAction = 'CHECK' 24 | this.turn = false 25 | } 26 | raise(amount) { 27 | const reRaiseAmount = amount - this.bet 28 | if (reRaiseAmount > this.stack) { return } 29 | 30 | this.bet = amount 31 | this.stack -= reRaiseAmount 32 | this.turn = false 33 | this.lastAction = 'RAISE' 34 | } 35 | placeBlind(amount) { 36 | this.bet = amount 37 | this.stack -= amount 38 | } 39 | callRaise(amount) { 40 | let amountCalled = amount - this.bet 41 | if (amountCalled >= this.stack) { 42 | amountCalled = this.stack 43 | } 44 | 45 | this.bet += amountCalled 46 | this.stack -= amountCalled 47 | this.turn = false 48 | this.lastAction = 'CALL' 49 | } 50 | winHand(amount) { 51 | this.bet = 0 52 | this.stack += amount 53 | this.turn = false 54 | this.lastAction = 'WINNER' 55 | } 56 | } 57 | 58 | module.exports = Seat -------------------------------------------------------------------------------- /client/app/components/Game/ChatAndInfo/Chat.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Message from './Message' 3 | import { blueGrey } from 'material-ui/styles/colors' 4 | import { withStyles, createStyleSheet } from 'material-ui/styles' 5 | import Input from 'material-ui/Input/Input' 6 | 7 | const styleSheet = createStyleSheet('GameChat', theme => ({ 8 | container: { 9 | height: '100%', 10 | }, 11 | messages: { 12 | fontFamily: 'Montserrat, sans-serif', 13 | height: '144px', 14 | padding: '3px', 15 | color: '#fff', 16 | background: blueGrey[900], 17 | overflowY: 'auto', 18 | }, 19 | input: { 20 | fontFamily: 'Montserrat, sans-serif', 21 | display: 'block', 22 | padding: '0px 3px', 23 | margin: '4px 0px', 24 | background: blueGrey[900], 25 | color: '#fff', 26 | overflow: 'hidden', 27 | } 28 | })) 29 | 30 | const Chat = ({ 31 | user, 32 | table, 33 | messages, 34 | onTableMessage, 35 | classes 36 | }) => ( 37 |
    38 |
    39 | {messages.map((message, index) => 40 | 41 | )} 42 |
    43 | 44 | 50 |
    51 | ) 52 | 53 | export default withStyles(styleSheet)(Chat) -------------------------------------------------------------------------------- /client/app/components/Groups/CreateGroupModal.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { css } from 'emotion' 3 | 4 | import Input from 'material-ui/Input'; 5 | import { Modal, Button } from 'app/components'; 6 | 7 | const buttons = css` 8 | text-align: right; 9 | `; 10 | const inputStyle = { 11 | width: '100%', 12 | fontSize: '20px', 13 | fontFamily: 'Montserrat, sans-serif', 14 | marginBottom: '32px', 15 | }; 16 | 17 | type Props = { 18 | open: boolean, 19 | createGroup: (groupAttrs: Object) => void, 20 | closeModal: () => void, 21 | }; 22 | class CreateGroupModal extends React.Component { 23 | handleSubmit = () => { 24 | this.props.createGroup({ name: this.groupName.value }); 25 | }; 26 | 27 | render() { 28 | return ( 29 | 30 |

    What's the name of your group?

    31 | { this.groupName = ref }} 33 | type="text" 34 | style={inputStyle} 35 | /> 36 |
    37 | 44 | 47 |
    48 |
    49 | ); 50 | } 51 | } 52 | 53 | export default CreateGroupModal -------------------------------------------------------------------------------- /client/app/components/Game/Seats/ShotClock.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import { css } from 'emotion' 4 | 5 | const shotClock = css` 6 | position: absolute; 7 | width: 100%; 8 | bottom: -3px; 9 | 10 | div { 11 | float: right; 12 | height: 3px; 13 | transition: 0.2s all; 14 | } 15 | ` 16 | 17 | type Props = { 18 | seconds: number 19 | } 20 | type State = { 21 | seconds: number 22 | } 23 | class ShotClock extends React.Component { 24 | interval: any 25 | 26 | constructor(props: Props) { 27 | super(props) 28 | 29 | this.state = { 30 | seconds: props.seconds 31 | } 32 | } 33 | 34 | componentDidMount() { 35 | this.interval = setInterval(() => { 36 | if (this.state.seconds === 0) { 37 | // Todo: this should fire some event that ends the player's turn 38 | } else { 39 | this.setState({ seconds: this.state.seconds - 1 }) 40 | } 41 | }, 1000) 42 | } 43 | 44 | componentWillUnmount() { 45 | clearInterval(this.interval) 46 | } 47 | 48 | render() { 49 | const { seconds } = this.state; 50 | if (seconds === 0) return null 51 | 52 | let background = '#ffeb3b' 53 | if (seconds <= 5) { 54 | background = '#ff5722'; 55 | } else if (seconds <= 10) { 56 | background = '#ff9800'; 57 | } 58 | 59 | return ( 60 |
    61 |
    62 |
    63 | ); 64 | } 65 | } 66 | 67 | export default ShotClock 68 | -------------------------------------------------------------------------------- /client/app/components/Game/TableControls/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'emotion' 3 | 4 | import Button from 'material-ui/Button' 5 | import Icon from 'material-ui/Icon' 6 | 7 | const styles = { 8 | container: { 9 | position: 'absolute', 10 | top: '-30px', 11 | left: '250px', 12 | }, 13 | button: { 14 | minWidth: '10px', 15 | minHeight: '10px', 16 | padding: '2px', 17 | }, 18 | } 19 | 20 | type Props = { 21 | onLeaveClick: () => void, 22 | onStandClick: () => void, 23 | onRotateClockwise: () => void, 24 | onRotateCounterClockwise: () => void, 25 | } 26 | function TableControls(props: Props) { 27 | const { 28 | onLeaveClick, 29 | onStandClick, 30 | onRotateClockwise, 31 | onRotateCounterClockwise, 32 | } = props 33 | 34 | return ( 35 |
    36 | 42 | 48 | 55 | 62 |
    63 | ) 64 | } 65 | 66 | export default TableControls -------------------------------------------------------------------------------- /client/app/components/Game/Seats/SeatedPlayer.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import Hand from './Hand' 4 | import ShotClock from './ShotClock' 5 | import Bet from './Bet' 6 | import Paper from 'material-ui/Paper' 7 | import { blueGrey, cyan } from 'material-ui/styles/colors' 8 | 9 | type Props = { 10 | user: { 11 | username: string, 12 | }, 13 | seat: { 14 | id: number, 15 | player: { 16 | name: string, 17 | }, 18 | hand: Array<{ 19 | rank: string, 20 | suit: string, 21 | }>, 22 | bet: number, 23 | stack: number, 24 | }, 25 | isButton: boolean, 26 | } 27 | class SeatedPlayer extends React.Component { 28 | render() { 29 | const { user, seat, isButton } = this.props 30 | 31 | return ( 32 |
    33 | {seat.bet > 0 && } 34 | 35 | {seat.hand.length > 0 && 36 | 37 | } 38 | 39 | 40 |
    41 | ${seat.stack.toFixed(2)} 42 |
    43 | 44 |
    45 |
    46 | {seat.id} 47 |
    48 |
    49 | {seat.player.name} 50 | {user.username === seat.player.name ? ' (me)' : ''} 51 | {isButton && B} 52 |
    53 |
    54 | 55 | {seat.turn && } 56 |
    57 |
    58 | ) 59 | } 60 | } 61 | 62 | export default SeatedPlayer 63 | -------------------------------------------------------------------------------- /server/db/models/__test__/user.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Sequelize = require('sequelize'); 3 | 4 | describe('User model', () => { 5 | let db; 6 | let User; 7 | let createdUser; 8 | 9 | before(async () => { 10 | db = new Sequelize('postgresql://localhost/poker_friends_test', { logging: false }); 11 | User = db.define('user', { 12 | username: Sequelize.STRING, 13 | password: Sequelize.STRING, 14 | location: Sequelize.STRING, 15 | bankroll: Sequelize.FLOAT, 16 | }); 17 | }) 18 | 19 | beforeEach(async () => { 20 | await User.sync(); 21 | createdUser = await User.create({ 22 | username: 'Byron', 23 | bankroll: 100, 24 | }); 25 | }) 26 | 27 | afterEach(async () => { 28 | await User.drop(); 29 | }) 30 | 31 | describe('finding a user by id', () => { 32 | it('should return the user', async () => { 33 | const user = await User.findById(createdUser.id) 34 | expect(user.username).to.equal('Byron') 35 | expect(user.bankroll).to.equal(100) 36 | }) 37 | }) 38 | 39 | describe('finding a user by name', () => { 40 | it('should return the user', async () => { 41 | const user = await User.findOne({ where: { username: createdUser.username } }) 42 | expect(user.username).to.equal('Byron'); 43 | expect(user.bankroll).to.equal(100); 44 | }); 45 | }); 46 | 47 | describe('updating the user', () => { 48 | it('should update the user', async () => { 49 | await User.update( 50 | { bankroll: 110 }, 51 | { where: { id: createdUser.id }} 52 | ) 53 | const user = await User.findById(createdUser.id) 54 | expect(user.username).to.equal('Byron') 55 | expect(user.bankroll).to.equal(110) 56 | }); 57 | }); 58 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var path = require('path') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | module.exports = { 6 | entry: ['./client/app/index.js', './client/scss/main.scss', 'webpack-hot-middleware/client', 'webpack/hot/dev-server'], 7 | output: { 8 | path: path.resolve(__dirname, 'server'), 9 | publicPath: '/', 10 | filename: 'bundle.js' 11 | }, 12 | resolve: { 13 | extensions: ['.js', '.jsx'], 14 | modules: [ 15 | path.resolve('./client/modules'), 16 | path.resolve('./node_modules'), 17 | ], 18 | alias: { 19 | 'app': path.resolve('./client/modules'), 20 | }, 21 | }, 22 | devtool: 'source-maps', 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.jsx?$/, 27 | exclude: /node_modules/, 28 | loaders: [ 29 | 'react-hot-loader', 30 | 'babel-loader?presets[]=es2015,presets[]=react,presets[]=stage-0,plugins[]=transform-class-properties' 31 | ] 32 | }, 33 | { // regular css files 34 | test: /\.css$/, 35 | loader: ExtractTextPlugin.extract({ 36 | use: 'css-loader?importLoaders=1', 37 | }), 38 | }, 39 | { // sass / scss loader for webpack 40 | test: /\.(sass|scss)$/, 41 | loader: ExtractTextPlugin.extract(['css-loader', 'sass-loader']) 42 | }, 43 | { 44 | test: /\.(jpe?g|png|gif|svg)$/i, 45 | loaders: [ 46 | 'file-loader?hash=sha512&digest=hex&name=[hash].[ext]', 47 | 'image-webpack-loader?bypassOnDebug&optimizationLevel=7&interlaced=false' 48 | ] 49 | } 50 | ] 51 | }, 52 | plugins: [ 53 | new ExtractTextPlugin({ 54 | filename: 'dist/[name].bundle.css', 55 | allChunks: true, 56 | }), 57 | new webpack.HotModuleReplacementPlugin(), 58 | new webpack.NoEmitOnErrorsPlugin() 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /client/app/components/lobby/MainMenu/TableList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from 'emotion'; 3 | 4 | import Table from './Table' 5 | import { Panel } from 'app/components'; 6 | 7 | const tableList = css` 8 | text-align: center; 9 | font-size: 16px; 10 | border-spacing: 0 5px; 11 | ` 12 | const tableHeader = css` 13 | padding-bottom: 10px; 14 | font-weight: 600; 15 | font-size: 20px; 16 | ` 17 | 18 | type Props = { 19 | tables: { 20 | [id: string]: Object, 21 | }, 22 | openTables: Array<{}>, 23 | onTableClick: () => void, 24 | hasTableOpen: boolean, 25 | } 26 | 27 | class TableList extends React.Component { 28 | render() { 29 | const { tables, openTables, onTableClick, hasTableOpen } = this.props 30 | if (Object.keys(tables).length > 0) { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {Object.keys(tables).map((id) => { 44 | const active = openTables.indexOf(id.toString()) !== -1 ? true : false 45 | return ( 46 |
    NameStakesPlayers
    53 | ) 54 | })} 55 | 56 |
    57 |
    58 | ) 59 | } else { 60 | return
    Loading...
    61 | } 62 | } 63 | } 64 | 65 | export default TableList -------------------------------------------------------------------------------- /server/routes/hands.js: -------------------------------------------------------------------------------- 1 | const db = require('../db/models'); 2 | const jwt = require('jsonwebtoken'); 3 | 4 | module.exports = { 5 | handHistory(req, res) { 6 | jwt.verify(req.body.token, process.env.JWT_SECRET, (err, decoded) => { 7 | if (err) { 8 | return res.status(404).json({ 9 | error: true, 10 | message: 'JWT invalid' 11 | }) 12 | } 13 | 14 | db.User 15 | .find({ where: { id: decoded.id } }) 16 | .then(user => { 17 | if (!user) { 18 | return res.status(404).json({ 19 | error: true, 20 | message: 'JWT invalid' 21 | }) 22 | } 23 | 24 | const limit = 20; 25 | let offset = 0; 26 | 27 | db.Hand.findAndCountAll({ 28 | include: [{ 29 | model: db.UserHand, 30 | where: { user_id: user.id }, 31 | }] 32 | }) 33 | .then(data => { 34 | const page = req.params.page; 35 | const pages = Math.ceil(data.count / limit); 36 | offset = limit * (page - 1); 37 | 38 | db.Hand.findAll({ 39 | include: [{ 40 | model: db.UserHand, 41 | where: { user_id: user.id }, 42 | }], 43 | limit, 44 | offset, 45 | order: [ 46 | ['createdAt', 'desc'], 47 | ], 48 | }) 49 | .then(hands => { 50 | res.send({ 51 | hands: hands, 52 | count: data.count, 53 | pages: pages, 54 | }) 55 | }) 56 | .catch(err => { 57 | res.status(500).send(`Internal Server Error: ${err}`) 58 | }) 59 | }) 60 | .catch(err => { 61 | res.status(500).send(`Internal Server Error: ${err}`) 62 | }) 63 | }) 64 | }) 65 | }, 66 | } -------------------------------------------------------------------------------- /client/app/components/Game/Pieces/Card.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import { css } from 'emotion' 4 | import Paper from 'material-ui/Paper' 5 | import { 6 | red, 7 | grey, 8 | blue, 9 | green 10 | } from 'material-ui/styles/colors' 11 | 12 | const suits = { 13 | 'spades': '♠', 14 | 'diamonds': '♦', 15 | 'hearts': '♥', 16 | 'clubs': '♣' 17 | } 18 | const ranks = { 19 | 'ace': 'A', 20 | 'king': 'K', 21 | 'queen': 'Q', 22 | 'jack': 'J', 23 | '10': 'T' 24 | } 25 | const smallCard = css`padding: 4px 2px; margin: 0 4px; display: inline-block; font-size: 32px;` 26 | const smallCardInner = css`display: flex` 27 | 28 | const getSuitColor = suit => { 29 | switch(suit) { 30 | case 'spades': 31 | return { color: grey[900] } 32 | case 'diamonds': 33 | return { color: blue[500] } 34 | case 'hearts': 35 | return { color: red[500] } 36 | case 'clubs': 37 | return { color: green[500] } 38 | default: 39 | return { color: 'purple' } 40 | } 41 | } 42 | 43 | type Props = { 44 | card: { 45 | rank: string, 46 | suit: string, 47 | }, 48 | small?: boolean, 49 | } 50 | function Card(props: Props) { 51 | const { card } = props 52 | 53 | if (card.rank === '0') { 54 | return
    55 | } else if (card.rank === 'hidden') { 56 | return
    57 | } else { 58 | return ( 59 | 60 |
    61 |
    {ranks[card.rank] ? ranks[card.rank] : card.rank}
    62 |
    {suits[card.suit]}
    63 |
    64 | {!props.small && 65 |
    66 | {ranks[card.rank] ? ranks[card.rank] : card.rank} 67 |
    68 | } 69 |
    70 | ) 71 | } 72 | } 73 | 74 | export default Card -------------------------------------------------------------------------------- /server/poker/deck.js: -------------------------------------------------------------------------------- 1 | var Deck = (function () { 2 | function Deck(opt) { 3 | if (opt === void 0) { opt = {}; } 4 | this._opt = { 5 | extend: opt['extend'] || [], 6 | suits: opt['suits'] || ['spades', 'hearts', 'diamonds', 'clubs'], 7 | ranks: opt['ranks'] || ['ace', 'king', 'queen', 'jack', '10', '9', '8', '7', '6', '5', '4', '3', '2'], 8 | multiply: opt['multiply'] || 1 9 | }; 10 | if (this._opt.multiply < 1) 11 | this._opt.multiply = 1; 12 | this.shuffle(); 13 | } 14 | Deck.prototype.shuffle = function () { 15 | this.cards = []; 16 | for (var i = 0; i < this._opt.multiply; i++) { 17 | for (var _i = 0, _a = this._opt.suits; _i < _a.length; _i++) { 18 | var suit = _a[_i]; 19 | for (var _b = 0, _c = this._opt.ranks; _b < _c.length; _b++) { 20 | var rank = _c[_b]; 21 | this.inlay({ suit: suit, rank: rank }); 22 | } 23 | } 24 | for (var _d = 0, _e = this._opt.extend; _d < _e.length; _d++) { 25 | var card = _e[_d]; 26 | if (!card.limit || i < card.limit) 27 | this.inlay({ suit: card.suit, rank: card.rank }); 28 | } 29 | } 30 | }; 31 | Deck.prototype.inlay = function (card) { 32 | if (card && card.suit && card.rank) { 33 | this.cards.push(card); 34 | return true; 35 | } 36 | else 37 | return false; 38 | }; 39 | Deck.prototype.count = function () { 40 | return this.cards.length; 41 | }; 42 | Deck.prototype.draw = function () { 43 | var count = this.count(); 44 | if (count > 0) 45 | return this.cards.splice(Math.floor(Math.random() * count), 1)[0] || null; 46 | else 47 | return null; 48 | }; 49 | return Deck; 50 | }()); 51 | var module = module; 52 | if (typeof module == "object" && typeof module.exports == "object") 53 | module.exports = Deck; -------------------------------------------------------------------------------- /client/app/components/Game/Pieces/ChipPile.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import ChipStack from './ChipStack' 4 | import { withStyles, createStyleSheet } from 'material-ui/styles' 5 | import { red, green, purple, orange } from 'material-ui/styles/colors' 6 | 7 | const styleSheet = createStyleSheet('ChipPile', () => ({ 8 | chipPile: { 9 | display: 'flex', 10 | alignItems: 'flex-end', 11 | justifyContent: 'center', 12 | } 13 | })) 14 | 15 | type Props = { 16 | amount: number, 17 | classes: Object, 18 | } 19 | const ChipPile = ({ amount, classes }: Props) => { 20 | // deal with floating point numbers 21 | let cents = amount * 100 22 | 23 | let tenDollarChips, 24 | oneDollarChips, 25 | fiftyCentChips, 26 | tenCentChips, 27 | fiveCentChips, 28 | oneCentChips 29 | 30 | tenDollarChips = Math.floor(cents / 1000) 31 | cents -= tenDollarChips * 1000 32 | 33 | oneDollarChips = Math.floor(cents / 100) 34 | cents -= oneDollarChips * 100 35 | 36 | fiftyCentChips = Math.floor(cents / 50) 37 | cents -= fiftyCentChips * 50 38 | 39 | tenCentChips = Math.floor(cents / 10) 40 | cents -= tenCentChips * 10 41 | 42 | fiveCentChips = Math.floor(cents / 5) 43 | cents -= fiveCentChips * 5 44 | 45 | oneCentChips = Math.round(cents) 46 | 47 | return ( 48 |
    49 | {tenDollarChips > 0 && 50 | 51 | } 52 | {oneDollarChips > 0 && 53 | 54 | } 55 | {fiftyCentChips > 0 && 56 | 57 | } 58 | {tenCentChips > 0 && 59 | 60 | } 61 | {fiveCentChips > 0 && 62 | 63 | } 64 | {oneCentChips > 0 && 65 | 66 | } 67 |
    68 | ) 69 | } 70 | 71 | export default withStyles(styleSheet)(ChipPile) -------------------------------------------------------------------------------- /client/app/reducers/groups.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_GROUPS, 3 | REQUEST_GROUPS_SUCCESS, 4 | REQUEST_GROUPS_FAILURE, 5 | CREATE_GROUP_REQUEST, 6 | CREATE_GROUP_SUCCESS, 7 | CREATE_GROUP_FAILURE, 8 | DELETE_GROUP_REQUEST, 9 | DELETE_GROUP_SUCCESS, 10 | DELETE_GROUP_FAILURE, 11 | } from '../actions/groups' 12 | 13 | const initialState = { 14 | groups: [], 15 | isLoading: false, 16 | errorMessage: '', 17 | } 18 | 19 | function groups(state = initialState, action) { 20 | switch (action.type) { 21 | case REQUEST_GROUPS: 22 | return { 23 | ...state, 24 | isLoading: true, 25 | errorMessage: '', 26 | } 27 | case REQUEST_GROUPS_SUCCESS: 28 | return { 29 | ...state, 30 | groups: action.groups, 31 | isLoading: false, 32 | errorMessage: '', 33 | } 34 | case REQUEST_GROUPS_FAILURE: 35 | return { 36 | ...state, 37 | isLoading: false, 38 | errorMessage: action.message.response.data.message, 39 | } 40 | case CREATE_GROUP_REQUEST: 41 | return { 42 | ...state, 43 | isLoading: true, 44 | errorMessage: '', 45 | } 46 | case CREATE_GROUP_SUCCESS: 47 | return { 48 | ...state, 49 | groups: [...state.groups, action.group], 50 | isLoading: false, 51 | errorMessage: '', 52 | } 53 | case CREATE_GROUP_FAILURE: 54 | return { 55 | ...state, 56 | isLoading: false, 57 | errorMessage: action.message.response.data.message, 58 | } 59 | case DELETE_GROUP_REQUEST: 60 | return { 61 | ...state, 62 | isLoading: true, 63 | errorMessage: '', 64 | } 65 | case DELETE_GROUP_SUCCESS: 66 | return { 67 | ...state, 68 | groups: state.groups.filter(group => group.id !== action.deletedGroupId), 69 | isLoading: false, 70 | errorMessage: '', 71 | } 72 | case DELETE_GROUP_FAILURE: 73 | return { 74 | ...state, 75 | isLoading: false, 76 | errorMessage: action.message.response.data.message, 77 | } 78 | default: 79 | return state 80 | } 81 | } 82 | 83 | export default groups -------------------------------------------------------------------------------- /client/app/index.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'development') { 2 | require('!style-loader!css-loader!sass-loader!../scss/main.scss') 3 | } 4 | import React from 'react' 5 | import ReactDOM from 'react-dom' 6 | import { MuiThemeProvider, createMuiTheme } from 'material-ui/styles' 7 | import createPalette from 'material-ui/styles/palette' 8 | import { blue, red } from 'material-ui/styles/colors' 9 | 10 | import { Provider } from 'react-redux' 11 | import { Router, Route, IndexRedirect, hashHistory } from 'react-router' 12 | import { syncHistoryWithStore, push } from 'react-router-redux' 13 | import store from './store/store' 14 | 15 | import App from './components/App' 16 | import NoMatch from './components/NoMatch' 17 | import Lobby from './components/Lobby' 18 | import Login from './components/Login' 19 | import Signup from './components/Signup' 20 | import Playground from './components/Playground' 21 | import HandHistory from './components/HandHistory' 22 | import Groups from './components/Groups' 23 | 24 | const history = syncHistoryWithStore(hashHistory, store) 25 | 26 | function requireLogin() { 27 | if (!store.getState().user.isAuthenticated) { 28 | store.dispatch(push('/login')) 29 | return 30 | } 31 | } 32 | 33 | const Root = () => ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ) 50 | 51 | const theme = createMuiTheme({ 52 | palette: createPalette({ 53 | type: 'light', 54 | primary: blue, 55 | accent: red 56 | }), 57 | }) 58 | 59 | ReactDOM.render( 60 | 61 | 62 | , 63 | document.getElementById('root') 64 | ) 65 | -------------------------------------------------------------------------------- /client/scss/main.scss: -------------------------------------------------------------------------------- 1 | $blue-lighten-5: #e3f2fd; 2 | $blue-lighten-4: #bbdefb; 3 | $blue-lighten-3: #90caf9; 4 | $blue-lighten-2: #64b5f6; 5 | $blue-lighten-1: #42a5f5; 6 | $blue: #2196f3; 7 | $blue-darken-1: #1e88e5; 8 | $blue-darken-2: #1976d2; 9 | $blue-darken-3: #1565c0; 10 | $blue-darken-4: #0d47a1; 11 | 12 | $amber: #ffc107; 13 | $amber-darken-1: #ffb300; 14 | $amber-darken-2: #ffa000; 15 | $amber-darken-3: #ff8f00; 16 | $amber-darken-4: #ff6f00; 17 | 18 | $orange: #ff9800; 19 | $orange-accent-1: #ffd180; 20 | $orange-accent-2: #ffab40; 21 | $orange-accent-3: #ff9100; 22 | $orange-accent-4: #ff6d00; 23 | 24 | $deep-orange-lighten-2: #ff8a65; 25 | $deep-orange-lighten-1: #ff7043; 26 | $deep-orange: #ff5722; 27 | $deep-orange-darken-1: #f4511e; 28 | $deep-orange-darken-2: #e64a19; 29 | 30 | $cyan-lighten-3: #80deea; 31 | $cyan-lighten-2: #4dd0e1; 32 | $cyan-lighten-1: #26c6da; 33 | $cyan: #00bcd4; 34 | 35 | $teal-lighten-3: #80cbc4; 36 | $teal-lighten-2: #4db6ac; 37 | $teal-lighten-1: #26a69a; 38 | $teal: #009688; 39 | 40 | $grey-lighten-5: #fafafa; 41 | $grey-lighten-4: #f5f5f5; 42 | $grey-lighten-3: #eeeeee; 43 | 44 | $blue-grey-lighten-5: #eceff1; 45 | $blue-grey-lighten-4: #cfd8dc; 46 | $blue-grey-lighten-3: #b0bec5; 47 | $blue-grey-lighten-2: #90a4ae; 48 | $blue-grey-lighten-1: #78909c; 49 | $blue-grey: #607d8b; 50 | $blue-grey-darken-1: #546e7a; 51 | $blue-grey-darken-2: #455a64; 52 | $blue-grey-darken-3: #37474f; 53 | $blue-grey-darken-4: #263238; 54 | 55 | // $box-shadow: 0px 1px 2px #222; 56 | $box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); 57 | 58 | @import './card.scss'; 59 | @import './seats.scss'; 60 | @import './small_table.scss'; 61 | @import './table.scss'; 62 | @import url('https://fonts.googleapis.com/css?family=Oswald:400,700'); 63 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400'); 64 | @import url('https://fonts.googleapis.com/css?family=Lato'); 65 | @import url('https://fonts.googleapis.com/css?family=Montserrat:400,600'); 66 | 67 | body { 68 | margin: 0px; 69 | overflow: hidden; 70 | font-size: 16px; 71 | font-family: Montserrat, sans-serif; 72 | } 73 | button, input { 74 | font-size: inherit; 75 | font-family: inherit; 76 | } 77 | th { 78 | font-weight: normal; 79 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "poker_friends", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server/index.js", 6 | "scripts": { 7 | "start": "node ./server/index", 8 | "test-poker": "mocha ./server/poker/__test__/*.js", 9 | "test-models": "mocha ./server/db/models/__test__/*.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "axios": "^0.16.1", 16 | "babel-core": "^6.21.0", 17 | "babel-loader": "^6.2.10", 18 | "babel-plugin-transform-class-properties": "^6.19.0", 19 | "babel-preset-es2015": "^6.18.0", 20 | "babel-preset-react": "^6.16.0", 21 | "babel-preset-stage-0": "^6.24.1", 22 | "bcrypt-nodejs": "0.0.3", 23 | "body-parser": "^1.16.0", 24 | "classnames": "^2.2.5", 25 | "cookie-parser": "^1.4.3", 26 | "css-loader": "^0.28.0", 27 | "emotion": "^8.0.12", 28 | "express": "^4.14.0", 29 | "extract-text-webpack-plugin": "^2.1.0", 30 | "foreman": "^2.0.0", 31 | "jsonwebtoken": "^7.4.0", 32 | "lodash": "^4.17.5", 33 | "material-ui": "^1.0.0-alpha.20", 34 | "material-ui-icons": "^1.0.0-alpha.19", 35 | "moment": "^2.18.1", 36 | "node-gyp": "^3.5.0", 37 | "node-sass": "^4.5.2", 38 | "path": "^0.12.7", 39 | "pg": "^6.1.5", 40 | "pg-hstore": "^2.3.2", 41 | "pokersolver": "^2.1.2", 42 | "random-id": "0.0.2", 43 | "react": "^15.4.2", 44 | "react-dom": "^15.4.2", 45 | "react-redux": "^5.0.4", 46 | "react-router": "^3.0.2", 47 | "react-router-redux": "^4.0.8", 48 | "redux": "^3.6.0", 49 | "redux-logger": "^3.0.1", 50 | "redux-thunk": "^2.2.0", 51 | "sass-loader": "^6.0.3", 52 | "sequelize": "^3.30.4", 53 | "socket.io": "^1.7.2", 54 | "style-loader": "^0.16.1", 55 | "underscore": "^1.8.3", 56 | "webpack": "^2.2.0-rc.4" 57 | }, 58 | "devDependencies": { 59 | "babel-eslint": "^8.0.3", 60 | "chai": "^3.5.0", 61 | "eslint": "^4.13.1", 62 | "eslint-config-airbnb": "^16.1.0", 63 | "eslint-plugin-import": "^2.8.0", 64 | "eslint-plugin-jsx-a11y": "^6.0.3", 65 | "eslint-plugin-react": "^7.5.1", 66 | "flow-bin": "^0.64.0", 67 | "mocha": "^3.3.0", 68 | "react-hot-loader": "^1.3.1", 69 | "webpack-dev-middleware": "^1.10.2", 70 | "webpack-hot-middleware": "^2.18.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /client/scss/table.scss: -------------------------------------------------------------------------------- 1 | .poker-game { 2 | position: relative; 3 | max-width: 1200px; 4 | height: 100vh; 5 | box-sizing: border-box; 6 | z-index: 0; 7 | // font-family: 'Oswald', sans-serif; 8 | overflow: hidden; 9 | margin: 0px auto; 10 | 11 | .table-bg { 12 | width: 900px; 13 | height: 420px; 14 | position: absolute; 15 | left: 115px; 16 | top: 135px; 17 | box-sizing: border-box; 18 | z-index: -1; 19 | 20 | @media screen and (min-height: 1050px) { 21 | top: 205px; 22 | } 23 | 24 | .bg-outer { 25 | // background: $teal; 26 | border: 22px solid #2a2a2a; 27 | height: 100%; 28 | position: absolute; 29 | // box-shadow: 2px 2px 20px #000; 30 | 31 | .bg-inner { 32 | width: calc(100% - 10px); 33 | height: calc(100% - 10px); 34 | border: 5px solid #aaa; 35 | } 36 | } 37 | .bg-outer:first-child, 38 | .bg-outer:last-child { 39 | width: 400px; 40 | top: 0px; 41 | border-radius: 50%; 42 | z-index: -2; 43 | 44 | .bg-inner { 45 | border-radius: 50%; 46 | } 47 | } 48 | .bg-outer:first-child { 49 | left: 0px; 50 | } 51 | .bg-outer:last-child { 52 | right: 0px; 53 | } 54 | .bg-outer:nth-child(2) { 55 | width: 450px; 56 | left: 225px; 57 | border-left: none; 58 | border-right: none; 59 | z-index: -1; 60 | 61 | .bg-inner { 62 | width: 100%; 63 | border-left: none; 64 | border-right: none; 65 | } 66 | } 67 | } 68 | .board { 69 | position: relative; 70 | margin-top: 296px; 71 | width: 405px; 72 | left: 363px; 73 | text-align: center; 74 | color: white; 75 | 76 | @media screen and (min-height: 1050px) { 77 | margin-top: 371px; 78 | } 79 | 80 | .main-pot { 81 | position: absolute; 82 | width: 100%; 83 | bottom: 140px; 84 | } 85 | .card { 86 | margin: 8px 3px; 87 | } 88 | .chip-pile { 89 | position: absolute; 90 | top: -10%; 91 | left: 35%; 92 | } 93 | .card-silhouette { 94 | display: inline-block; 95 | border: 1px dashed #ccc; 96 | width: 75px; 97 | height: 105px; 98 | margin: 8px 3px; 99 | border-radius: 4px; 100 | vertical-align: top; 101 | box-sizing: border-box; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | const db = require('../db/models'); 2 | const jwt = require('jsonwebtoken'); 3 | const bcrypt = require('bcrypt-nodejs'); 4 | const utils = require('../utils'); 5 | 6 | module.exports = { 7 | signup(req, res) { 8 | const { body } = req; 9 | const hash = bcrypt.hashSync(body.password.trim()) 10 | 11 | db.User 12 | .find({ where: { username: body.username } }) 13 | .then(user => { 14 | if (!user) { 15 | db.User.create({ 16 | username: body.username, 17 | password: hash 18 | }) 19 | .then(user => { 20 | res.send({ 21 | user: utils.getCleanUser(user), 22 | token: utils.generateToken(user) 23 | }) 24 | }) 25 | } else { 26 | return res.status(404).json({ 27 | error: true, 28 | message: 'Username already exists' 29 | }) 30 | } 31 | }) 32 | }, 33 | login(req, res) { 34 | const { body } = req; 35 | 36 | db.User 37 | .find({ where: { username: body.username } }) 38 | .then(user => { 39 | if (!user) { 40 | return res.status(404).json({ 41 | error: true, 42 | message: 'Username or password is wrong' 43 | }) 44 | } 45 | 46 | bcrypt.compare(body.password, user.password, (err, valid) => { 47 | if (!valid) { 48 | return res.status(404).json({ 49 | error: true, 50 | message: 'Username or password is wrong' 51 | }) 52 | } 53 | 54 | res.send({ 55 | user: utils.getCleanUser(user), 56 | token: utils.generateToken(user) 57 | }) 58 | }) 59 | }) 60 | }, 61 | verifyToken(req, res) { 62 | jwt.verify(req.body.token, process.env.JWT_SECRET, (err, decoded) => { 63 | if (err) { 64 | return res.status(404).json({ 65 | error: true, 66 | message: 'JWT invalid' 67 | }) 68 | } 69 | 70 | db.User 71 | .find({ where: { id: decoded.id } }) 72 | .then(user => { 73 | if (!user) { 74 | return res.status(404).json({ 75 | error: true, 76 | message: 'JWT invalid' 77 | }) 78 | } 79 | 80 | res.send({ 81 | user: utils.getCleanUser(user), 82 | token: utils.generateToken(user) 83 | }) 84 | }) 85 | }) 86 | }, 87 | } -------------------------------------------------------------------------------- /server/poker/__test__/winner.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | 3 | var Table = require('../table') 4 | var Player = require('../player') 5 | 6 | describe('When the winner is determined', () => { 7 | let table 8 | let player1 9 | let player2 10 | const raiseAmount = 5 11 | 12 | beforeEach(() => { 13 | table = new Table(1, 'Test table', 6, 10) 14 | player1 = new Player('1', 1, 'Byron', 100) 15 | player2 = new Player('2', 2, 'Alice', 100) 16 | 17 | table.sitPlayer(player1, 1, 5) 18 | table.sitPlayer(player2, 2, 5) 19 | table.startHand() 20 | }) 21 | 22 | describe('a straight vs two pair', () => { 23 | beforeEach(() => { 24 | table.deck.cards = [ 25 | { rank: 'ace', suit: 'diamonds' }, 26 | { rank: '5', suit: 'diamonds' }, 27 | { rank: '2', suit: 'spades' }, 28 | { rank: '8', suit: 'hearts' }, 29 | { rank: '9', suit: 'hearts' }, 30 | ] 31 | table.seats[1].hand = [ 32 | { rank: 'ace', suit: 'spades' }, 33 | { rank: '5', suit: 'spades' }, 34 | ] 35 | table.seats[2].hand = [ 36 | { rank: '3', suit: 'clubs' }, 37 | { rank: '4', suit: 'clubs' }, 38 | ] 39 | table.handleRaise('2', raiseAmount) 40 | table.changeTurn(2) 41 | table.handleCall('1') 42 | table.changeTurn(1) 43 | }) 44 | it('the player with the straight wins', () => { 45 | expect(+(table.seats[1].stack).toFixed(2)).to.be.equal(0) 46 | expect(+(table.seats[2].stack).toFixed(2)).to.be.equal(10) 47 | }) 48 | }) 49 | 50 | describe('two pair vs two pair', () => { 51 | beforeEach(() => { 52 | table.deck.cards = [ 53 | { rank: 'ace', suit: 'diamonds' }, 54 | { rank: '5', suit: 'diamonds' }, 55 | { rank: '3', suit: 'spades' }, 56 | { rank: '4', suit: 'hearts' }, 57 | { rank: 'jack', suit: 'hearts' }, 58 | ] 59 | table.seats[1].hand = [ 60 | { rank: 'ace', suit: 'spades' }, 61 | { rank: '5', suit: 'spades' }, 62 | ] 63 | table.seats[2].hand = [ 64 | { rank: '3', suit: 'clubs' }, 65 | { rank: '4', suit: 'clubs' }, 66 | ] 67 | table.handleRaise('2', raiseAmount) 68 | table.changeTurn(2) 69 | table.handleCall('1') 70 | table.changeTurn(1) 71 | }) 72 | it('the player with the higher two pair wins', () => { 73 | expect(+(table.seats[1].stack).toFixed(2)).to.be.equal(10) 74 | expect(+(table.seats[2].stack).toFixed(2)).to.be.equal(0) 75 | }) 76 | }) 77 | }) -------------------------------------------------------------------------------- /client/app/components/Signup.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { css } from 'emotion' 5 | 6 | import { signUp } from '../actions/user' 7 | 8 | import Icon from 'material-ui/Icon' 9 | import Input from 'material-ui/Input'; 10 | import { Panel, Button, Text, Link } from 'app/components' 11 | 12 | const container = css` 13 | max-width: 350px; 14 | padding-top: 200px; 15 | margin: 0 auto 0; 16 | `; 17 | const inputStyle = { 18 | fontFamily: 'Montserrat, sans-serif', 19 | width: '100%', 20 | marginBottom: '16px' 21 | }; 22 | const linkStyle = { 23 | display: 'flex', 24 | alignItems: 'center', 25 | marginTop: '12px', 26 | color: '#2196f3', 27 | }; 28 | 29 | type Props = { 30 | signUp: ({ username: string, password: string }) => void, 31 | isFetching: boolean, 32 | errorMessage: string, 33 | }; 34 | class Signup extends React.Component { 35 | username: { value: ?string } 36 | password: { value: ?string } 37 | 38 | handleSubmit = () => { 39 | const username = this.username.value 40 | const password = this.password.value 41 | 42 | if (!username || !password) { return } 43 | this.props.signUp({ username, password }) 44 | } 45 | 46 | render() { 47 | const { isFetching, errorMessage } = this.props 48 | 49 | return ( 50 |
    51 | 52 | {this.username = ref}} 56 | style={inputStyle} 57 | /> 58 | {this.password = ref}} 62 | style={inputStyle} 63 | /> 64 | 65 | 66 | 67 | 68 | Already have an account? Login 69 | trending_flat 70 | 71 | 72 | 73 | {isFetching &&
    Attemping signup...
    } 74 | {errorMessage &&
    {errorMessage}
    } 75 |
    76 |
    77 | ) 78 | } 79 | } 80 | 81 | function mapStateToProps(state) { 82 | return { 83 | isFetching: state.user.isFetching, 84 | errorMessage: state.user.errorMessage 85 | } 86 | } 87 | 88 | const mapDispatchToProps = ({ signUp }) 89 | 90 | export default connect( 91 | mapStateToProps, 92 | mapDispatchToProps 93 | )(Signup) 94 | -------------------------------------------------------------------------------- /client/app/components/Login.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { css } from 'emotion' 5 | 6 | import { login } from '../actions/user' 7 | 8 | import Icon from 'material-ui/Icon' 9 | import Input from 'material-ui/Input'; 10 | import { Panel, Button, Text, Link } from 'app/components' 11 | 12 | const container = css` 13 | max-width: 350px; 14 | padding-top: 200px; 15 | margin: 0 auto 0; 16 | ` 17 | const inputStyle = { 18 | fontFamily: 'Montserrat, sans-serif', 19 | width: '100%', 20 | marginBottom: '16px' 21 | }; 22 | const linkStyle = { 23 | display: 'flex', 24 | alignItems: 'center', 25 | marginTop: '12px', 26 | color: '#2196f3' 27 | }; 28 | 29 | type Props = { 30 | login: ({ username: string, password: string }) => void, 31 | isFetching: boolean, 32 | errorMessage: ?string, 33 | } 34 | class Login extends React.Component { 35 | username: { value: ?string } 36 | password: { value: ?string } 37 | 38 | handleSubmit = () => { 39 | const username = this.username.value 40 | const password = this.password.value 41 | 42 | if (!username || !password) { return } 43 | this.props.login({ username, password }) 44 | } 45 | 46 | render() { 47 | const { isFetching, errorMessage } = this.props 48 | 49 | return ( 50 |
    51 | 52 | {this.username = ref}} 56 | style={inputStyle} 57 | /> 58 | {this.password = ref}} 62 | style={inputStyle} 63 | /> 64 | 65 | 66 | 67 | 68 | Create a new account 69 | trending_flat 70 | 71 | 72 | 73 | {isFetching && Attemping login...} 74 | {errorMessage && {errorMessage}} 75 | 76 |
    77 | ) 78 | } 79 | } 80 | 81 | function mapStateToProps(state) { 82 | return { 83 | isFetching: state.user.isFetching, 84 | errorMessage: state.user.errorMessage 85 | } 86 | } 87 | 88 | const mapDispatchToProps = ({ login }) 89 | 90 | export default connect( 91 | mapStateToProps, 92 | mapDispatchToProps 93 | )(Login) 94 | -------------------------------------------------------------------------------- /client/app/components/Game/Seats/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import SeatedPlayer from './SeatedPlayer' 4 | import EmptySeat from './EmptySeat' 5 | 6 | type Props = { 7 | user: { 8 | id: number, 9 | username: string, 10 | }, 11 | table: { 12 | id: number, 13 | seats: { 14 | [seatId: any]: ?{ 15 | id: number, 16 | player: { 17 | name: string, 18 | }, 19 | hand: Array<{ 20 | rank: string, 21 | suit: string, 22 | }>, 23 | bet: number, 24 | stack: number, 25 | }, 26 | }, 27 | maxPlayers: number, 28 | }, 29 | onSeatClick: (tableId: number, seatId: number) => void, 30 | displayOffset: number, 31 | } 32 | class Seats extends React.Component { 33 | render() { 34 | const { user, table, onSeatClick, displayOffset } = this.props 35 | let seats = Object.keys(table.seats) 36 | const seatedPlayerIds = seats.map(seatId => table.seats[seatId] && table.seats[seatId].id); 37 | const seated = seatedPlayerIds.includes(user.id); 38 | 39 | return ( 40 |
    41 | {seats.map(seatId => { 42 | const seat = table.seats[seatId] 43 | const isButton = parseInt(seatId) === table.button ? true : false 44 | 45 | let displayOrder 46 | if (parseInt(seatId) + displayOffset > table.maxPlayers) { 47 | displayOrder = parseInt(seatId) + displayOffset - table.maxPlayers 48 | } else { 49 | displayOrder = parseInt(seatId) + displayOffset 50 | } 51 | 52 | let className = `seat-container seat${displayOrder}of${table.maxPlayers}` 53 | if (seat && seat.turn) { 54 | className += ' active' 55 | } 56 | 57 | if (seat) { 58 | return ( 59 |
    60 | 65 |
    66 | ) 67 | } else if (!seated) { 68 | return ( 69 |
    70 | { 73 | if (e) e.stopPropagation() 74 | onSeatClick(table.id, parseInt(seatId)) 75 | }}/> 76 |
    77 | ) 78 | } else { 79 | return
    80 | } 81 | })} 82 |
    83 | ) 84 | } 85 | } 86 | 87 | export default Seats 88 | -------------------------------------------------------------------------------- /client/app/components/Groups/Group.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { css } from 'emotion' 4 | 5 | import { Modal, Panel, Button, Text, UserSearchModal } from 'app/components'; 6 | 7 | const container = css` 8 | width: calc(100% - 470px); 9 | `; 10 | 11 | type Props = { 12 | group: any, 13 | deleteGroup: (groupId: number) => void, 14 | } 15 | type State = { 16 | confirmModalOpen: boolean, 17 | inviteModalOpen: boolean, 18 | } 19 | class Group extends React.Component { 20 | state = { 21 | confirmModalOpen: false, 22 | inviteModalOpen: false, 23 | } 24 | 25 | toggleConfirm = () => { 26 | this.setState({ confirmModalOpen: !this.state.confirmModalOpen }) 27 | } 28 | 29 | toggleInvite = () => { 30 | this.setState({ inviteModalOpen: !this.state.inviteModalOpen }) 31 | } 32 | 33 | handleDeleteClick = () => { 34 | this.props.deleteGroup(this.props.group.id); 35 | this.toggleConfirm(); 36 | } 37 | 38 | createInvites = data => { 39 | console.log(data); 40 | } 41 | 42 | render() { 43 | const { group } = this.props; 44 | 45 | return ( 46 |
    47 | 48 |
    49 |
    50 | Search code:
    51 | {group.code} 52 |
    53 |
    54 |
    55 | 58 | 61 |
    62 |
    63 | 64 |

    65 | Are you sure you want to delete group: {group.name}? 66 |

    67 |
    68 | 74 | 77 |
    78 |
    79 | 85 |
    86 | ) 87 | } 88 | } 89 | 90 | export default Group -------------------------------------------------------------------------------- /client/app/reducers/lobby.js: -------------------------------------------------------------------------------- 1 | import Moment from 'moment' 2 | import { 3 | RECEIVE_LOBBY_INFO, 4 | TABLES_UPDATED, 5 | PLAYERS_UPDATED, 6 | TABLE_JOINED, 7 | TABLE_LEFT, 8 | TABLE_UPDATED, 9 | } from '../actions/lobby' 10 | 11 | const initialState = { 12 | tables: {}, 13 | players: {}, 14 | openTables: {}, 15 | messages: [] 16 | } 17 | 18 | function lobby(state = initialState, action) { 19 | let newOpenTables 20 | switch (action.type) { 21 | case RECEIVE_LOBBY_INFO: 22 | return { 23 | ...state, 24 | tables: action.tables, 25 | players: action.players 26 | } 27 | case TABLES_UPDATED: 28 | newOpenTables = state.openTables 29 | for (let tableId of Object.keys(newOpenTables)) { 30 | newOpenTables[tableId].table = action.tables[tableId] 31 | } 32 | 33 | return { 34 | ...state, 35 | tables: action.tables, 36 | openTables: newOpenTables 37 | } 38 | case PLAYERS_UPDATED: 39 | return { 40 | ...state, 41 | players: action.players 42 | } 43 | case TABLE_JOINED: 44 | newOpenTables = state.openTables 45 | newOpenTables[action.tableId] = { 46 | table: action.tables[action.tableId], 47 | messages: [] 48 | } 49 | 50 | return { 51 | ...state, 52 | tables: action.tables, 53 | openTables: newOpenTables 54 | } 55 | case TABLE_LEFT: 56 | newOpenTables = state.openTables 57 | delete newOpenTables[action.tableId] 58 | 59 | return { 60 | ...state, 61 | tables: action.tables, 62 | openTables: newOpenTables 63 | } 64 | case TABLE_UPDATED: 65 | newOpenTables = JSON.parse(JSON.stringify(state.openTables)) 66 | 67 | if (newOpenTables[action.table.id]) { 68 | newOpenTables[action.table.id].table = action.table 69 | 70 | if (action.message) { 71 | const newMessage = { 72 | message: action.message, 73 | from: action.from, 74 | timestamp: Moment().format('LTS') 75 | } 76 | newOpenTables[action.table.id].messages.push(newMessage) 77 | } 78 | 79 | for (let winMessage of action.table.winMessages) { 80 | const newWinMessage = { 81 | message: winMessage, 82 | from: action.from, 83 | timestamp: Moment().format('LTS') 84 | } 85 | newOpenTables[action.table.id].messages.push(newWinMessage) 86 | } 87 | } 88 | 89 | return { 90 | ...state, 91 | openTables: newOpenTables 92 | } 93 | default: 94 | return state 95 | } 96 | } 97 | 98 | export default lobby -------------------------------------------------------------------------------- /client/modules/components/UserSearchModal.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { connect } from 'react-redux' 3 | import { css } from 'emotion' 4 | 5 | import { searchUsers } from '../../app/actions/searchUsers' 6 | 7 | import Input from 'material-ui/Input'; 8 | import Modal from './Modal' 9 | import Button from './Button' 10 | 11 | const buttons = css` 12 | text-align: right; 13 | `; 14 | const inputStyle = { 15 | width: '100%', 16 | fontSize: '20px', 17 | fontFamily: 'Montserrat, sans-serif', 18 | marginBottom: '32px', 19 | }; 20 | 21 | type Props = { 22 | open: boolean, 23 | header: string, 24 | closeModal: () => void, 25 | onSubmit: () => void, 26 | token: string, 27 | searchUsers: (token: string, query: string) => void, 28 | searchResults: Array, 29 | } 30 | class UserSearchModal extends React.Component { 31 | handleSubmit = () => { 32 | const { searchResults, closeModal, token, searchUsers, onSubmit } = this.props 33 | searchUsers(token, '') 34 | onSubmit(searchResults) 35 | closeModal() 36 | } 37 | 38 | handleChange = (e: SyntheticEvent) => { 39 | e.preventDefault() 40 | const { token, searchUsers } = this.props 41 | searchUsers(token, e.target.value) 42 | } 43 | 44 | handleClose = () => { 45 | const { closeModal, token, searchUsers } = this.props 46 | searchUsers(token, '') 47 | closeModal() 48 | } 49 | 50 | render() { 51 | const { open, header, searchResults } = this.props; 52 | 53 | return ( 54 | 55 |

    {header}

    56 | 61 | {searchResults.length > 0 && ( 62 |
    63 | {searchResults.map((result, idx) =>
    {result.username}
    )} 64 |
    65 | )} 66 |
    67 | 74 | 77 |
    78 |
    79 | ) 80 | } 81 | } 82 | 83 | function mapStateToProps(state) { 84 | return { 85 | searchResults: state.searchUsers.searchResults, 86 | isFetching: state.searchUsers.isFetching, 87 | errorMessage: state.searchUsers.errorMessage, 88 | token: state.user.token, 89 | } 90 | } 91 | 92 | const mapDispatchToProps = ({ 93 | searchUsers, 94 | }) 95 | 96 | export default connect( 97 | mapStateToProps, 98 | mapDispatchToProps 99 | )(UserSearchModal) -------------------------------------------------------------------------------- /server/routes/groups.js: -------------------------------------------------------------------------------- 1 | const db = require('../db/models') 2 | const jwt = require('jsonwebtoken'); 3 | 4 | module.exports = { 5 | fetchGroups(req, res) { 6 | jwt.verify(req.query.access_token, process.env.JWT_SECRET, (err, decoded) => { 7 | if (err) { 8 | return res.status(404).json({ 9 | error: true, 10 | message: 'Invalid access token' 11 | }) 12 | } 13 | 14 | db.Group.findAll({ 15 | include: [{ 16 | model: db.GroupMember, 17 | where: { user_id: decoded.id }, 18 | }], 19 | }) 20 | .then(groups => { 21 | res.send({ 22 | groups: groups, 23 | }) 24 | }) 25 | .catch(err => { 26 | res.status(500).send(`Internal Server Error: ${err}`) 27 | }) 28 | }) 29 | }, 30 | createGroup(req, res) { 31 | jwt.verify(req.body.accessToken, process.env.JWT_SECRET, async (err, decoded) => { 32 | if (err) { 33 | return res.status(404).json({ 34 | error: true, 35 | message: 'Invalid access token' 36 | }) 37 | } 38 | 39 | const user = await db.User.findById(decoded.id) 40 | if (!user) throw new Error('User not found'); 41 | 42 | let group; 43 | await db.sequelize.transaction(async transaction => { 44 | group = await db.Group.create({ 45 | creator_id: user.id, 46 | name: req.body.groupAttrs.name, 47 | code: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15), 48 | }, { transaction }) 49 | await db.GroupMember.create({ 50 | group_id: group.id, 51 | user_id: user.id, 52 | is_admin: true, 53 | bankroll: 0, 54 | }, { transaction }) 55 | }) 56 | 57 | const createdGroup = await db.Group.findOne({ 58 | where: { id: group.id }, 59 | include: [{ 60 | model: db.GroupMember, 61 | }] 62 | }) 63 | 64 | if (!createdGroup) { 65 | res.status(500).send('Failed to create group'); 66 | } 67 | 68 | res.send({ group: createdGroup }); 69 | }) 70 | }, 71 | deleteGroup(req, res) { 72 | jwt.verify(req.query.accessToken, process.env.JWT_SECRET, async (err, decoded) => { 73 | if (err) { 74 | return res.status(404).json({ 75 | error: true, 76 | message: 'Invalid access token' 77 | }) 78 | } 79 | 80 | const group = await db.Group.findOne({ 81 | where: { 82 | $and: { 83 | id: req.params.groupId, 84 | creator_id: decoded.id, 85 | } 86 | } 87 | }) 88 | 89 | if (!group) { 90 | res.status(500).send('Could not find group to delete'); 91 | } 92 | 93 | await group.destroy(); 94 | res.send({ deletedGroupId: group.id }) 95 | }) 96 | } 97 | } -------------------------------------------------------------------------------- /client/scss/seats.scss: -------------------------------------------------------------------------------- 1 | @media screen and (min-height: 1050px) { 2 | .seat1of6 { 3 | left: 475px; 4 | top: 95px; 5 | 6 | .bet { 7 | top: 155px; 8 | } 9 | } 10 | .seat2of6 { 11 | top: 195px; 12 | left: 895px; 13 | 14 | .bet { 15 | top: 145px; 16 | left: -150px; 17 | } 18 | } 19 | .seat3of6 { 20 | top: 460px; 21 | left: 895px; 22 | 23 | .bet { 24 | left: -150px; 25 | bottom: 85px; 26 | } 27 | } 28 | .seat4of6 { 29 | top: 560px; 30 | left: 475px; 31 | 32 | .bet { 33 | bottom: 130px; 34 | } 35 | } 36 | .seat5of6 { 37 | top: 460px; 38 | left: 55px; 39 | 40 | .bet { 41 | right: -150px; 42 | bottom: 85px; 43 | } 44 | } 45 | .seat6of6 { 46 | top: 195px; 47 | left: 55px; 48 | 49 | .bet { 50 | top: 145px; 51 | right: -150px; 52 | } 53 | } 54 | } 55 | @media screen and (max-height: 1049px) { 56 | .seat1of6 { 57 | left: 475px; 58 | top: 25px; 59 | 60 | .bet { 61 | top: 170px; 62 | } 63 | } 64 | .seat2of6 { 65 | top: 125px; 66 | left: 895px; 67 | 68 | .bet { 69 | top: 160px; 70 | left: -150px; 71 | } 72 | } 73 | .seat3of6 { 74 | top: 390px; 75 | left: 895px; 76 | 77 | .bet { 78 | left: -150px; 79 | bottom: 110px; 80 | } 81 | } 82 | .seat4of6 { 83 | top: 490px; 84 | left: 475px; 85 | 86 | .bet { 87 | bottom: 150px; 88 | } 89 | } 90 | .seat5of6 { 91 | top: 390px; 92 | left: 55px; 93 | 94 | .bet { 95 | right: -150px; 96 | bottom: 110px; 97 | } 98 | } 99 | .seat6of6 { 100 | top: 125px; 101 | left: 55px; 102 | 103 | .bet { 104 | top: 160px; 105 | right: -150px; 106 | } 107 | } 108 | } 109 | .seat-container { 110 | color: #fff; 111 | width: 180px; 112 | height: 150px; 113 | position: absolute; 114 | text-align: center; 115 | z-index: 5; 116 | transition: 0.2s all; 117 | } 118 | .bet { 119 | position: absolute; 120 | width: 100%; 121 | } 122 | .seat-info { 123 | position: absolute; 124 | width: 100%; 125 | bottom: 5px; 126 | } 127 | .seat-number { 128 | padding: 4px 0px; 129 | display: inline-block; 130 | width: 20%; 131 | border-radius: 0 0 0 4px; 132 | } 133 | .seat-player { 134 | position: relative; 135 | display: inline-block; 136 | padding: 4px 0px; 137 | width: 80%; 138 | border-radius: 0 0 4px 0; 139 | } 140 | .seat-container.active .seat-info .seat-player { 141 | color: #ffeb3b; 142 | } 143 | .seat-stack { 144 | font-size: 24px; 145 | padding: 8px 0px; 146 | border-radius: 4px 4px 0 0; 147 | } 148 | .button-chip { 149 | color: #fff; 150 | position: absolute; 151 | right: 5px; 152 | top: -42px; 153 | font-size: 10px; 154 | } -------------------------------------------------------------------------------- /server/poker/__test__/betting.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | 3 | var Table = require('../table') 4 | var Player = require('../player') 5 | 6 | describe('Betting scenarios', () => { 7 | let table 8 | let player1 9 | let player2 10 | 11 | beforeEach(() => { 12 | table = new Table(1, 'Test table', 6, 10) 13 | player1 = new Player('socket1', 1, 'Byron', 100) 14 | player2 = new Player('socket2', 2, 'Alice', 100) 15 | 16 | table.sitPlayer(player1, 1, 10) 17 | table.sitPlayer(player2, 2, 5) 18 | table.startHand() 19 | }) 20 | 21 | describe('after the hand has been started', () => { 22 | it('the first sat player is the big blind', () => { 23 | expect(table.seats[1].bet).to.be.equal(table.limit/100) 24 | }) 25 | 26 | it('the second sat player is the small blind with the action', () => { 27 | expect(table.seats[2].bet).to.be.equal(table.limit/200) 28 | expect(table.seats[2].turn).to.be.true 29 | expect(table.turn).to.be.equal(2) 30 | }) 31 | }) 32 | 33 | describe('when the small blind folds preflop', () => { 34 | beforeEach(() => { 35 | table.handleFold('socket2') 36 | table.changeTurn(2) 37 | }) 38 | 39 | it('the big blind wins $0.05', () => { 40 | expect(table.seats[1].stack).to.be.equal(10.05) 41 | }) 42 | 43 | it('the small blind loses $0.05', () => { 44 | expect(table.seats[2].stack).to.be.equal(4.95) 45 | }) 46 | 47 | it('the hand is over', () => { 48 | expect(table.handOver).to.be.true 49 | }) 50 | }) 51 | 52 | describe('when the small blind calls preflop', () => { 53 | beforeEach(() => { 54 | table.handleCall('socket2') 55 | table.changeTurn(2) 56 | }) 57 | 58 | it('the small blind has a bet equal to the big blind', () => { 59 | expect(table.seats[2].bet).to.be.equal(table.seats[1].bet) 60 | }) 61 | 62 | it('the small blind\'s stack decreases by the amount called', () => { 63 | expect(table.callAmount).to.be.equal(.1) 64 | expect(table.seats[2].stack).to.be.equal(4.9) 65 | }) 66 | 67 | it('it is the big blind\'s turn', () => { 68 | expect(table.seats[1].turn).to.be.true 69 | expect(table.turn).to.be.equal(1) 70 | }) 71 | }) 72 | 73 | describe('when the small blind raises preflop', () => { 74 | beforeEach(() => { 75 | table.handleRaise('socket2', .3) 76 | table.changeTurn(2) 77 | }) 78 | 79 | it('the small blind has a bet larger than the big blind', () => { 80 | expect(table.seats[2].bet).to.be.equal(.3) 81 | }) 82 | 83 | it('the small blind\'s stack decreases by additional the amount raised', () => { 84 | expect(table.callAmount).to.be.equal(.3) 85 | expect(table.seats[2].stack).to.be.equal(4.7) 86 | }) 87 | 88 | it('it is the big blind\'s turn', () => { 89 | expect(table.seats[1].turn).to.be.true 90 | expect(table.turn).to.be.equal(1) 91 | }) 92 | }) 93 | }) -------------------------------------------------------------------------------- /client/app/components/Game/Actions/RaiseSlider.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from 'material-ui/Button' 3 | import AddIcon from 'material-ui-icons/Add' 4 | import Input from 'material-ui/Input/Input' 5 | import { withStyles, createStyleSheet } from 'material-ui/styles' 6 | 7 | const styleSheet = createStyleSheet('RaiseSlider', () => ({ 8 | button: { 9 | width: '36px', 10 | height: '36px', 11 | margin: '0px 10px', 12 | }, 13 | icon: { 14 | position: 'relative', 15 | color: '#fff' 16 | }, 17 | container: { 18 | color: '#333', 19 | display: 'flex', 20 | alignItems: 'center', 21 | margin: '10px 0px', 22 | }, 23 | sliderContainer: { 24 | display: 'flex', 25 | alignItems: 'center', 26 | flex: 4 27 | }, 28 | slider: { 29 | flex: 5 30 | }, 31 | input: { 32 | flex: 1, 33 | height: '65%', 34 | marginRight: '10px', 35 | fontFamily: 'Montserrat, sans-serif', 36 | } 37 | })) 38 | 39 | type Props = { 40 | raiseAmount: number, 41 | decreaseRaiseAmount: () => void, 42 | increaseRaiseAmount: () => void, 43 | onRaiseChange: () => void, 44 | table: { 45 | minRaise: number, 46 | maxBet: number, 47 | }, 48 | seat: { 49 | stack: number, 50 | bet: number, 51 | }, 52 | classes: Object, 53 | } 54 | const RaiseSlider = ({ 55 | raiseAmount, 56 | decreaseRaiseAmount, 57 | increaseRaiseAmount, 58 | onRaiseChange, 59 | table, 60 | seat, 61 | classes 62 | }: Props) => { 63 | const maxBet = seat.stack + seat.bet 64 | const minRaise = table.minRaise > maxBet ? maxBet : table.minRaise 65 | 66 | return ( 67 |
    68 |
    69 | 76 | 77 | ${minRaise.toFixed(2)} 78 | 79 | 89 | 90 | 91 | ${maxBet.toFixed(2)} 92 | 93 | 100 |
    101 | 102 | 106 |
    107 | ) 108 | } 109 | 110 | export default withStyles(styleSheet)(RaiseSlider) -------------------------------------------------------------------------------- /client/app/components/Game/Actions/ActionButtons.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react' 3 | import Button from 'material-ui/Button' 4 | import { withStyles, createStyleSheet } from 'material-ui/styles' 5 | 6 | const styleSheet = createStyleSheet('ActionButtons', () => ({ 7 | container: { 8 | display: 'flex' 9 | }, 10 | button: { 11 | flex: 1, 12 | color: '#fff', 13 | margin: '3px', 14 | height: '80px', 15 | padding: '0px 20px', 16 | fontSize: '16px', 17 | fontFamily: 'Montserrat, sans-serif', 18 | } 19 | })) 20 | 21 | type Seat = { 22 | id: number, 23 | bet: number, 24 | stack: number, 25 | } 26 | type Table = { 27 | id: number, 28 | seats: { 29 | [id: string]: Seat, 30 | }, 31 | pot: number, 32 | callAmount: number, 33 | } 34 | type Props = { 35 | seat: Seat, 36 | table: Table, 37 | raiseAmount: number, 38 | totalCallAmount: number, 39 | handleFoldClick: () => void, 40 | handleCheckClick: () => void, 41 | handleCallClick: () => void, 42 | handleRaiseClick: () => void, 43 | classes: Object, 44 | } 45 | const ActionButtons = ({ 46 | seat, 47 | table, 48 | raiseAmount, 49 | totalCallAmount, 50 | handleFoldClick, 51 | handleCheckClick, 52 | handleCallClick, 53 | handleRaiseClick, 54 | classes 55 | }: Props) => { 56 | const players = Object.keys(table.seats).map(id => table.seats[id]) 57 | const notAllInPlayers = players.filter(tableSeat => 58 | tableSeat && tableSeat.stack > 0 59 | ) 60 | const othersAllIn = notAllInPlayers.length === 1 61 | && notAllInPlayers[0].id === seat.id 62 | 63 | return ( 64 |
    65 | {table.callAmount && 66 | 73 | } 74 | 75 | {((!table.callAmount || seat.bet === table.callAmount) && seat.stack > 0) && 76 | 83 | } 84 | 85 | {(table.callAmount > 0 && seat.bet !== table.callAmount) && 86 | 93 | } 94 | 95 | {((seat.stack > table.callAmount) && !othersAllIn) && 96 | 103 | } 104 |
    105 | ) 106 | } 107 | 108 | export default withStyles(styleSheet)(ActionButtons) -------------------------------------------------------------------------------- /client/app/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOGIN_REQUEST, 3 | LOGIN_SUCCESS, 4 | LOGIN_FAILURE, 5 | SIGNUP_REQUEST, 6 | SIGNUP_SUCCESS, 7 | SIGNUP_FAILURE, 8 | LOGOUT, 9 | TOKEN_LOGIN_REQUEST, 10 | TOKEN_LOGIN_SUCCESS, 11 | TOKEN_LOGIN_FAILURE 12 | } from '../actions/user' 13 | 14 | const initialState = { 15 | isFetching: false, 16 | isAuthenticated: false, 17 | user: null, 18 | token: localStorage.getItem('client') || null, 19 | errorMessage: '' 20 | } 21 | 22 | function user(state = initialState, action) { 23 | switch (action.type) { 24 | case LOGIN_REQUEST: 25 | return { 26 | ...state, 27 | isFetching: true, 28 | isAuthenticated: false, 29 | errorMessage: '' 30 | } 31 | case LOGIN_SUCCESS: 32 | localStorage.setItem('client', action.token) 33 | return { 34 | isFetching: false, 35 | isAuthenticated: true, 36 | user: action.user, 37 | token: action.token, 38 | errorMessage: '' 39 | } 40 | case LOGIN_FAILURE: 41 | return { 42 | isFetching: false, 43 | isAuthenticated: false, 44 | user: null, 45 | token: null, 46 | errorMessage: action.message.response.data.message 47 | } 48 | case SIGNUP_REQUEST: 49 | return { 50 | ...state, 51 | isFetching: true, 52 | isAuthenticated: false, 53 | errorMessage: '' 54 | } 55 | case SIGNUP_SUCCESS: 56 | localStorage.setItem('client', action.token) 57 | return { 58 | isFetching: false, 59 | isAuthenticated: true, 60 | user: action.user, 61 | token: action.token, 62 | errorMessage: '' 63 | } 64 | case SIGNUP_FAILURE: 65 | return { 66 | isFetching: false, 67 | isAuthenticated: false, 68 | user: null, 69 | token: null, 70 | errorMessage: action.message.response.data.message 71 | } 72 | case LOGOUT: 73 | localStorage.removeItem('client') 74 | return { 75 | isFetching: false, 76 | isAuthenticated: false, 77 | user: null, 78 | token: null, 79 | errorMessage: '' 80 | } 81 | case TOKEN_LOGIN_REQUEST: 82 | return { 83 | ...state, 84 | isFetching: true, 85 | isAuthenticated: false, 86 | errorMessage: '' 87 | } 88 | case TOKEN_LOGIN_SUCCESS: 89 | localStorage.setItem('client', action.token) 90 | return { 91 | isFetching: false, 92 | isAuthenticated: true, 93 | user: action.user, 94 | token: action.token, 95 | errorMessage: '' 96 | } 97 | case TOKEN_LOGIN_FAILURE: 98 | localStorage.removeItem('client') 99 | return { 100 | isFetching: false, 101 | isAuthenticated: false, 102 | user: null, 103 | token: null, 104 | errorMessage: action.message.response.data.message 105 | } 106 | default: 107 | return state 108 | } 109 | } 110 | 111 | export default user -------------------------------------------------------------------------------- /client/app/components/Game/ChatAndInfo/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { blueGrey } from 'material-ui/styles/colors' 4 | 5 | import TableControls from '../TableControls'; 6 | import Chat from './Chat'; 7 | import Spectators from './Spectators'; 8 | import SitOutCheckbox from './SitOutCheckbox'; 9 | 10 | const styles = { 11 | container: { 12 | position: 'absolute', 13 | width: 'calc(50vw - 10px)', 14 | height: '184px', 15 | padding: '5px', 16 | right: '0', 17 | bottom: '0', 18 | backgroundColor: blueGrey[100], 19 | border: `1px solid ${blueGrey[100]}` 20 | }, 21 | tabs: { 22 | position: 'absolute', 23 | display: 'flex', 24 | height: '30px', 25 | top: '-30px', 26 | listStyleType: 'none', 27 | margin: '0', 28 | padding: '0', 29 | }, 30 | tab: { 31 | color: '#999', 32 | backgroundColor: blueGrey[100], 33 | padding: '5px 10px', 34 | margin: '0 2px', 35 | borderRadius: '3px 3px 0 0', 36 | }, 37 | activeTab: { 38 | color: '#333', 39 | backgroundColor: blueGrey[100], 40 | fontWeight: '600', 41 | padding: '5px 10px', 42 | margin: '0 2px', 43 | borderRadius: '3px 3px 0 0', 44 | }, 45 | } 46 | 47 | type Props = { 48 | user: Object, 49 | table: Object, 50 | onLeaveClick: () => void, 51 | onStandClick: () => void, 52 | onRotateClockwise: () => void, 53 | onRotateCounterClockwise: () => void, 54 | } 55 | class ChatAndInfo extends React.Component { 56 | constructor() { 57 | super() 58 | 59 | this.state = { 60 | activeTab: 'Chat', 61 | } 62 | } 63 | 64 | setActiveTab = tabName => { 65 | this.setState({ activeTab: tabName }) 66 | } 67 | 68 | render() { 69 | const tabs = ['Chat', 'Players', 'Table Info'] 70 | const { user, table } = this.props; 71 | 72 | return ( 73 |
    74 |
      75 | {tabs.map((tab, index) => ( 76 |
    • this.setActiveTab(tab)} 80 | > 81 | {tab} 82 |
    • 83 | ))} 84 |
    85 | 86 | 92 | 93 | {this.state.activeTab === 'Chat' && 94 | 95 | } 96 | {this.state.activeTab === 'Players' && 97 | 101 | } 102 | {this.state.activeTab === 'Table Info' && 103 |
    104 | {table.name}, ${table.limit.toFixed(2)} NL Holdem, {table.maxPlayers} players 105 |
    106 | } 107 | 108 | 109 |
    110 | ) 111 | } 112 | } 113 | 114 | export default ChatAndInfo -------------------------------------------------------------------------------- /client/app/components/Game/Game.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Seats from './Seats' 3 | import Background from './Pieces/Background' 4 | import Board from './Pieces/Board' 5 | import Actions from './Actions' 6 | import ChatAndInfo from './ChatAndInfo'; 7 | 8 | class Game extends React.Component { 9 | constructor() { 10 | super() 11 | this.state = { 12 | displayOffset: 0 13 | } 14 | this.rotateClockwiseClick = this.rotateClockwiseClick.bind(this) 15 | this.rotateCounterClockwiseClick = this.rotateCounterClockwiseClick.bind(this) 16 | } 17 | 18 | rotateClockwiseClick() { 19 | let currentOffset = this.state.displayOffset 20 | let maxOffset = this.props.table.maxPlayers - 1 21 | let newOffset = currentOffset === maxOffset ? 0 : currentOffset + 1 22 | this.setState({ displayOffset: newOffset }) 23 | } 24 | 25 | rotateCounterClockwiseClick() { 26 | let currentOffset = this.state.displayOffset 27 | let maxOffset = this.props.table.maxPlayers - 1 28 | let newOffset = currentOffset === 0 ? maxOffset : currentOffset - 1 29 | this.setState({ displayOffset: newOffset }) 30 | } 31 | 32 | isOwnTurn() { 33 | const { user, table } = this.props 34 | for (let seat of Object.values(table.seats)) { 35 | if (seat && seat.turn && seat.player.name === user.username) { 36 | return true 37 | } 38 | } 39 | return false 40 | } 41 | 42 | render() { 43 | const { 44 | classes, 45 | user, 46 | table, 47 | messages, 48 | onLeaveClick, 49 | onSeatClick, 50 | onStandClick, 51 | onRaiseClick, 52 | onCheckClick, 53 | onCallClick, 54 | onFoldClick, 55 | onTableMessage, 56 | gridViewOn, 57 | socket 58 | } = this.props 59 | 60 | const gameClass = gridViewOn ? 'poker-game poker-game-small' : 'poker-game' 61 | const seat = Object.values(table.seats).find(seat => 62 | seat && seat.player.socketId === socket.id 63 | ) 64 | 65 | return ( 66 |
    67 |
    68 | 69 | 70 | 76 |
    77 | 78 | {this.isOwnTurn() && ( 79 | 87 | )} 88 | onTableMessage(e, table.id)} 95 | onLeaveClick={() => onLeaveClick(table.id)} 96 | onStandClick={() => onStandClick(table.id)} 97 | onRotateClockwise={this.rotateClockwiseClick} 98 | onRotateCounterClockwise={this.rotateCounterClockwiseClick} 99 | /> 100 |
    101 | ) 102 | } 103 | } 104 | 105 | export default Game -------------------------------------------------------------------------------- /client/app/components/lobby/MainMenu/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { css } from 'emotion' 4 | 5 | import TableList from './TableList' 6 | import PlayerList from './PlayerList' 7 | 8 | const styles = { 9 | tab: css` 10 | font-size: 20px; 11 | margin-right: 30px; 12 | transition: 100ms ease; 13 | 14 | &:hover { 15 | cursor: pointer; 16 | color: #2196f3; 17 | border-bottom: 2px solid #2196f3; 18 | } 19 | `, 20 | activeTab: css` 21 | font-size: 20px; 22 | margin-right: 30px; 23 | color: #2196f3; 24 | border-bottom: 2px solid #2196f3; 25 | font-weight: 600; 26 | `, 27 | container: css` 28 | max-width: 525px; 29 | margin: 200px auto 0; 30 | `, 31 | } 32 | 33 | type Props = { 34 | user: { 35 | id: number, 36 | username: string, 37 | bankroll: number, 38 | }, 39 | logout: () => void, 40 | openTables: {}, 41 | tables: {}, 42 | handleTableClick: (tableId: number) => void, 43 | players: { 44 | [socketId: string]: ?{ 45 | id: number, 46 | name: string, 47 | bankroll: number, 48 | }, 49 | }, 50 | } 51 | type State = { 52 | activeTab: 'games' | 'players', 53 | } 54 | class MainMenu extends React.Component { 55 | state = { 56 | activeTab: 'games' 57 | } 58 | 59 | renderTabs() { 60 | const { activeTab } = this.state 61 | return ( 62 |
    68 |
    69 | this.setState({ activeTab: 'games' })}> 72 | Games 73 | 74 | this.setState({ activeTab: 'players' })}> 77 | Players 78 | 79 |
    80 |
    81 | ) 82 | } 83 | 84 | render() { 85 | const { 86 | user, 87 | openTables, 88 | tables, 89 | handleTableClick, 90 | players, 91 | } = this.props 92 | 93 | if (!user) return null; 94 | const hasTableOpen = Object.keys(openTables).length > 0 95 | const userPlayer = Object.values(players).find(player => 96 | player && player.id && player.id === user.id 97 | ) 98 | if (!userPlayer) return null; 99 | 100 | return ( 101 |
    102 |
    103 | {false && this.renderTabs()} 104 | {this.state.activeTab === 'games' && 105 | 111 | } 112 | {this.state.activeTab === 'players' && 113 | 117 | } 118 |
    119 |
    120 | ) 121 | } 122 | } 123 | 124 | export default MainMenu; -------------------------------------------------------------------------------- /client/app/actions/groups.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded' 3 | 4 | export const REQUEST_GROUPS = 'REQUEST_GROUPS' 5 | export const REQUEST_GROUPS_SUCCESS = 'REQUEST_GROUPS_SUCCESS' 6 | export const REQUEST_GROUPS_FAILURE = 'REQUEST_GROUPS_FAILURE' 7 | 8 | export const CREATE_GROUP_REQUEST = 'CREATE_GROUP_REQUEST' 9 | export const CREATE_GROUP_SUCCESS = 'CREATE_GROUP_SUCCESS' 10 | export const CREATE_GROUP_FAILURE = 'CREATE_GROUP_FAILURE' 11 | 12 | export const DELETE_GROUP_REQUEST = 'DELETE_GROUP_REQUEST' 13 | export const DELETE_GROUP_SUCCESS = 'DELETE_GROUP_SUCCESS' 14 | export const DELETE_GROUP_FAILURE = 'DELETE_GROUP_FAILURE' 15 | 16 | const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:9000/api' : '/api' 17 | 18 | export function requestGroups() { 19 | return { 20 | type: REQUEST_GROUPS, 21 | } 22 | } 23 | 24 | export function requestGroupsSuccess(groups) { 25 | return { 26 | type: REQUEST_GROUPS_SUCCESS, 27 | groups, 28 | } 29 | } 30 | 31 | export function requestGroupsFailure(message) { 32 | return { 33 | type: REQUEST_GROUPS_FAILURE, 34 | message, 35 | } 36 | } 37 | 38 | export function fetchGroups(token) { 39 | return function (dispatch) { 40 | dispatch(requestGroups()) 41 | 42 | return axios.get(`${ROOT_URL}/groups?access_token=${token}`) 43 | .then(res => { 44 | const { groups } = res.data 45 | dispatch(requestGroupsSuccess(groups)) 46 | }) 47 | .catch(err => { 48 | console.log(err) 49 | dispatch(requestGroupsFailure(err)) 50 | }) 51 | } 52 | } 53 | 54 | export function createGroupRequest() { 55 | return { 56 | type: CREATE_GROUP_REQUEST, 57 | } 58 | } 59 | 60 | export function createGroupSuccess(group) { 61 | return { 62 | type: CREATE_GROUP_SUCCESS, 63 | group, 64 | } 65 | } 66 | 67 | export function createGroupFailure(message) { 68 | return { 69 | type: CREATE_GROUP_FAILURE, 70 | message, 71 | } 72 | } 73 | 74 | export function createGroup(accessToken, groupAttrs) { 75 | return function (dispatch) { 76 | dispatch(createGroupRequest()) 77 | 78 | return axios.post(`${ROOT_URL}/groups`, { accessToken, groupAttrs }) 79 | .then(res => { 80 | const { group } = res.data 81 | dispatch(createGroupSuccess(group)) 82 | }) 83 | .catch(err => { 84 | console.log(err) 85 | dispatch(createGroupFailure(err)) 86 | }) 87 | } 88 | } 89 | 90 | export function deleteGroupRequest() { 91 | return { 92 | type: DELETE_GROUP_REQUEST, 93 | } 94 | } 95 | 96 | export function deleteGroupSuccess(deletedGroupId) { 97 | return { 98 | type: DELETE_GROUP_SUCCESS, 99 | deletedGroupId, 100 | } 101 | } 102 | 103 | export function deleteGroupFailure(message) { 104 | return { 105 | type: DELETE_GROUP_FAILURE, 106 | message, 107 | } 108 | } 109 | 110 | export function deleteGroup(accessToken, groupId) { 111 | return function (dispatch) { 112 | dispatch(deleteGroupRequest()) 113 | 114 | return axios.delete(`${ROOT_URL}/groups/${groupId}?accessToken=${accessToken}`) 115 | .then(res => { 116 | const { deletedGroupId } = res.data 117 | dispatch(deleteGroupSuccess(deletedGroupId)) 118 | }) 119 | .catch(err => { 120 | console.log(err) 121 | dispatch(deleteGroupFailure(err)) 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /client/app/components/Game/BuyinModal.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react'; 3 | import { Button } from 'app/components'; 4 | import Dialog from 'material-ui/Dialog'; 5 | import Input from 'material-ui/Input'; 6 | 7 | const styles = { 8 | modal: { 9 | padding: '24px', 10 | }, 11 | inputContainer: { 12 | display: 'flex', 13 | justifyContent: 'space-between', 14 | alignItems: 'center', 15 | marginBottom: '32px', 16 | fontFamily: 'Montserrat, sans-serif', 17 | }, 18 | buttons: { 19 | textAlign: 'right', 20 | }, 21 | error: { 22 | color: 'red', 23 | marginTop: '24px', 24 | fontSize: '14px', 25 | } 26 | } 27 | 28 | type Props = { 29 | open: boolean, 30 | tableId: ?number, 31 | seatId: ?number, 32 | table: ?{ 33 | limit: number, 34 | seats: { 35 | [key: number]: ?{}, 36 | }, 37 | }, 38 | seat: ?{ 39 | stack: number, 40 | player: {}, 41 | }, 42 | closeModal: () => void, 43 | buyInAndSitDown: (tableId: number, seatId: number, amount: number) => void, 44 | handleRebuy: (tableId: number, seatId: number, amount: number) => void, 45 | } 46 | type State = { 47 | errorMessage: ?string, 48 | } 49 | class BuyinModal extends React.Component { 50 | buyinAmount: { 51 | value: null 52 | }; 53 | state = { 54 | errorMessage: null, 55 | } 56 | 57 | handleClose = () => { 58 | this.setState({ errorMessage: null }) 59 | this.props.closeModal(); 60 | } 61 | 62 | handleBuyin = () => { 63 | const { tableId, seatId, table } = this.props 64 | if (!tableId || !seatId || !table) return null; 65 | 66 | const amount = parseFloat(this.buyinAmount.value) 67 | const minBuy = table.limit / 2 68 | const maxBuy = table.limit 69 | 70 | if (amount < minBuy || amount > maxBuy) { 71 | this.setState({ errorMessage: `Please enter an amount between ${minBuy} and ${maxBuy}` }) 72 | return 73 | }; 74 | 75 | const alreadySeated = table.seats[seatId] 76 | if (alreadySeated) { 77 | this.props.handleRebuy(tableId, seatId, amount) 78 | } else { 79 | this.props.buyInAndSitDown(tableId, seatId, amount) 80 | } 81 | 82 | this.props.closeModal() 83 | this.setState({ errorMessage: null }) 84 | } 85 | 86 | render() { 87 | const { open, table, seat } = this.props 88 | if (!table) return null 89 | const mustBuyIn = table.handOver && seat && seat.stack == 0 90 | 91 | return ( 92 | 93 |
    94 |

    How much will you buy in for?

    95 |
    96 | Buy in amount ($): 97 | { this.buyinAmount = ref }} 99 | type="number" 100 | defaultValue={table.limit} 101 | style={{ width: '100px', fontFamily: 'Montserrat, sans-serif' }} 102 | /> 103 |
    104 | 105 |
    106 | {!mustBuyIn && 107 | 113 | } 114 | 117 |
    118 | 119 | {this.state.errorMessage && 120 |
    {this.state.errorMessage}
    121 | } 122 |
    123 |
    124 | ); 125 | } 126 | } 127 | 128 | export default BuyinModal -------------------------------------------------------------------------------- /client/scss/small_table.scss: -------------------------------------------------------------------------------- 1 | .poker-game.poker-game-small { 2 | width: 50%; 3 | height: 50vh; 4 | font-size: 12px; 5 | float: left; 6 | box-sizing: border-box; 7 | outline: 1px solid #ccc; 8 | 9 | .seat1of6 { 10 | left: calc(50% - 50px); 11 | top: 5%; 12 | 13 | .bet { 14 | left: 85px; 15 | bottom: -40px; 16 | } 17 | } 18 | .seat2of6 { 19 | left: calc(94% - 100px); 20 | 21 | .bet { 22 | bottom: -20px; 23 | left: -85px; 24 | } 25 | } 26 | .seat3of6 { 27 | top: calc(63% - 80px); 28 | left: calc(94% - 100px); 29 | 30 | .bet { 31 | left: -85px; 32 | } 33 | } 34 | .seat4of6 { 35 | top: calc(76% - 80px); 36 | left: calc(50% - 50px); 37 | 38 | .bet { 39 | left: -85px; 40 | top: -15px; 41 | } 42 | } 43 | .seat5of6 { 44 | top: calc(63% - 80px); 45 | 46 | .bet { 47 | right: -85px; 48 | } 49 | } 50 | .seat6of6 { 51 | .bet { 52 | bottom: -20px; 53 | right: -85px; 54 | } 55 | } 56 | .seat-container { 57 | width: 100px; 58 | height: 80px; 59 | 60 | .seat-stack { 61 | font-size: 2em; 62 | padding: 0px; 63 | } 64 | .hand { 65 | bottom: 96px; 66 | 67 | .card:first-child { 68 | left: 21px; 69 | } 70 | .card:last-child { 71 | right: 21px; 72 | } 73 | } 74 | .hand:hover { 75 | .card:first-child { 76 | top: -25px; 77 | left: 7px; 78 | } 79 | .card:last-child { 80 | top: -25px; 81 | right: 7px; 82 | } 83 | } 84 | .seat-info > div:nth-child(2) { 85 | font-size: 0.9em; 86 | } 87 | .seat-last-action { 88 | font-size: 0.8em; 89 | bottom: -9px; 90 | } 91 | .button-chip { 92 | width: 15px; 93 | height: 15px; 94 | top: 25px; 95 | right: -20px; 96 | } 97 | .bet span { 98 | font-size: 0.9em; 99 | } 100 | } 101 | .card { 102 | width: 40px; 103 | height: 56px; 104 | margin: 2px; 105 | border-radius: 2px; 106 | 107 | .card-back { 108 | width: 36px; 109 | height: 52px; 110 | margin: 2px; 111 | } 112 | .small-picture { 113 | padding-left: 2px; 114 | 115 | div:last-child { 116 | margin-top: -10px; 117 | } 118 | } 119 | .big-picture { 120 | font-size: 3.5em; 121 | padding-right: 2px; 122 | } 123 | } 124 | .board { 125 | top: 30%; 126 | 127 | .card { 128 | margin: 5px 2px; 129 | } 130 | .chip-pile { 131 | top: -5%; 132 | left: 33%; 133 | } 134 | .card-silhouette { 135 | width: 40px; 136 | height: 56px; 137 | margin: 5px 2px; 138 | border-radius: 2px; 139 | } 140 | } 141 | .table-bg { 142 | .bg-outer { 143 | border-width: 12px; 144 | 145 | .bg-inner { 146 | width: calc(100% - 6px); 147 | height: calc(100% - 6px); 148 | border-width: 3px; 149 | } 150 | } 151 | } 152 | .chips { 153 | width: 17px; 154 | 155 | > div { 156 | height: 3px; 157 | width: 15px; 158 | } 159 | } 160 | .game-chat { 161 | font-size: 1em; 162 | 163 | .messages { 164 | height: 6.5vh; 165 | margin: 5px 5px 0px 5px; 166 | } 167 | .chat-input { 168 | padding: 3px 27px 3px 5px; 169 | } 170 | } 171 | .actions-container { 172 | .actions { 173 | margin: 5px 2px 3px 2px; 174 | 175 | button { 176 | height: 25px; 177 | } 178 | .raise-slider-container { 179 | padding: 5px 3px; 180 | font-size: 1em; 181 | 182 | .raise-slider { 183 | input { 184 | width: 50%; 185 | } 186 | span { 187 | top: -6px; 188 | } 189 | button { 190 | font-size: 0.75em; 191 | width: 20px; 192 | height: 20px; 193 | margin-top: 3px; 194 | } 195 | } 196 | } 197 | .raise-size-buttons { 198 | button { 199 | height: 20px; 200 | font-size: 0.9em; 201 | } 202 | } 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /client/app/actions/user.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded' 3 | import { hashHistory } from 'react-router' 4 | import { push } from 'react-router-redux' 5 | 6 | export const LOGIN_REQUEST = 'LOGIN_REQUEST' 7 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' 8 | export const LOGIN_FAILURE = 'LOGIN_FAILURE' 9 | 10 | export const SIGNUP_REQUEST = 'SIGNUP_REQUEST' 11 | export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS' 12 | export const SIGNUP_FAILURE = 'SIGNUP_FAILURE' 13 | 14 | export const LOGOUT = 'LOGOUT' 15 | 16 | export const TOKEN_LOGIN_REQUEST = 'TOKEN_LOGIN_REQUEST' 17 | export const TOKEN_LOGIN_SUCCESS = 'TOKEN_LOGIN_SUCCESS' 18 | export const TOKEN_LOGIN_FAILURE = 'TOKEN_LOGIN_FAILURE' 19 | 20 | const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:9000/api' : '/api' 21 | 22 | // login 23 | export function requestLogin(params) { 24 | return { 25 | type: LOGIN_REQUEST, 26 | params 27 | } 28 | } 29 | 30 | export function loginSuccess(user, token) { 31 | return { 32 | type: LOGIN_SUCCESS, 33 | user, 34 | token 35 | } 36 | } 37 | 38 | export function loginFailure(message) { 39 | return { 40 | type: LOGIN_FAILURE, 41 | message 42 | } 43 | } 44 | 45 | export function login(params) { 46 | return function(dispatch) { 47 | dispatch(requestLogin(params)) 48 | 49 | return axios.post(`${ROOT_URL}/login`, params) 50 | .then(res => { 51 | const user = res.data.user 52 | const token = res.data.token 53 | 54 | if (user) { 55 | dispatch(loginSuccess(user, token)) 56 | dispatch(push('/lobby')) 57 | } else { 58 | console.log(res) 59 | } 60 | }) 61 | .catch(err => { 62 | console.log(err) 63 | dispatch(loginFailure(err)) 64 | }) 65 | } 66 | } 67 | 68 | // signup 69 | export function requestSignUp(params) { 70 | return { 71 | type: SIGNUP_REQUEST, 72 | params 73 | } 74 | } 75 | 76 | export function signUpSuccess(user, token) { 77 | return { 78 | type: SIGNUP_SUCCESS, 79 | user, 80 | token 81 | } 82 | } 83 | 84 | export function signUpFailure(message) { 85 | return { 86 | type: SIGNUP_FAILURE, 87 | message 88 | } 89 | } 90 | 91 | export function signUp(params) { 92 | return function(dispatch) { 93 | dispatch(requestSignUp(params)) 94 | 95 | return axios.post(`${ROOT_URL}/signup`, params) 96 | .then(res => { 97 | const user = res.data.user 98 | const token = res.data.token 99 | 100 | if (user) { 101 | dispatch(signUpSuccess(user, token)) 102 | dispatch(push('/lobby')) 103 | } else { 104 | console.log(res) 105 | } 106 | }) 107 | .catch(err => { 108 | console.log(err) 109 | dispatch(signUpFailure(err)) 110 | }) 111 | } 112 | } 113 | 114 | // logout 115 | export function logout() { 116 | return function(dispatch) { 117 | dispatch({ type: LOGOUT }) 118 | dispatch(push('/login')) 119 | } 120 | } 121 | 122 | // jwt token verification 123 | export function requestTokenLogin() { 124 | return { 125 | type: TOKEN_LOGIN_REQUEST 126 | } 127 | } 128 | 129 | export function tokenLoginSuccess(user, token) { 130 | return { 131 | type: TOKEN_LOGIN_SUCCESS, 132 | user, 133 | token 134 | } 135 | } 136 | 137 | export function tokenLoginFailure(message) { 138 | return { 139 | type: TOKEN_LOGIN_FAILURE, 140 | message 141 | } 142 | } 143 | 144 | export function tokenLogin(token) { 145 | return function(dispatch) { 146 | dispatch(requestTokenLogin()) 147 | 148 | return axios.post(`${ROOT_URL}/verify_jwt`, { token }) 149 | .then(res => { 150 | const user = res.data.user 151 | const token = res.data.token 152 | 153 | if (user) { 154 | dispatch(tokenLoginSuccess(user, token)) 155 | if (hashHistory.getCurrentLocation().pathname !== '/lobby') { 156 | dispatch(push('/lobby')) 157 | } 158 | } else { 159 | console.log(res) 160 | } 161 | }) 162 | .catch(err => { 163 | console.log(err) 164 | dispatch(tokenLoginFailure(err)) 165 | }) 166 | } 167 | } -------------------------------------------------------------------------------- /client/app/components/Game/Actions/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import RaiseSlider from './RaiseSlider' 3 | import PotSizeButton from './PotSizeButton' 4 | import ActionButtons from './ActionButtons' 5 | 6 | import { blueGrey } from 'material-ui/styles/colors' 7 | 8 | const styles = { 9 | container: { 10 | position: 'absolute', 11 | width: 'calc(50vw - 10px)', 12 | height: '184px', 13 | padding: '5px', 14 | left: '0', 15 | bottom: '0', 16 | backgroundColor: blueGrey[100], 17 | border: `1px solid ${blueGrey[100]}` 18 | }, 19 | } 20 | 21 | class Actions extends React.Component { 22 | constructor(props) { 23 | super(props) 24 | 25 | const { user, table } = this.props 26 | const seat = Object.values(table.seats).filter(seat => 27 | seat !== null && seat.player.name === user.username 28 | )[0] 29 | 30 | const raiseAmount = table.minRaise > seat.stack + seat.bet ? 31 | seat.stack + seat.bet : table.minRaise 32 | 33 | this.state = { 34 | raiseAmount: raiseAmount 35 | } 36 | 37 | this.handleRaiseChange = this.handleRaiseChange.bind(this) 38 | } 39 | 40 | handleRaiseChange(e) { 41 | const { table } = this.props 42 | if (e.target.value < table.minRaise || e.target.value > table.maxBet) { 43 | return 44 | } 45 | this.setState({ raiseAmount: e.target.value }) 46 | } 47 | 48 | handleRaiseUpdate(amount) { 49 | const { table } = this.props 50 | const seat = this.findOwnSeat() 51 | let newRaiseAmount = parseFloat(amount) 52 | 53 | if (amount < table.minRaise) { 54 | newRaiseAmount = table.minRaise 55 | } else if (amount > seat.stack + seat.bet) { 56 | newRaiseAmount = seat.stack + seat.bet 57 | } 58 | 59 | this.setState({ raiseAmount: newRaiseAmount }) 60 | } 61 | 62 | findOwnSeat() { 63 | const { user, table } = this.props 64 | const seat = Object.values(table.seats).filter(seat => 65 | seat !== null && seat.player.name === user.username 66 | )[0] 67 | 68 | return seat 69 | } 70 | 71 | render() { 72 | const { 73 | table, 74 | onRaiseClick, 75 | onCheckClick, 76 | onCallClick, 77 | onFoldClick 78 | } = this.props 79 | 80 | const seat = this.findOwnSeat() 81 | if (seat.sittingOut || table.handOver) return null; 82 | 83 | const totalCallAmount = table.callAmount - seat.bet > seat.stack ? seat.stack : table.callAmount - seat.bet 84 | 85 | let pot = table.pot 86 | if (table.callAmount) { 87 | pot = table.pot * 2 + totalCallAmount 88 | } 89 | 90 | const potSizes = [ 91 | ['Min', table.minRaise], 92 | ['½ pot', pot * 1/2], 93 | ['⅔ pot', pot * 2/3], 94 | ['¾ pot', pot * 3/4], 95 | ['Pot', pot], 96 | ['All in', seat.bet + seat.stack] 97 | ] 98 | 99 | return ( 100 |
    101 | onFoldClick(table.id)} 107 | handleCheckClick={() => onCheckClick(table.id)} 108 | handleCallClick={() => onCallClick(table.id)} 109 | handleRaiseClick={() => onRaiseClick(table.id, parseFloat(this.state.raiseAmount))} 110 | /> 111 | 112 | {seat.stack > table.callAmount && 113 | this.handleRaiseUpdate(this.state.raiseAmount - table.minBet)} 116 | increaseRaiseAmount={() => this.handleRaiseUpdate(this.state.raiseAmount + table.minBet)} 117 | onRaiseChange={this.handleRaiseChange} 118 | table={table} 119 | seat={seat} 120 | /> 121 | } 122 | 123 | {seat.stack > table.callAmount && 124 |
    125 | {potSizes.map(potSize => 126 | this.handleRaiseUpdate(potSize[1])} 130 | /> 131 | )} 132 |
    133 | } 134 |
    135 | ) 136 | } 137 | } 138 | 139 | export default Actions -------------------------------------------------------------------------------- /client/app/components/HandHistory/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { connect } from 'react-redux' 4 | import { css } from 'emotion' 5 | 6 | import { fetchHandHistory } from '../../actions/hands' 7 | import Icon from 'material-ui/Icon' 8 | import Hand from './Hand' 9 | import { Panel, Button } from 'app/components'; 10 | import theme from 'app/utils/theme'; 11 | 12 | const container = css` 13 | height: calc(100% - 170px); 14 | padding: 80px 20px; 15 | `; 16 | const link = css` 17 | display: flex; 18 | justify-content: space-between; 19 | margin-bottom: 10px; 20 | &:hover { 21 | cursor: pointer; 22 | color: ${theme.colors.blue}; 23 | } 24 | `; 25 | const handList = css` 26 | position: relative; 27 | width: 450px; 28 | height: 100%; 29 | ` 30 | const pagination = css` 31 | width: calc(100% - 48px); 32 | position: absolute; 33 | bottom: 16px; 34 | font-size: 12px; 35 | display: flex; 36 | justify-content: space-between; 37 | align-items: center; 38 | `; 39 | const paginationButton = { 40 | padding: '2px 4px', 41 | } 42 | 43 | type Props = { 44 | hands: Array, 45 | pages: number, 46 | count: number, 47 | fetchHandHistory: (token: ?string, page: number) => void, 48 | isFetching: boolean, 49 | errorMessage: ?string, 50 | token: ?string, 51 | } 52 | type State = { 53 | hand: ?Object, 54 | page: number, 55 | } 56 | class HandHistory extends React.Component { 57 | constructor(props) { 58 | super(props) 59 | 60 | this.state = { 61 | hand: null, 62 | page: 1, 63 | } 64 | } 65 | 66 | componentDidMount() { 67 | this.props.fetchHandHistory(this.props.token, 1) 68 | } 69 | 70 | previousPage = () => { 71 | if (this.state.page === 1) return 72 | const prevPage = this.state.page - 1 73 | this.setState({ page: prevPage }) 74 | this.props.fetchHandHistory(this.props.token, prevPage) 75 | } 76 | 77 | nextPage = () => { 78 | if (this.state.page === this.props.pages) return 79 | const nextPage = this.state.page + 1 80 | this.setState({ page: nextPage }) 81 | this.props.fetchHandHistory(this.props.token, nextPage) 82 | } 83 | 84 | handleBackClick = () => { 85 | this.setState({ hand: null }) 86 | } 87 | 88 | render() { 89 | const { hands, pages } = this.props; 90 | 91 | if (this.state.hand) { 92 | return ( 93 |
    94 | 95 |
    96 | ) 97 | } 98 | 99 | return ( 100 |
    101 | 102 |
    103 | {hands.length > 0 && hands.map(hand => ( 104 |
    this.setState({ hand })}> 105 | Hand #{hand.id} 106 | {new Date(hand.createdAt).toUTCString()} 107 |
    108 | ))} 109 |
    110 |
    111 | 119 |
    120 | Page: {this.state.page} / {this.props.pages}{' '} 121 | ({this.props.count} hands) 122 |
    123 | 131 |
    132 |
    133 |
    134 | ) 135 | } 136 | } 137 | 138 | function mapStateToProps(state) { 139 | return { 140 | hands: state.hands.hands, 141 | pages: state.hands.pages, 142 | count: state.hands.count, 143 | isFetching: state.hands.isFetching, 144 | errorMessage: state.hands.errorMessage, 145 | token: state.user.token, 146 | } 147 | } 148 | 149 | const mapDispatchToProps = ({ 150 | fetchHandHistory 151 | }) 152 | 153 | export default connect( 154 | mapStateToProps, 155 | mapDispatchToProps 156 | )(HandHistory) 157 | -------------------------------------------------------------------------------- /client/app/components/Groups/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { connect } from 'react-redux' 4 | import { css } from 'emotion' 5 | 6 | import { fetchGroups, createGroup, deleteGroup } from '../../actions/groups' 7 | 8 | import { Panel, Text, Button } from 'app/components'; 9 | import Group from './Group'; 10 | import CreateGroupModal from './CreateGroupModal'; 11 | 12 | const container = css` 13 | display: flex; 14 | height: calc(100% - 168px); 15 | padding: 80px 20px; 16 | `; 17 | const leftColumn = css` 18 | width: 450px; 19 | margin-right: 20px; 20 | `; 21 | const groupList = css` 22 | position: relative; 23 | height: calc(50% - 12px); 24 | margin-bottom: 20px; 25 | `; 26 | const inviteList = css` 27 | position: relative; 28 | height: calc(50% - 12px); 29 | `; 30 | const groupName = css` 31 | display: flex; 32 | align-items: center; 33 | justify-content: space-between; 34 | margin-bottom: 8px; 35 | `; 36 | const button = { 37 | position: 'absolute', 38 | bottom: '16px', 39 | right: '24px', 40 | }; 41 | 42 | type GroupType = { 43 | id: number, 44 | name: string, 45 | GroupMembers: Array, 46 | } 47 | type Props = { 48 | groups: Array, 49 | isFetching: boolean, 50 | errorMessage: ?string, 51 | token: ?string, 52 | fetchGroups: (token: ?string) => void, 53 | createGroup: (token: ?string, groupAttrs: any) => void, 54 | deleteGroup: (token: ?string, groupId: number) => void, 55 | } 56 | type State = { 57 | activeGroup: ?GroupType, 58 | modalOpen: boolean, 59 | } 60 | class Groups extends React.Component { 61 | constructor(props) { 62 | super(props) 63 | 64 | this.state ={ 65 | activeGroup: null, 66 | modalOpen: false, 67 | } 68 | } 69 | 70 | componentDidMount() { 71 | this.props.fetchGroups(this.props.token) 72 | } 73 | 74 | componentWillReceiveProps(nextProps) { 75 | if (this.state.activeGroup && !nextProps.groups.find(group => 76 | this.state.activeGroup && group.id === this.state.activeGroup.id 77 | )) { 78 | this.setState({ activeGroup: null }) 79 | } 80 | } 81 | 82 | handleGroupClick = group => (e: SyntheticEvent<>) => { 83 | e.preventDefault(); 84 | this.setState({ activeGroup: group }) 85 | } 86 | 87 | toggleModal = () => { 88 | this.setState({ modalOpen: !this.state.modalOpen }) 89 | } 90 | 91 | createGroup = groupAttrs => { 92 | this.props.createGroup(this.props.token, groupAttrs) 93 | this.toggleModal(); 94 | } 95 | 96 | deleteGroup = groupId => { 97 | this.props.deleteGroup(this.props.token, groupId); 98 | } 99 | 100 | render() { 101 | const { activeGroup } = this.state; 102 | 103 | return ( 104 |
    105 |
    106 | 107 | {this.props.groups.map(group => { 108 | const members = group.GroupMembers.length; 109 | const isActive = activeGroup && group.id === activeGroup.id; 110 | return ( 111 |
    112 | {group.name} 113 | ({members} {members === 1 ? 'member' : 'members'}) 114 |
    115 | ); 116 | })} 117 | 120 |
    121 | 122 | 123 |
    124 | {activeGroup && ( 125 | 126 | )} 127 | 132 |
    133 | ) 134 | } 135 | } 136 | 137 | function mapStateToProps(state) { 138 | return { 139 | groups: state.groups.groups, 140 | isFetching: state.groups.isFetching, 141 | isCreating: state.groups.isCreating, 142 | errorMessage: state.groups.errorMessage, 143 | token: state.user.token, 144 | } 145 | } 146 | 147 | const mapDispatchToProps = ({ 148 | fetchGroups, 149 | createGroup, 150 | deleteGroup, 151 | }) 152 | 153 | export default connect( 154 | mapStateToProps, 155 | mapDispatchToProps 156 | )(Groups) 157 | -------------------------------------------------------------------------------- /server/poker/__test__/seat.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | var should = require('chai').should() 3 | var Seat = require('../seat') 4 | 5 | describe('Seat', function() { 6 | const id = '123456' 7 | const player = { name: 'Byron' } 8 | 9 | describe('initialization', () => { 10 | const seat = new Seat(id, player, 100, 100) 11 | 12 | it('has correct id', () => { 13 | expect(seat.id).to.be.equal('123456') 14 | }) 15 | it('has correct player', () => { 16 | expect(seat.player).to.have.property('name', 'Byron') 17 | }) 18 | it('has correct stack', () => { 19 | expect(seat.stack).to.be.equal(100) 20 | }) 21 | it('has correct buyin', () => { 22 | expect(seat.buyin).to.be.equal(100) 23 | }) 24 | }) 25 | 26 | describe('#fold()', () => { 27 | const seat = new Seat(id, player, 100, 100) 28 | seat.fold() 29 | 30 | it(`sets seat's bet to 0`, () => { 31 | expect(seat.bet).to.be.equal(0) 32 | }) 33 | it(`sets seat's folded flag to true`, () => { 34 | expect(seat.folded).to.be.true 35 | }) 36 | it(`changes seat's last action to 'FOLD'`, () => { 37 | expect(seat.lastAction).to.be.equal('FOLD') 38 | }) 39 | it(`sets seat's turn to false`, () => { 40 | expect(seat.turn).to.be.false 41 | }) 42 | }) 43 | 44 | describe('#check()', () => { 45 | const seat = new Seat(id, player, 100, 100) 46 | seat.check() 47 | 48 | it(`sets seat's checked flag to true`, () => { 49 | expect(seat.checked).to.be.true 50 | }) 51 | it(`changes seat's last action to 'CHECK'`, () => { 52 | expect(seat.lastAction).to.be.equal('CHECK') 53 | }) 54 | it(`sets seat's turn to false`, () => { 55 | expect(seat.turn).to.be.false 56 | }) 57 | }) 58 | 59 | describe('#raise(amount)', () => { 60 | const seat = new Seat(id, player, 100, 100) 61 | seat.raise(20) 62 | 63 | it(`subtracts from the stack`, () => { 64 | expect(seat.stack).to.be.equal(80) 65 | }) 66 | it(`sets seat's bet to the amount raised`, () => { 67 | expect(seat.bet).to.be.equal(20) 68 | }) 69 | it(`changes seat's last action to 'RAISE'`, () => { 70 | expect(seat.lastAction).to.be.equal('RAISE') 71 | }) 72 | it(`sets seat's turn to false`, () => { 73 | expect(seat.turn).to.be.false 74 | }) 75 | 76 | it(`cannot raise more than it's stack`, () => { 77 | seat.raise(110) 78 | expect(seat.bet).to.be.equal(20) 79 | }) 80 | }) 81 | 82 | describe('#placeBlind(amount)', () => { 83 | const seat = new Seat(id, player, 100, 100) 84 | seat.placeBlind(5) 85 | 86 | it(`subtracts from the stack`, () => { 87 | expect(seat.stack).to.be.equal(95) 88 | }) 89 | it(`does NOT change seat's last action`, () => { 90 | expect(seat.lastAction).to.be.equal(null) 91 | }) 92 | it(`sets seat's bet to the amount raised`, () => { 93 | expect(seat.bet).to.be.equal(5) 94 | }) 95 | }) 96 | 97 | describe('#callRaise(amount) [amount <= stack]', () => { 98 | const seat = new Seat(id, player, 100, 100) 99 | seat.callRaise(50) 100 | 101 | it(`subtracts from the stack`, () => { 102 | expect(seat.stack).to.be.equal(50) 103 | }) 104 | it(`sets seat's bet to the amount raised`, () => { 105 | expect(seat.bet).to.be.equal(50) 106 | }) 107 | it(`changes seat's last action to 'CALL'`, () => { 108 | expect(seat.lastAction).to.be.equal('CALL') 109 | }) 110 | it(`sets seat's turn to false`, () => { 111 | expect(seat.turn).to.be.false 112 | }) 113 | }) 114 | 115 | describe(`#callRaise(amount) [amount > stack]`, () => { 116 | const seat = new Seat(id, player, 100, 100) 117 | seat.callRaise(200) 118 | 119 | it(`sets seat's stack to 0 - all in`, () => { 120 | expect(seat.stack).to.be.equal(0) 121 | }) 122 | it(`sets seat's bet to remaining stack`, () => { 123 | expect(seat.bet).to.be.equal(100) 124 | }) 125 | }) 126 | 127 | describe('#winHand(amount)', () => { 128 | const seat = new Seat(id, player, 100, 100) 129 | seat.winHand(125) 130 | 131 | it(`adds won amount to stack`, () => { 132 | expect(seat.stack).to.be.equal(225) 133 | }) 134 | it(`sets seat's bet to 0`, () => { 135 | expect(seat.bet).to.be.equal(0) 136 | }) 137 | it(`changes seat's last action to 'WINNER'`, () => { 138 | expect(seat.lastAction).to.be.equal('WINNER') 139 | }) 140 | it(`sets seat's turn to false`, () => { 141 | expect(seat.turn).to.be.false 142 | }) 143 | }) 144 | }) -------------------------------------------------------------------------------- /client/app/components/HandHistory/Hand.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { css } from 'emotion' 4 | import Card from '../Game/Pieces/Card' 5 | import theme from 'app/utils/theme'; 6 | 7 | const flex = css` 8 | display: flex; 9 | align-items: center; 10 | ` 11 | const handStyle = css` 12 | max-height: calc(100vh - 160px); 13 | overflow-y: auto; 14 | `; 15 | const linkStyle = css` 16 | &:hover { 17 | cursor: pointer; 18 | color: ${theme.colors.blue}; 19 | } 20 | `; 21 | const cell = css`min-width: 150px;` 22 | const stepContainer = css` 23 | display: inline-flex; 24 | margin: 4px 0; 25 | padding: 6px; 26 | background: #eee; 27 | border-radius: 4px; 28 | ` 29 | const handNumber = css` 30 | text-transform: uppercase; 31 | font-size: 20px; 32 | ` 33 | 34 | type Props = { 35 | hand: { 36 | id: string, 37 | createdAt: string, 38 | history: string, 39 | }, 40 | onBackClick: () => void, 41 | } 42 | class Hand extends React.Component { 43 | renderHands(hand: { history: string }) { 44 | const history = JSON.parse(hand.history) 45 | const seats = Object.keys(history[0].seats).map(id => history[0].seats[id]).filter(seat => seat != null) 46 | const step = history[0] 47 | 48 | return ( 49 |
    50 | {seats.map(seat => ( 51 |
    52 |
    53 | Seat {seat.id} {step.button === seat.id ? '(D)' : ''}
    54 | ${seat.stack.toFixed(2)} 55 | {seat.player.username && 56 |
    {seat.player.username}
    57 | } 58 |
    59 | 60 | 61 |
    62 | ))} 63 |
    64 | ) 65 | } 66 | 67 | renderStep(step: any, renderBoard: boolean) { 68 | const realSidePots = step.sidePots.filter(pot => pot.amount > 0) 69 | 70 | return ( 71 |
    72 | {renderBoard && 73 |
    {step.board.map((card, index) => )}
    74 | } 75 |
    76 |
    77 | {Object.keys(step.seats).map(id => step.seats[id]).filter(seat => seat != null).map(seat => ( 78 |
    79 |
    Seat {seat.id} {seat.lastAction ? `(${seat.lastAction})` : ''}
    80 |
    Bet: ${seat.bet.toFixed(2)}
    81 |
    Stack: ${seat.stack.toFixed(2)}
    82 |
    83 | ))} 84 |
    85 |
    86 |
    87 | Main pot: ${step.mainPot.toFixed(2)}
    88 | Total pot: ${step.pot.toFixed(2)} 89 | {realSidePots.length > 0 && 90 |
    Side pots: [{realSidePots.map(p => `$${p.amount.toFixed(2)}`).join(', ')}]
    91 | } 92 |
    93 |
    94 |
    95 | {step.winMessages.length > 0 && ( 96 |
    97 | Winner: 98 | {step.winMessages.map((message, index) => ( 99 |
    {message}
    100 | ))} 101 |
    102 | )} 103 |
    104 | ) 105 | } 106 | 107 | render() { 108 | const { hand } = this.props 109 | const history = JSON.parse(hand.history) 110 | 111 | let lastDealt = -1; 112 | let dealt = -1; 113 | 114 | return ( 115 |
    116 |
    117 | {`<- Back to hand history`} 118 |
    119 |

    120 | Hand #{hand.id} - {new Date(hand.createdAt).toDateString()} 121 |

    122 | {this.renderHands(hand)} 123 | {history.slice(1).map((step, index) => { 124 | lastDealt = dealt; 125 | dealt = step.board.length; 126 | const showStreetName = lastDealt !== dealt; 127 | 128 | return ( 129 |
    130 | {showStreetName && ( 131 |
    132 | {(dealt === 0) && 'Preflop'} 133 | {(dealt === 3) && 'Flop'} 134 | {(dealt === 4) && 'Turn'} 135 | {(dealt === 5) && 'River'} 136 |
    137 | )} 138 | {this.renderStep(step, showStreetName)} 139 |
    140 | ) 141 | })} 142 |
    143 | ) 144 | } 145 | } 146 | 147 | export default Hand -------------------------------------------------------------------------------- /server/poker/__test__/multiplayer.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | 3 | var Table = require('../table') 4 | var Player = require('../player') 5 | 6 | function getAcesAndKings() { 7 | const suits = ['diamonds', 'hearts', 'clubs'] 8 | const kings = suits.map(suit => ({ 9 | rank: 'king', suit: suit 10 | })) 11 | const aces = suits.map(suit => ({ 12 | rank: 'ace', suit: suit 13 | })) 14 | return aces.concat(kings) 15 | } 16 | function getAceKing(suit) { 17 | return [ 18 | { rank: 'ace', suit: suit }, 19 | { rank: 'king', suit: suit } 20 | ] 21 | } 22 | function getSevenDeuce(suit) { 23 | return [ 24 | { rank: '7', suit: suit }, 25 | { rank: '2', suit: suit } 26 | ] 27 | } 28 | 29 | describe('A 3 handed game', () => { 30 | const table = new Table(1, 'Test table', 6, 10) 31 | const player1 = new Player('1', 1, 'Byron', 100) 32 | const player2 = new Player('2', 2, 'Alice', 100) 33 | const player3 = new Player('3', 3, 'Sandy', 100) 34 | 35 | table.sitPlayer(player1, 1, 5) 36 | table.sitPlayer(player2, 2, 8) 37 | table.sitPlayer(player3, 3, 10) 38 | table.startHand() 39 | 40 | describe('everyone is all in', () => { 41 | beforeEach(() => { 42 | table.seats[1].hand = getAceKing('spades') 43 | table.seats[2].hand = getSevenDeuce('spades') 44 | table.seats[3].hand = getSevenDeuce('hearts') 45 | table.deck.cards = getAcesAndKings() 46 | 47 | table.handleRaise('2', 8) 48 | table.changeTurn(2) 49 | table.handleRaise('3', 10) 50 | table.changeTurn(3) 51 | table.handleCall('1') 52 | table.changeTurn(1) 53 | }) 54 | 55 | it('the short stack wins the main pot and the others chop', () => { 56 | expect(table.pot).to.be.equal(15) 57 | expect(table.seats[1].stack).to.be.equal(15) 58 | expect(table.seats[2].stack).to.be.equal(3) 59 | expect(table.seats[3].stack).to.be.equal(5) 60 | }) 61 | }) 62 | 63 | it('the pot is equal to the blinds', () => { 64 | expect(+table.pot.toFixed(2)).to.be.equal(0.15) 65 | }) 66 | it('two players are in the blinds', () => { 67 | expect(table.seats[3].bet).to.be.equal(0.05) 68 | expect(table.seats[1].bet).to.be.equal(0.1) 69 | }) 70 | it('the player to act is on the button', () => { 71 | expect(table.turn).to.be.equal(2) 72 | expect(table.seats[2].bet).to.be.equal(0) 73 | expect(table.seats[2].turn).to.be.true 74 | }) 75 | }) 76 | 77 | describe('A 4 handed game', () => { 78 | let table 79 | let player1 80 | let player2 81 | let player3 82 | let player4 83 | 84 | beforeEach(() => { 85 | table = new Table(1, 'Test table', 6, 10) 86 | player1 = new Player('1', 1, 'Byron', 100) 87 | player2 = new Player('2', 2, 'Alice', 100) 88 | player3 = new Player('3', 3, 'Sandy', 100) 89 | player4 = new Player('4', 4, 'Robert', 100) 90 | 91 | table.sitPlayer(player1, 1, 5) 92 | table.sitPlayer(player2, 2, 8) 93 | table.sitPlayer(player3, 3, 10) 94 | table.sitPlayer(player4, 4, 20) 95 | table.startHand() 96 | }) 97 | 98 | describe('everyone is all in', () => { 99 | beforeEach(() => { 100 | table.seats[1].hand = getAceKing('spades') 101 | table.seats[2].hand = getSevenDeuce('spades') 102 | table.seats[3].hand = getSevenDeuce('hearts') 103 | table.seats[4].hand = getAceKing('hearts') 104 | table.deck.cards = getAcesAndKings() 105 | 106 | table.handleRaise('2', 8) 107 | table.changeTurn(2) 108 | table.handleRaise('3', 10) 109 | table.changeTurn(3) 110 | table.handleRaise('4', 20) 111 | table.changeTurn(4) 112 | table.handleCall('1') 113 | table.changeTurn(1) 114 | }) 115 | 116 | it('the board runs out', () => { 117 | expect(table.board).to.have.lengthOf(5) 118 | }) 119 | it('the pots have the correct amounts', () => { 120 | expect(table.pot).to.be.equal(20) 121 | expect(table.sidePots[0].amount).to.be.equal(9) 122 | expect(table.sidePots[1].amount).to.be.equal(4) 123 | expect(table.sidePots[2].amount).to.be.equal(10) 124 | }) 125 | it('the pots go to the correct players', () => { 126 | expect(table.seats[1].stack).to.be.equal(10) 127 | expect(table.seats[2].stack).to.be.equal(0) 128 | expect(table.seats[3].stack).to.be.equal(0) 129 | expect(table.seats[4].stack).to.be.equal(33) 130 | }) 131 | it('the felted players are sitting out', () => { 132 | expect(table.seats[2].sittingOut).to.be.true 133 | expect(table.seats[3].sittingOut).to.be.true 134 | }) 135 | it('the hand is over', () => { 136 | expect(table.handOver).to.be.true 137 | }) 138 | }) 139 | 140 | it('the pot is equal to the blinds', () => { 141 | expect(+table.pot.toFixed(2)).to.be.equal(0.15) 142 | }) 143 | it('two players are in the blinds', () => { 144 | expect(table.seats[3].bet).to.be.equal(0.05) 145 | expect(table.seats[4].bet).to.be.equal(0.1) 146 | }) 147 | it('the player to act is on the button', () => { 148 | expect(table.turn).to.be.equal(1) 149 | expect(table.seats[1].bet).to.be.equal(0) 150 | expect(table.seats[1].turn).to.be.true 151 | expect(table.seats[2].bet).to.be.equal(0) 152 | expect(table.seats[2].turn).to.be.false 153 | }) 154 | }) -------------------------------------------------------------------------------- /server/poker/__test__/table.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | 3 | var Table = require('../table') 4 | var Player = require('../player') 5 | 6 | describe('Table', function() { 7 | describe('initialization', () => { 8 | const table = new Table(1, 'Test Table', 6, 100) 9 | 10 | it('has correct id', () => { 11 | expect(table.id).to.be.equal(1) 12 | }) 13 | it('has correct name', () => { 14 | expect(table).to.have.property('name', 'Test Table') 15 | }) 16 | it('has correct max number of players', () => { 17 | expect(table.maxPlayers).to.be.equal(6) 18 | }) 19 | it('initializes the seats object', () => { 20 | expect(table.seats).to.have.property('1', null) 21 | expect(table.seats).to.have.property('2', null) 22 | expect(table.seats).to.have.property('3', null) 23 | expect(table.seats).to.have.property('4', null) 24 | expect(table.seats).to.have.property('5', null) 25 | expect(table.seats).to.have.property('6', null) 26 | }) 27 | }) 28 | 29 | describe('#addPlayer(player)', () => { 30 | const table = new Table(1, 'Test Table', 6, 100) 31 | const player = new Player('9988', 1, 'Byron', 1000) 32 | table.addPlayer(player) 33 | 34 | it('adds the player to the players array', () => { 35 | expect(table.players).to.have.lengthOf(1) 36 | expect(table.players[0]).to.have.property('socketId', '9988') 37 | expect(table.players[0]).to.have.property('name', 'Byron') 38 | expect(table.players[0]).to.have.property('bankroll', 1000) 39 | }) 40 | }) 41 | 42 | describe('#sitPlayer(player, seatId)', () => { 43 | const table = new Table(1, 'Test Table', 6, 100) 44 | const player1 = new Player('9988', 1, 'Byron', 1000) 45 | const player2 = new Player('7766', 2, 'Jim', 500) 46 | table.sitPlayer(player1, 6) 47 | 48 | it('adds a new Seat to its seats object', () => { 49 | expect(table.seats[6]).to.have.property('id', 6) 50 | expect(table.seats[6].player).to.have.property('socketId', '9988') 51 | expect(table.seats[6].player).to.have.property('name', 'Byron') 52 | expect(table.seats[6].player).to.have.property('bankroll', 1000) 53 | }) 54 | it('does nothing for occupied seats', () => { 55 | table.sitPlayer(player2, 6) 56 | expect(table.seats[6].player).to.have.property('socketId', '9988') 57 | expect(table.seats[6].player).to.have.property('name', 'Byron') 58 | expect(table.seats[6].player).to.have.property('bankroll', 1000) 59 | }) 60 | }) 61 | 62 | describe('#standPlayer(socketId)', () => { 63 | const table = new Table(1, 'Test Table', 6, 100) 64 | const player = new Player('9988', 1, 'Byron', 1000) 65 | table.sitPlayer(player, 6) 66 | table.standPlayer('9988') 67 | 68 | it('sets seat with matching player socket id to null', () => { 69 | expect(table.seats[6]).to.be.equal(null) 70 | }) 71 | }) 72 | 73 | describe('#removePlayer(socketId)', () => { 74 | const table = new Table(1, 'Test Table', 6, 100) 75 | const player = new Player('9988', 1, 'Byron', 1000) 76 | table.addPlayer(player) 77 | table.sitPlayer(player, 3) 78 | table.removePlayer('9988') 79 | 80 | it('removes player with matching socket id from players array', () => { 81 | expect(table.players.filter(player => player.socketId === '9988')).to.have.lengthOf(0) 82 | }) 83 | it('sets seat with matching player socket id to null', () => { 84 | expect(table.seats[3]).to.be.equal(null) 85 | }) 86 | it('resets the table properties when the last player leaves', () => { 87 | expect(table.players).to.have.lengthOf(0) 88 | expect(table.board).to.have.lengthOf(0) 89 | expect(table.deck).to.be.equal(null) 90 | expect(table.button).to.be.equal(null) 91 | expect(table.turn).to.be.equal(null) 92 | expect(table.pot).to.be.equal(0) 93 | expect(table.mainPot).to.be.equal(0) 94 | expect(table.callAmount).to.be.equal(null) 95 | expect(table.minBet).to.be.equal(table.limit / 200) 96 | expect(table.minRaise).to.be.equal(table.limit / 100) 97 | expect(table.smallBlind).to.be.equal(null) 98 | expect(table.bigBlind).to.be.equal(null) 99 | expect(table.handOver).to.be.true 100 | expect(table.winMessages).to.have.lengthOf(0) 101 | expect(table.wentToShowdown).to.be.false 102 | }) 103 | it('resets the seats when the last player leaves', () => { 104 | for (let seat of Object.values(table.seats)) { 105 | expect(seat).to.be.equal(null) 106 | } 107 | }) 108 | }) 109 | 110 | describe('Sitting out', () => { 111 | let table 112 | let player1 113 | let player2 114 | let player3 115 | 116 | beforeEach(() => { 117 | table = new Table(1, 'Test table', 6, 10) 118 | player1 = new Player(1, 1, 'Player 1', 10) 119 | player2 = new Player(2, 2, 'Player 2', 10) 120 | player3 = new Player(3, 3, 'Player 3', 10) 121 | 122 | table.sitPlayer(player1, 1, 10) 123 | table.sitPlayer(player2, 2, 10) 124 | table.sitPlayer(player3, 3, 10) 125 | }) 126 | 127 | describe('when a new hand starts', () => { 128 | beforeEach(() => { 129 | table.seats[1].folded = false 130 | table.seats[1].sittingOut = true 131 | table.startHand() 132 | }) 133 | 134 | it('does not deal in the sitting out player', () => { 135 | expect(table.seats[1].hand).to.have.lengthOf(0) 136 | expect(table.seats[1].folded).to.be.true 137 | }) 138 | it('deals in the other players', () => { 139 | expect(table.seats[2].hand).to.have.lengthOf(2) 140 | expect(table.seats[3].hand).to.have.lengthOf(2) 141 | 142 | expect(table.seats[2].folded).to.be.false 143 | expect(table.seats[3].folded).to.be.false 144 | }) 145 | }) 146 | 147 | describe('when only 1 player is active', () => { 148 | beforeEach(() => { 149 | table.seats[1].sittingOut = true 150 | table.seats[2].sittingOut = true 151 | table.startHand() 152 | }) 153 | 154 | it('does not start the hand', () => { 155 | expect(table.board).to.have.lengthOf(0) 156 | expect(table.handOver).to.be.true 157 | }) 158 | }) 159 | 160 | describe('when in the middle of a hand', () => { 161 | beforeEach(() => { 162 | table.startHand() 163 | table.seats[1].sittingOut = true 164 | table.seats[2].sittingOut = true 165 | table.seats[3].sittingOut = true 166 | }) 167 | 168 | it('allows the sitting out player to finish the current hand', () => { 169 | expect(table.seats[1].hand).to.have.lengthOf(2) 170 | expect(table.seats[1].folded).to.be.false 171 | expect(table.unfoldedPlayers()).to.have.lengthOf(3) 172 | expect(table.turn).to.be.equal(2) 173 | }) 174 | }) 175 | 176 | describe('when changing turns', () => { 177 | beforeEach(() => { 178 | table.startHand() 179 | table.seats[3].sittingOut = true 180 | table.changeTurn(table.turn) 181 | }) 182 | 183 | it('changes turns to the sitting out player', () => { 184 | expect(table.nextUnfoldedPlayer(2, 1)).to.be.equal(3) 185 | expect(table.turn).to.be.equal(3) 186 | }) 187 | 188 | it('the hand is not over', () => { 189 | expect(table.handOver).to.be.false 190 | }) 191 | }) 192 | }) 193 | }) -------------------------------------------------------------------------------- /server/socket/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const db = require('../db/models') 3 | const Player = require('../poker/player.js') 4 | const Table = require('../poker/table.js') 5 | 6 | const tables = {} 7 | const players = {} 8 | 9 | tables[1] = new Table(1, 'Table 1', 6, 10) 10 | tables[2] = new Table(2, 'Table 2', 6, 10) 11 | tables[3] = new Table(3, 'Table 3', 6, 20) 12 | tables[4] = new Table(4, 'Table 4', 6, 20) 13 | tables[5] = new Table(5, 'Table 5', 6, 50) 14 | tables[6] = new Table(6, 'Table 6', 9, 10) 15 | tables[7] = new Table(7, 'Table 7', 9, 10) 16 | tables[8] = new Table(8, 'Table 8', 9, 20) 17 | tables[9] = new Table(9, 'Table 9', 9, 20) 18 | tables[10] = new Table(10, 'Table 10', 9, 50) 19 | 20 | module.exports = { 21 | init(socket, io) { 22 | socket.on('fetch_lobby_info', user => { 23 | players[socket.id] = new Player(socket.id, user.id, user.username, user.bankroll) 24 | 25 | socket.emit('receive_lobby_info', { tables, players, socketId: socket.id }) 26 | socket.broadcast.emit('players_updated', players) 27 | }) 28 | 29 | socket.on('join_table', tableId => { 30 | tables[tableId].addPlayer(players[socket.id]) 31 | 32 | socket.broadcast.emit('tables_updated', tables) 33 | socket.emit('table_joined', { tables, tableId }) 34 | }) 35 | 36 | socket.on('leave_table', tableId => { 37 | const table = tables[tableId] 38 | const player = players[socket.id] 39 | const seat = Object.values(table.seats).find(seat => 40 | seat && seat.player.socketId === socket.id 41 | ) 42 | if (seat) { 43 | updatePlayerBankroll(player, seat.stack) 44 | } 45 | 46 | table.removePlayer(socket.id) 47 | 48 | socket.broadcast.emit('tables_updated', tables) 49 | socket.emit('table_left', { tables, tableId }) 50 | 51 | if (table.activePlayers().length === 1) { 52 | clearForOnePlayer(table) 53 | } 54 | }) 55 | 56 | socket.on('fold', tableId => { 57 | let table = tables[tableId] 58 | let { seatId, message } = table.handleFold(socket.id) 59 | broadcastToTable(table, message) 60 | changeTurnAndBroadcast(table, seatId) 61 | }) 62 | 63 | socket.on('check', tableId => { 64 | let table = tables[tableId] 65 | let { seatId, message } = table.handleCheck(socket.id) 66 | broadcastToTable(table, message) 67 | changeTurnAndBroadcast(table, seatId) 68 | }) 69 | 70 | socket.on('call', tableId => { 71 | let table = tables[tableId] 72 | let { seatId, message } = table.handleCall(socket.id) 73 | broadcastToTable(table, message) 74 | changeTurnAndBroadcast(table, seatId) 75 | }) 76 | 77 | socket.on('raise', ({ tableId, amount }) => { 78 | let table = tables[tableId] 79 | let { seatId, message } = table.handleRaise(socket.id, amount) 80 | broadcastToTable(table, message) 81 | changeTurnAndBroadcast(table, seatId) 82 | }) 83 | 84 | socket.on('table_message', ({ message, from, tableId }) => { 85 | let table = tables[tableId] 86 | broadcastToTable(table, message, from) 87 | }) 88 | 89 | socket.on('sit_down', ({ tableId, seatId, amount }) => { 90 | const table = tables[tableId] 91 | const player = players[socket.id] 92 | 93 | table.sitPlayer(player, seatId, amount) 94 | let message = `${player.name} sat down in Seat ${seatId}` 95 | 96 | updatePlayerBankroll(player, -(amount)) 97 | 98 | broadcastToTable(table, message) 99 | if (table.activePlayers().length === 2) { 100 | initNewHand(table) 101 | } 102 | }) 103 | 104 | socket.on('rebuy', ({ tableId, seatId, amount }) => { 105 | const table = tables[tableId] 106 | const player = players[socket.id] 107 | 108 | table.rebuyPlayer(seatId, amount) 109 | updatePlayerBankroll(player, -(amount)) 110 | 111 | broadcastToTable(table) 112 | }) 113 | 114 | socket.on('stand_up', tableId => { 115 | const table = tables[tableId] 116 | const player = players[socket.id] 117 | const seat = Object.values(table.seats).find(seat => 118 | seat && seat.player.socketId === socket.id 119 | ) 120 | 121 | let message = ''; 122 | if (seat) { 123 | updatePlayerBankroll(player, seat.stack) 124 | message = `${player.name} left the table` 125 | } 126 | 127 | table.standPlayer(socket.id) 128 | 129 | broadcastToTable(table, message) 130 | if (table.activePlayers().length === 1) { 131 | clearForOnePlayer(table) 132 | } 133 | }) 134 | 135 | socket.on('sitting_out', ({ tableId, seatId }) => { 136 | const table = tables[tableId] 137 | const seat = table.seats[seatId] 138 | seat.sittingOut = true 139 | 140 | broadcastToTable(table) 141 | }) 142 | 143 | socket.on('sitting_in', ({ tableId, seatId }) => { 144 | const table = tables[tableId] 145 | const seat = table.seats[seatId] 146 | seat.sittingOut = false 147 | 148 | broadcastToTable(table) 149 | if (table.handOver && table.activePlayers().length === 2) { 150 | initNewHand(table) 151 | } 152 | }) 153 | 154 | socket.on('disconnect', () => { 155 | const seat = findSeatBySocketId(socket.id) 156 | if (seat) { 157 | updatePlayerBankroll(seat.player, seat.stack) 158 | } 159 | 160 | delete players[socket.id] 161 | removeFromTables(socket.id) 162 | 163 | socket.broadcast.emit('tables_updated', tables) 164 | socket.broadcast.emit('players_updated', players) 165 | }) 166 | 167 | async function updatePlayerBankroll(player, amount) { 168 | const user = await db.User.findById(player.id) 169 | await db.User.update( 170 | { bankroll: user.bankroll + amount }, 171 | { where: { id: player.id } } 172 | ) 173 | players[socket.id].bankroll = user.bankroll + amount 174 | io.to(socket.id).emit('players_updated', players) 175 | } 176 | 177 | async function saveHandHistory(table) { 178 | const seats = Object.keys(table.seats).map(seatId => table.seats[seatId]) 179 | const players = seats.filter(seat => seat != null).map(seat => seat.player) 180 | 181 | const hand = await db.Hand.create({ 182 | history: JSON.stringify(table.history), 183 | }) 184 | await db.UserHand.bulkCreate( 185 | players.map(player => ({ 186 | user_id: player.id, 187 | hand_id: hand.id, 188 | })) 189 | ); 190 | } 191 | 192 | function findSeatBySocketId(socketId) { 193 | let foundSeat = null 194 | Object.values(tables).forEach(table => { 195 | Object.values(table.seats).forEach(seat => { 196 | if (seat && seat.player.socketId === socketId) { 197 | foundSeat = seat 198 | } 199 | }) 200 | }) 201 | return foundSeat 202 | } 203 | 204 | function removeFromTables(socketId) { 205 | for (let i = 0; i < Object.keys(tables).length; i++) { 206 | tables[Object.keys(tables)[i]].removePlayer(socketId) 207 | } 208 | } 209 | 210 | function broadcastToTable(table, message = null, from = null) { 211 | for (let i = 0; i < table.players.length; i++) { 212 | let socketId = table.players[i].socketId 213 | let tableCopy = hideOpponentCards(table, socketId) 214 | io.to(socketId).emit('table_updated', { table: tableCopy, message, from }) 215 | } 216 | } 217 | 218 | function changeTurnAndBroadcast(table, seatId) { 219 | setTimeout(() => { 220 | table.changeTurn(seatId) 221 | broadcastToTable(table) 222 | 223 | if (table.handOver) { 224 | saveHandHistory(table) 225 | initNewHand(table) 226 | } 227 | }, 1000) 228 | } 229 | 230 | function initNewHand(table) { 231 | table.clearWinMessages() 232 | if (table.activePlayers().length > 1) { 233 | broadcastToTable(table, '---New hand starting in 5 seconds---') 234 | } 235 | setTimeout(() => { 236 | table.startHand() 237 | broadcastToTable(table) 238 | }, 5000) 239 | } 240 | 241 | function clearForOnePlayer(table) { 242 | saveHandHistory(table) 243 | 244 | table.clearWinMessages() 245 | setTimeout(() => { 246 | table.clearSeatHands() 247 | table.resetBoardAndPot() 248 | broadcastToTable(table, 'Waiting for more players') 249 | }, 5000) 250 | } 251 | 252 | function hideOpponentCards(table, socketId) { 253 | let tableCopy = JSON.parse(JSON.stringify(table)) 254 | let hiddenCard = { suit: 'hidden', rank: 'hidden' } 255 | let hiddenHand = [hiddenCard, hiddenCard] 256 | 257 | for (let i = 1; i <= tableCopy.maxPlayers; i++) { 258 | let seat = tableCopy.seats[i] 259 | if ( 260 | seat && 261 | seat.hand.length > 0 && 262 | seat.player.socketId !== socketId && 263 | !(seat.lastAction === 'WINNER' && tableCopy.wentToShowdown) 264 | ) { 265 | seat.hand = hiddenHand 266 | } 267 | } 268 | return tableCopy 269 | } 270 | } 271 | } -------------------------------------------------------------------------------- /client/app/components/Lobby/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import { connect } from 'react-redux' 4 | import { css } from 'emotion'; 5 | 6 | import { logout } from '../../actions/user' 7 | import { 8 | receiveLobbyInfo, tablesUpdated, playersUpdated, 9 | tableJoined, tableLeft, tableUpdated 10 | } from '../../actions/lobby' 11 | 12 | import MainMenu from './MainMenu' 13 | import TopNav from './TopNav'; 14 | import BottomNav from './BottomNav'; 15 | import Game from '../Game/Game' 16 | import BuyinModal from '../Game/BuyinModal' 17 | import { Button } from 'app/components' 18 | 19 | const outerContainer = css` 20 | display: flex; 21 | width: 200vw; 22 | overflow-x: auto; 23 | transition: transform 0.3s; 24 | ` 25 | const innerContainer = css` 26 | width: 100vw; 27 | height: 100vh; 28 | max-height: 100vh; 29 | position: relative; 30 | overflow-x: hidden; 31 | overflow-y: hidden; 32 | ` 33 | const lobbyContainer = css` 34 | ${innerContainer} 35 | background: #fafafa; 36 | ` 37 | 38 | type Props = { 39 | children?: React.Node, 40 | socket: any, 41 | user: { 42 | id: number, 43 | username: string, 44 | bankroll: number, 45 | }, 46 | tables: { 47 | [key: number]: { 48 | table: { 49 | limit: number, 50 | seats: { 51 | [key: number]: ?{ 52 | stack: number, 53 | player: {}, 54 | }, 55 | }, 56 | }, 57 | messages: Array, 58 | }, 59 | }, 60 | players: { 61 | [socketId: string]: ?{ 62 | id: number, 63 | name: string, 64 | bankroll: number, 65 | }, 66 | }, 67 | openTables: { 68 | [key: string]: { 69 | table: Object, 70 | }, 71 | }, 72 | messages: Array<{ 73 | message: string, 74 | from: string, 75 | tableId: number, 76 | }>, 77 | gridViewOn: boolean, 78 | logout: () => void, 79 | receiveLobbyInfo: (tables: {}, players: {}, socketId: string) => void, 80 | tablesUpdated: (tables: {}) => void, 81 | playersUpdated: (players: {}) => void, 82 | tableJoined: (tables: {}, tableId: number) => void, 83 | tableLeft: (tables: {}, tableId: number) => void, 84 | tableUpdated: (table: {}, message: ?string, from: string) => void, 85 | } 86 | type State = { 87 | modalOpen: boolean, 88 | tableId: ?number, 89 | seatId: ?number, 90 | onMenu: boolean, 91 | } 92 | class Lobby extends React.Component { 93 | constructor() { 94 | super() 95 | this.state = { 96 | modalOpen: false, 97 | tableId: null, 98 | seatId: null, 99 | onMenu: true, 100 | } 101 | } 102 | 103 | componentDidMount() { 104 | const { 105 | socket, user, receiveLobbyInfo, tablesUpdated, playersUpdated, 106 | tableJoined, tableLeft, tableUpdated 107 | } = this.props 108 | 109 | if (user) { 110 | socket.emit('fetch_lobby_info', user) 111 | } 112 | 113 | socket.on('receive_lobby_info', ({ tables, players, socketId }) => { 114 | receiveLobbyInfo(tables, players, socketId) 115 | }) 116 | socket.on('tables_updated', tables => { 117 | tablesUpdated(tables) 118 | }) 119 | socket.on('players_updated', players => { 120 | playersUpdated(players) 121 | }) 122 | socket.on('table_joined', ({ tables, tableId }) => { 123 | tableJoined(tables, tableId) 124 | }) 125 | socket.on('table_left', ({ tables, tableId }) => { 126 | tableLeft(tables, tableId) 127 | }) 128 | socket.on('table_updated', ({ table, message, from }) => { 129 | tableUpdated(table, message, from) 130 | let gameChat = document.getElementById(`table-${table.id}-game-chat`) 131 | if (gameChat) { 132 | gameChat.scrollTop = gameChat.scrollHeight 133 | } 134 | }) 135 | } 136 | 137 | componentWillReceiveProps(nextProps) { 138 | const { socket, user } = nextProps 139 | if (!this.props.user && user) { 140 | socket.emit('fetch_lobby_info', user) 141 | } 142 | } 143 | 144 | handleTableClick = tableId => { 145 | if (Object.keys(this.props.openTables).length < 4) { 146 | this.props.socket.emit('join_table', tableId) 147 | } 148 | this.setState({ onMenu: false }) 149 | } 150 | 151 | handleLeaveClick = tableId => { 152 | this.props.socket.emit('leave_table', tableId) 153 | this.setState({ onMenu: true }) 154 | } 155 | 156 | handleSeatClick = (tableId, seatId) => { 157 | this.setState({ modalOpen: true, tableId: tableId, seatId: seatId }) 158 | } 159 | 160 | toggleMenu = () => { 161 | this.setState({ onMenu: !this.state.onMenu }) 162 | } 163 | 164 | closeModal = () => { 165 | this.setState({ modalOpen: false, tableId: null, seatId: null }) 166 | } 167 | 168 | buyInAndSitDown = (tableId, seatId, amount) => { 169 | this.props.socket.emit('sit_down', { tableId, seatId, amount }) 170 | } 171 | 172 | handleRebuy = (tableId, seatId, amount) => { 173 | this.props.socket.emit('rebuy', { tableId, seatId, amount }) 174 | } 175 | 176 | handleStandClick = tableId => { 177 | this.props.socket.emit('stand_up', tableId) 178 | } 179 | 180 | handleRaiseClick = (tableId, amount) => { 181 | this.props.socket.emit('raise', { tableId, amount }) 182 | } 183 | 184 | handleCheckClick = tableId => { 185 | this.props.socket.emit('check', tableId) 186 | } 187 | 188 | handleCallClick = tableId => { 189 | this.props.socket.emit('call', tableId) 190 | } 191 | 192 | handleFoldClick = tableId => { 193 | this.props.socket.emit('fold', tableId) 194 | } 195 | 196 | sendTableMessage = (e, tableId) => { 197 | const { socket, user } = this.props 198 | const body = e.target.value 199 | 200 | if (e.keyCode === 13 && body) { 201 | socket.emit('table_message', { message: body, from: user.username, tableId }) 202 | e.target.value = '' 203 | } 204 | } 205 | 206 | render() { 207 | const { 208 | socket, 209 | user, 210 | tables, 211 | players, 212 | openTables, 213 | messages, 214 | logout 215 | } = this.props 216 | 217 | let table 218 | let seat 219 | 220 | const keys = Object.keys(openTables) 221 | const isInGame = keys.length > 0 222 | 223 | if (isInGame) { 224 | table = keys[0] && openTables[keys[0]].table 225 | const seatKeys = table && Object.keys(table.seats) 226 | seat = seatKeys && seatKeys.map(id => table && table.seats[id]).find(seat => 227 | seat && seat.player.socketId === socket.id 228 | ) 229 | } 230 | 231 | if (!user) { 232 | return
    233 | } 234 | 235 | return ( 236 |
    237 |
    238 | 239 | {this.props.children ? this.props.children : ( 240 | 250 | )} 251 | 252 | {isInGame && ( 253 |
    254 | 257 |
    258 | )} 259 |
    260 | 261 |
    262 |
    263 | 266 |
    267 | {Object.keys(openTables).length > 0 && 268 | Object.values(openTables).map(table => 269 | 284 | ) 285 | } 286 | 296 |
    297 |
    298 | ) 299 | } 300 | } 301 | 302 | function mapStateToProps(state) { 303 | return { 304 | user: state.user.user, 305 | tables: state.lobby.tables, 306 | players: state.lobby.players, 307 | openTables: state.lobby.openTables, 308 | messages: state.lobby.messages, 309 | } 310 | } 311 | 312 | const mapDispatchToProps = ({ 313 | logout, 314 | receiveLobbyInfo, 315 | tablesUpdated, 316 | playersUpdated, 317 | tableJoined, 318 | tableLeft, 319 | tableUpdated, 320 | }) 321 | 322 | export default connect( 323 | mapStateToProps, 324 | mapDispatchToProps 325 | )(Lobby) 326 | -------------------------------------------------------------------------------- /server/poker/__test__/raise.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | 3 | var Table = require('../table') 4 | var Player = require('../player') 5 | 6 | function getHighCards() { 7 | return [ 8 | { rank: 'ace', suit: 'diamonds' }, 9 | { rank: 'ace', suit: 'hearts' }, 10 | { rank: 'ace', suit: 'clubs' }, 11 | { rank: 'king', suit: 'diamonds' }, 12 | { rank: 'king', suit: 'hearts' }, 13 | ] 14 | } 15 | function getAceKing() { 16 | return [ 17 | { rank: 'ace', suit: 'spades' }, 18 | { rank: 'king', suit: 'spades' } 19 | ] 20 | } 21 | function getSevenDeuce() { 22 | return [ 23 | { rank: '7', suit: 'spades' }, 24 | { rank: '2', suit: 'spades' } 25 | ] 26 | } 27 | 28 | describe('Table.handleRaise', () => { 29 | let table 30 | let player1 31 | let player2 32 | 33 | beforeEach(() => { 34 | table = new Table(1, 'Test table', 6, 10) 35 | player1 = new Player('1', 1, 'Byron', 100) 36 | player2 = new Player('2', 2, 'Alice', 100) 37 | 38 | table.sitPlayer(player1, 1, 10) 39 | table.sitPlayer(player2, 2, 10) 40 | table.startHand() 41 | }) 42 | 43 | it('places the blinds', () => { 44 | expect(table.seats[1].bet).to.be.equal(table.limit/100) 45 | expect(table.seats[2].bet).to.be.equal(table.limit/200) 46 | }) 47 | 48 | describe('when someone raises', () => { 49 | const raiseAmount = 3 50 | 51 | beforeEach(() => { 52 | table.handleRaise('1', raiseAmount) 53 | }) 54 | it('their stack loses the amount', () => { 55 | expect(table.seats[1]).to.have.property('stack', 10 - raiseAmount) 56 | }) 57 | it('they have a bet of the raise amount', () => { 58 | expect(table.seats[1].bet).to.be.equal(raiseAmount) 59 | }) 60 | it('the pot increases by the raise amount', () => { 61 | expect(table.pot).to.be.equal(raiseAmount + table.seats[2].bet) 62 | }) 63 | it('the new call amount is the raise amount', () => { 64 | expect(table.callAmount).to.be.equal(raiseAmount) 65 | }) 66 | }) 67 | 68 | describe('hero raises and villain calls', () => { 69 | beforeEach(() => { 70 | const raiseAmount = 1.35 71 | table.handleRaise('2', raiseAmount) 72 | table.changeTurn(2) 73 | table.handleCall('1') 74 | table.changeTurn(1) 75 | }) 76 | 77 | it('the amounts are correct', () => { 78 | expect(+(table.seats[1].stack).toFixed(2)).to.be.equal(8.65) 79 | expect(+(table.seats[2].stack).toFixed(2)).to.be.equal(8.65) 80 | expect(table.pot).to.be.equal(2.7) 81 | expect(table.board).to.have.lengthOf(3) 82 | }) 83 | 84 | describe('then hero raises', () => { 85 | beforeEach(() => { 86 | const raiseAmount = 2.93 87 | table.handleRaise('2', raiseAmount) 88 | table.changeTurn(2) 89 | }) 90 | it('the amounts are correct', () => { 91 | expect(+(table.seats[1].stack).toFixed(2)).to.be.equal(8.65) 92 | expect(+(table.seats[2].stack).toFixed(2)).to.be.equal(5.72) 93 | }) 94 | 95 | describe('then villain re-raises and we call, ', () => { 96 | beforeEach(() => { 97 | const raiseAmount = 6.2 98 | table.handleRaise('1', raiseAmount) 99 | table.changeTurn(1) 100 | table.handleCall('2') 101 | table.changeTurn(1) 102 | }) 103 | it('the amounts are correct', () => { 104 | expect(+(table.seats[1].stack).toFixed(2)).to.be.equal(2.45) 105 | expect(+(table.seats[2].stack).toFixed(2)).to.be.equal(2.45) 106 | expect(+(table.pot).toFixed(2)).to.be.equal(15.1) 107 | expect(table.board).to.have.lengthOf(4) 108 | }) 109 | }) 110 | }) 111 | }) 112 | 113 | describe('when a player is all in', () => { 114 | beforeEach(() => { 115 | const raiseAmount = 10 116 | table.handleRaise('2', raiseAmount) 117 | table.changeTurn(2) 118 | }) 119 | 120 | it('the big blind has not acted yet', () => { 121 | expect(table.seats[1].bet).to.be.equal(0.1) 122 | expect(table.seats[1].stack).to.be.equal(9.9) 123 | }) 124 | 125 | it("all of the player's chips are bet", () => { 126 | expect(table.seats[2].bet).to.be.equal(10) 127 | expect(table.seats[2].stack).to.be.equal(0) 128 | }) 129 | 130 | describe('and the other player folds', () => { 131 | beforeEach(() => { 132 | table.seats[1].stack = 4.9 133 | table.handleFold('1') 134 | table.changeTurn(1) 135 | }) 136 | 137 | it('the all in player wins the pot', () => { 138 | expect(table.seats[1].stack).to.be.equal(4.9) 139 | expect(table.seats[2].stack).to.be.equal(10.1) 140 | }) 141 | }) 142 | 143 | describe('and the other player calls with more chips and loses', () => { 144 | beforeEach(() => { 145 | table.seats[1].hand = getSevenDeuce() 146 | table.seats[2].hand = getAceKing() 147 | table.deck.cards = getHighCards() 148 | 149 | table.seats[1].stack = 19.9 150 | table.handleCall('1') 151 | table.changeTurn(1) 152 | }) 153 | 154 | it('the all in player only wins as much as they started with', () => { 155 | expect(+(table.seats[1].stack).toFixed(2)).to.be.equal(10) 156 | expect(+(table.seats[2].stack).toFixed(2)).to.be.equal(20) 157 | }) 158 | 159 | describe('starting a new hand afterwards', () => { 160 | beforeEach(() => { 161 | table.startHand() 162 | }) 163 | 164 | it('the sidepots are reset', () => { 165 | expect(table.sidePots).to.have.lengthOf(0) 166 | }) 167 | 168 | it('the blinds rotate and are places', () => { 169 | expect(+(table.seats[1].stack).toFixed(2)).to.be.equal(9.95) 170 | expect(+(table.seats[2].stack).toFixed(2)).to.be.equal(19.9) 171 | expect(+(table.pot).toFixed(2)).to.be.equal(0.15) 172 | }) 173 | }) 174 | }) 175 | 176 | describe('and the other player calls with less chips and loses', () => { 177 | describe('in a random hand', () => { 178 | beforeEach(() => { 179 | table.seats[1].hand = getSevenDeuce() 180 | table.seats[2].hand = getAceKing() 181 | table.deck.cards = getHighCards() 182 | 183 | table.seats[1].stack = 4.9 184 | table.handleCall('1') 185 | table.changeTurn(1) 186 | }) 187 | 188 | it('the pot is the sum of their starting stacks', () => { 189 | expect(table.pot).to.be.equal(10) 190 | }) 191 | 192 | it('there is a sidepot', () => { 193 | expect(table.sidePots).to.have.lengthOf(1) 194 | expect(table.sidePots[0]).to.have.property('amount', 5) 195 | }) 196 | 197 | it('the felted player is sitting out', () => { 198 | expect(table.seats[1].sittingOut).to.be.true 199 | }) 200 | 201 | it('the all in player wins the entire pot', () => { 202 | expect(table.seats[1].stack).to.be.equal(0) 203 | expect(table.seats[2].stack).to.be.equal(15) 204 | }) 205 | }) 206 | }) 207 | 208 | describe('on a paired board', () => { 209 | beforeEach(() => { 210 | table.seats[1].hand = [ 211 | { rank: 'queen', suit: 'hearts' }, 212 | { rank: '5', suit: 'clubs' }, 213 | ]; 214 | table.seats[2].hand = [ 215 | { rank: 'jack', suit: 'clubs' }, 216 | { rank: '5', suit: 'spades' }, 217 | ]; 218 | table.deck.cards = [ 219 | { rank: '3', suit: 'clubs' }, 220 | { rank: '5', suit: 'hearts' }, 221 | { rank: 'jack', suit: 'hearts' }, 222 | { rank: 'ace', suit: 'spades' }, 223 | { rank: 'ace', suit: 'diamonds' }, 224 | ] 225 | table.seats[1].stack = 4.9 226 | table.handleCall('1') 227 | table.changeTurn(1) 228 | }) 229 | 230 | it('higher 2 pair wins', () => { 231 | expect(table.seats[1].stack).to.be.equal(0) 232 | expect(table.seats[2].stack).to.be.equal(15) 233 | }) 234 | }) 235 | 236 | describe('on board with 3 of a kind', () => { 237 | beforeEach(() => { 238 | table.seats[1].hand = [ 239 | { rank: 'queen', suit: 'hearts' }, 240 | { rank: '6', suit: 'clubs' }, 241 | ]; 242 | table.seats[2].hand = [ 243 | { rank: 'king', suit: 'clubs' }, 244 | { rank: '5', suit: 'spades' }, 245 | ]; 246 | table.deck.cards = [ 247 | { rank: '6', suit: 'clubs' }, 248 | { rank: 'king', suit: 'hearts' }, 249 | { rank: 'ace', suit: 'hearts' }, 250 | { rank: 'ace', suit: 'spades' }, 251 | { rank: 'ace', suit: 'diamonds' }, 252 | ] 253 | table.seats[1].stack = 4.9 254 | table.handleCall('1') 255 | table.changeTurn(1) 256 | }) 257 | 258 | it('higher full house wins', () => { 259 | expect(table.seats[1].stack).to.be.equal(0) 260 | expect(table.seats[2].stack).to.be.equal(15) 261 | }) 262 | }) 263 | 264 | describe('and the other player calls with less chips and wins', () => { 265 | beforeEach(() => { 266 | table.seats[1].hand = getAceKing() 267 | table.seats[2].hand = getSevenDeuce() 268 | table.deck.cards = getHighCards() 269 | 270 | table.seats[1].stack = 4.9 271 | table.handleCall('1') 272 | table.changeTurn(1) 273 | }) 274 | 275 | it('the other player only wins as much as they started with', () => { 276 | expect(table.seats[1].stack).to.be.equal(10) 277 | expect(table.seats[2].stack).to.be.equal(5) 278 | }) 279 | }) 280 | 281 | describe('and the other player calls with less chips and wins', () => { 282 | beforeEach(() => { 283 | table.seats[1].hand = getAceKing() 284 | table.seats[2].hand = getSevenDeuce() 285 | table.deck.cards = getHighCards() 286 | 287 | table.seats[1].stack = 0 288 | table.handleCall('1') 289 | table.changeTurn(1) 290 | }) 291 | 292 | it('expect the all in player to double up their blind', () => { 293 | expect(+(table.seats[1].stack).toFixed(2)).to.be.equal(0.2) 294 | expect(+(table.seats[2].stack).toFixed(2)).to.be.equal(9.9) 295 | }) 296 | }) 297 | }) 298 | }) --------------------------------------------------------------------------------