<file_sep>
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import QRCode from 'qrcode.react';
import Connection from '../connection';

import { D_GRAY, DARK_SLATE, WHITE } from '../utils/Colors';

const Wrapper = styled.div`
  display: flex;
  justify-content: center;
  flex-direction: column;
  font-size: 2rem;
  margin-top: 5vh;

  @media only screen and (max-width: 600px) {
    width: 100%;
  }
`;

const OptionsWrapper = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
`;

const Option = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-content: center;
  align-items: center;
  width: 20rem;
  height: 10rem;
  margin: 1rem 0;
  user-select: none;
  
  color: ${(props) => props.color};
  border: 2px solid ${(props) => props.color};

  ${(props) => props.selected && `
    background-color: ${props.color};
    color: ${WHITE};
  `}
`;


const Picking = ({ players, playerId }) => {
  const currentPlayer = players[playerId];
  console.log(players)
  return (
    <Wrapper>
      <br />
      <OptionsWrapper>
        {['ROCK', 'PAPER', 'SCISSORS'].map(option => (
          <Option
            color={currentPlayer.color}
            onClick={() => Connection.makeMove(option)}
            selected={currentPlayer.move == option}
            key={option}
          >
          {option}
        </Option>
        ))}
      </OptionsWrapper>
    </Wrapper>
  );
};

const mapStateToProps = (state) => ({
  players: state.players,
  playerId: state.playerId,
});

export default connect(mapStateToProps)(Picking);
<file_sep>dnspython==1.16.0
eventlet==0.25.1
greenlet==0.4.15
monotonic==1.5
python-engineio==3.10.0
python-socketio==4.3.1
six==1.13.0
gunicorn==20.0.0<file_sep>
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import QRCode from 'qrcode.react';

import { D_GRAY, DARK_SLATE, WHITE } from '../utils/Colors';

const Wrapper = styled.div`
  display: flex;
  justify-content: center;
  flex-direction: column;
  font-size: 2rem;
  margin-top: 5vh;
  color: ${WHITE};

  @media only screen and (max-width: 600px) {
    width: 100%;
  }
`;

const Row = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;

  ${(props) => props.small && `
    font-size: 1.5rem;
  `}}
`;

const QRWrapper = styled.div`
  align-self: center;
  background-color: ${WHITE};
  border-radius: 0.25rem;
  padding: 0.7rem 0.7rem 0.3rem;
  margin: 1rem;
`;

const Landing = ({ status, gameId, playerId, players }) => {
  // You are not in the game
  // You are in the game, waiting for another
  const isInGame = !!players[playerId];
  console.log(players)
  const prompt = isInGame ? 'Waiting for another player' : 'Game currently full';
  return (
    <Wrapper>
      <Row>Welcome</Row>
      <Row>Game:{gameId} | Player:{playerId}</Row>
      <Row>{prompt}</Row>
    </Wrapper>
  );
};

const mapStateToProps = (state) => ({
  status: state.status,
  gameId: state.gameId,
  playerId: state.playerId,
  players: state.players,
});

export default connect(mapStateToProps)(Landing);
<file_sep>
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import QRCode from 'qrcode.react';
import { getStatus } from '../store/selectors';

import { D_GRAY, DARK_SLATE, WHITE } from '../utils/Colors';

const Wrapper = styled.div`
  display: flex;
  justify-content: center;
  flex-direction: column;
  font-size: 6rem;
  margin-top: 5vh;
  color: ${WHITE};

  @media only screen and (max-width: 600px) {
    width: 100%;
  }
`;

const Row = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;

  ${(props) => props.small && `
    font-size: 4.5rem;
  `}}
`;

const QRWrapper = styled.div`
  align-self: center;
  background-color: ${WHITE};
  border-radius: 0.25rem;
  padding: 1rem;
  margin: 20vh;
`;

console.log(process.env.REMOTE_HOST)
const BASE_HOST = process.env.REMOTE_HOST || 'localhost'
const BASE = `${BASE_HOST}:5001`;
// const BASE = 'arkade.ngrok.io';
// const BASE = 'http://ixn-arkade.s3-website-us-east-1.amazonaws.com';

const Landing = ({ gameId, status }) => {
  const gameUrl = `${BASE}/?id=${gameId}`
  console.log(gameUrl)
  let prompt = 'Default prompt';
  if (status === 'SLEEPING') prompt = '^ Scan to Start ^';
  else if (status === 'JOINING') prompt = 'Waiting for one more...';
  return (
    <Wrapper>
      <Row>Rock,</Row>
      <Row>Paper,</Row>
      <Row>Scissors</Row>
      <QRWrapper>
        <a href={gameUrl}>
          <QRCode size={500} value={gameUrl} />
        </a>
      </QRWrapper>
      <Row small>{prompt}</Row>
    </Wrapper>
  );
};

const mapStateToProps = (state) => ({
  status: getStatus(state),
  gameId: state.gameId,
});

export default connect(mapStateToProps)(Landing);
<file_sep>import io from 'socket.io-client';
import * as Actions from './store/actions';
import { getStatus } from './store/selectors';

const API_BASE = 'localhost:3000';
// const API_BASE = 'http://api.arkade.ngrok.io';
// const API_BASE = '192.168.3.11';

class Connection {
  init(store) {
    this.store = store;
    this.io = io(API_BASE);
    
    this.io.on('connect', () => {
      const { gameId } = this.store.getState();
      console.log(`Connected: Game ${gameId}`)
      this.io.emit('create_game', gameId);
      this.registerHandlers();
    });
  }

  registerHandlers() {
    this.io.on('new_player', (playerId) => {
      this.store.dispatch(Actions.addPlayer(playerId));
    });

    this.io.on('new_move', ({ player_id, move }) => {
      console.log(player_id)
      this.store.dispatch(Actions.setMove(player_id, move));
    });

    this.io.on('disconnect', (e) => {
      console.log(e)
    });

    const unsubscribe = store.subscribe(() => {
      const state = store.getState();
      const gameState = {
        status: getStatus(state),
        ...state,
      };
      console.log(gameState)
      this.io.emit('update_state', state.gameId, gameState);
    });
  }
}

export default new Connection();<file_sep>import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router';

import * as Actions from './actions';

const url = new URL(location);
const urlParameters = new URLSearchParams(url.search);
const GAME_ID = urlParameters.get('id') || Math.round(Math.random()*10000);

const gameId = () => GAME_ID;

export const players = (state = {}, action) => {
  switch (action.type) {
    case Actions.ADD_PLAYER: {
      if (Object.keys(state).length < 2) {
        const color = Object.keys(state).length == 0 ? 'OrangeRed': 'DodgerBlue';
        return {
          [action.playerId]: {
            id: action.playerId,
            color,
            move: null,
            winner: null,
          },
          ...state,
        }
      }
      return state;
    }
    case Actions.SET_MOVE: {
      const player = state[action.playerId];
      return {
        ...state,
        [action.playerId]: {
          ...player,
          move: action.move,
        },
      };
    }
    case Actions.RESET_MOVES: {
      return Object.entries(state).reduce((prev, [id, player]) => ({
        ...prev,
        [id]: {
          ...player,
          move: null,
        }
      }), {});
    }
    case Actions.SET_WINNER: {
      const player = state[action.playerId];
      return {
        ...state,
        [action.playerId]: {
          ...player,
          winner: true,
        },
      };
    }
    default:
      return state;
  }
};

export default (history) =>
  combineReducers({
    router: connectRouter(history),
    gameId,
    players,
  });
<file_sep>
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import QRCode from 'qrcode.react';
import * as Actions from '../store/actions';

import { D_GRAY, DARK_SLATE, WHITE } from '../utils/Colors';

const Wrapper = styled.div`
  display: flex;
  justify-content: center;
  flex-direction: column;
  font-size: 4rem;
  margin-top: 5vh;
  color: ${WHITE};

  @media only screen and (max-width: 600px) {
    width: 100%;
  }
`;

const Row = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;

  ${(props) => props.small && `
    font-size: 1.5rem;
  `}}
`;

const SelectionWrapper = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;
  width: 50%;
  height: 50%;
  padding: 20rem 0;
  background-color: ${(props) => props.color};

  ${(props) => props.small && `
    font-size: 1.5rem;
  `}}
`;

const PlayerSelection = ({ color, move }) => {
  return move ? (
    <SelectionWrapper color={color}>
      READY
    </SelectionWrapper>
  ) : (
    <SelectionWrapper>
      WAITING
    </SelectionWrapper>
  );
}

const moveMap = {
  ROCK: 'SCISSORS',
  PAPER: 'ROCK',
  SCISSORS: 'PAPER',
}

const getWinningMove = ([moveA, moveB]) => {
  if (moveA === moveB) return null;
  if (moveMap[moveA] === moveB) return moveA;
  else return moveB;
}

const pickWinner = (players) => {
  const moves = Object.values(players).map(player => player.move);
  const winningMove = getWinningMove(moves)
  if (!winningMove) return null;

  const winningPlayer = Object.values(players).find(player => player.move === winningMove);
  return winningPlayer;
}

class Picking extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidUpdate(prevProps, prevState) {
    const { players, dispatch } = this.props;
    const numMoves = (play) => Object.values(play).filter(player => player.move).length;

    if (numMoves(prevProps.players) != numMoves(players)) {
      const [playerA, playerB] = Object.values(players);
      if (playerA.move && playerB.move) {
        // FOR SUSPENSE
        setTimeout(() => {
          const winner = pickWinner(players);
          if (winner === null) {
            iziToast.info({
              title: 'TIE',
              titleSize: '2rem',
              position: 'center',
              timeout: 1000,
            });
            setTimeout(() => dispatch(Actions.resetMoves()), 1000);
          } else {
            dispatch(Actions.setWinner(winner.id));
          }
        }, 1500);
      }
    }
  }
  
  render() {
    const { dispatch, players } = this.props;
    const [playerA, playerB] = Object.values(players);
    return (
      <Wrapper>
        <Row>CHOOSE WISELY</Row>
        <br />
        <Row>
          <PlayerSelection color={playerA.color} move={playerA.move} />
          <PlayerSelection color={playerB.color} move={playerB.move} />
        </Row>
      </Wrapper>
    );
  }
}

const mapStateToProps = (state) => ({
  players: state.players,
});

const mapDispatchToProps = (dispatch) => ({
  dispatch,
});

export default connect(mapStateToProps, mapDispatchToProps)(Picking);
<file_sep>export const SET_STATUS = 'SET_STATUS';
export const SET_PLAYERS = 'SET_PLAYERS';

export const setStatus = (status) => ({ type: SET_STATUS, status });
export const setPlayers = (players) => ({ type: SET_PLAYERS, players });
<file_sep># base image
FROM python:3

# set working directory
WORKDIR /app


# install and cache app dependencies
COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt
COPY . /app/

# start app
CMD ["python", "/app/app.py"]<file_sep>
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import QRCode from 'qrcode.react';
import Connection from '../connection';

import { D_GRAY, DARK_SLATE, WHITE } from '../utils/Colors';

const Wrapper = styled.div`
  display: flex;
  justify-content: center;
  flex-direction: column;
  font-size: 2rem;
  margin-top: 20vh;
  text-align: center;
  color: ${(props) => props.color};

  @media only screen and (max-width: 600px) {
    width: 100%;
  }
`;

const COOL_ADJECTIVES = ['AWESOME', 'GLORIOUS', 'ILLUSTRIOUS', 'MAGNIFICENT'];
const COOL_NOUNS = ['CHAMPION', 'GENIUS', 'PSYCHIC', 'EXPERT'];

const BAD_ADJECTIVES = ['UNLUCKY', 'UNFORTUNATE', 'POOR', 'SORRY'];
const BAD_NOUNS = ['SOD', 'FOOL', 'AMATEUR', 'NOOB'];

const getRandom = (items) => items[Math.floor(Math.random()*items.length)];


const Result = ({ players, playerId }) => {
  const player = players[playerId];
  const isWinner = players[playerId].winner;
  const winningContent = `${getRandom(COOL_ADJECTIVES)} ${getRandom(COOL_NOUNS)}`;

  let losingContent = `${getRandom(BAD_ADJECTIVES)} ${getRandom(BAD_NOUNS)}`;
  if (Math.floor(Math.random()*10) < 3) {
    losingContent = 'THE WRONG SIDE OF A TERNARY OPEARATOR MY FRIEND';
  }
  return (
    <Wrapper color={player.color}>
      {isWinner ? 'WINNER!' : 'LOSER!'}
      <br />
      <br />
      {isWinner ? winningContent : losingContent}
    </Wrapper>
  );
};

const mapStateToProps = (state) => ({
  players: state.players,
  playerId: state.playerId,
});

export default connect(mapStateToProps)(Result);
<file_sep>
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import QRCode from 'qrcode.react';
import * as Actions from '../store/actions';

import { D_GRAY, DARK_SLATE, WHITE } from '../utils/Colors';

const Wrapper = styled.div`
  display: flex;
  justify-content: center;
  flex-direction: column;
  font-size: 2rem;
  margin-top: 5vh;
  color: ${WHITE};

  @media only screen and (max-width: 600px) {
    width: 100%;
  }
`;

const Row = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;

  ${(props) => props.small && `
    font-size: 1.5rem;
  `}}
`;

const WinnernWrapper = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;
  width: 100%;
  padding: 20rem 0;
  background-color: ${(props) => props.color};

  ${(props) => props.small && `
    font-size: 1.5rem;
  `}}
`;

const Move = styled.span`
  color: ${(props) => props.color};
  margin: 0 1rem;
`;

const Picking = ({ players }) => {
  const winner = Object.values(players).find(player => player.winner);
  const loser = Object.values(players).find(player => !player.winner);
  setTimeout(() => location.reload(), 5000)
  return (
    <Wrapper>
      <Row>
        <Move color={winner.color}>{winner.move}</Move> beats <Move color={loser.color}>{loser.move}</Move>
      </Row>
      <br />
      <Row>
        <WinnernWrapper color={winner.color}>
          Player {winner.id} wins!
        </WinnernWrapper>
      </Row>
    </Wrapper>
  );
};

const mapStateToProps = (state) => ({
  players: state.players,
});

const mapDispatchToProps = (dispatch) => ({
  dispatch,
});

export default connect(mapStateToProps, mapDispatchToProps)(Picking);
<file_sep>import eventlet
import socketio

sio = socketio.Server(async_mode='eventlet', cors_allowed_origins='*')
app = socketio.WSGIApp(sio)


@sio.event
def connect(sid, data):
    print('connect ', sid)

@sio.event
def create_game(sid, game_id):
    game_room = f'game-{game_id}'
    print('create_game', sid, game_room)
    sio.enter_room(sid, game_room)

@sio.event
def join_game(sid, game_id, player_id):
    game_room = f'game-{game_id}'
    print('join_game', sid, game_room, player_id)
    sio.enter_room(sid, game_room)
    sio.emit('new_player', player_id, room=game_room)

@sio.event
def update_state(sid, game_id, game_state):
    game_room = f'game-{game_id}'
    sio.emit('new_game_state', game_state, room=game_room, skip_sid=sid)

@sio.event
def new_move(sid, game_id, player_id, move):
    game_room = f'game-{game_id}'
    print('new move', game_room, player_id, move)
    data = {
        'player_id': player_id,
        'move': move
    }
    sio.emit('new_move', data, room=game_room, skip_sid=sid)

@sio.event
def disconnect(sid):
    print('disconnect ', sid)

if __name__ == '__main__':
    eventlet.wsgi.server(eventlet.listen(('', 3000)), app, debug=True)<file_sep># arkade
PoC Rock, Paper, Scissors game that allows phones to join and function as controllers (via websockets)

### [Demo](https://s3.amazonaws.com/www.jdhayford.io/videos/quick-demo.mp4)

![Pic](https://s3.amazonaws.com/www.jdhayford.io/images/arkade-shot.png)

## Components
- master-client
- remote-client
- server

### master-client
This client (React) is meant to serve as the "screen" for the game. Starts with prompt (QR code) for a user to scan to "join".

### remote-cient
This client (React) that one arrives at upon "joining" the game, see above. This serves as the users remote control that they can use to make a selection.

### Server
The websocket server responsible for syncronizing game state between the clients.

## Running Locally

Everything is setup with docker so all you need to do is:

```
docker-compose up
```

After that you can pull up `localhost:5000` for the host screen. To add a player, you click the QR code (when configured and deployed this can just be scanned by your phone).
<file_sep>export const getSession = (state) => state.session;
export const getSessionStatus = (state) => state.sessionStatus;
export const getStatus = (state) => {
  const { players } = state;
  if (Object.keys(players).length == 0) return 'SLEEPING'
  if (Object.keys(players).length < 2) return 'JOINING'
    
  const isWinner = Object.values(players).find(player => player.winner);
  const moves = Object.values(players).map(player => player.move).filter(move => !!move);
  if (isWinner) return 'SHOWING';
  if (moves.length <= 2) return 'PICKING';
  return 'DEFAULT';
}