<|start_filename|>public/poker/host.css<|end_filename|>
body {
    height: 100vh;
    color: white;
    background-color: #404040;
}
.bottomleft {
    opacity: .5;
    position: absolute;
    bottom: 0;
}

.player {
    font-size: 1.3em;
}

.player.showdown {
    font-size: 1.3em;
}

.player.active {
    background-color: #03a9f4;
}
.player.fold {
    color: gray;
}

.player.win {
    background-color: #4CAF50;
}

.row {
    display: flex;
    flex-direction: row;
}

.table > * {
    flex: 1;
    padding: 3px;
}

.community-cards {
    align-items: center;
    justify-content: center;
    margin: 1em;
}

.community-cards > * {
    /* border: 1px solid white; */
    font-size: 3em;
    /* margin-left: 3px; */
    margin-right: .3em;
    /* padding: 3px; */
}

.undealt {
    /* border: 1px dotted white; */
}

.header > * {
    text-transform: uppercase;
}



<|start_filename|>public/poker/poker.js<|end_filename|>
'use strict'
import * as webtendo from '../scripts/webtendo'
import {Card, Hand, Deck} from './libpoker'
import Vue from 'vue'

var then
var names = ['Phillips', 'Ahaltimof', 'Fghulds', 'Argyle', 'Angalope', 'Goofball', 'Lumpy', 'Beefsteak', 'Strongarm']
var currentPlayerIndex = 0
var currentBigBlindIndex = 0
var revealHand = false
const stages = ['Deal', 'Bet', '3', 'Bet', '1', 'Bet', '1', 'Bet', 'Reveal', 'Post', 'Reset']
var deck = new Deck()
var bigBlindIndex = 0
var lastMessageDate = 0
const STARTING_MONEY = 200
const AUTO_BETTING = false

let app = new Vue({
  el: '#app',
  data: {
    stages: stages,
    currentStageIndex: 0,
    players: {},
    currentPlayerId: undefined,
    sharedHand: new Hand([])
  }
})

class Player {
  // commit: number;
  constructor (id) {
    this.name = names.pop()
    this.money = STARTING_MONEY
    this.score = 0
    this.committedBet = 0// this.commit stored in this.committedBet
    this.folded = false// this.fold stored in this.folded
    this.betAlready = false
    this.hand = new Hand([])
    this.finalHand = new Hand([])
    this.id = id
    this.recentWinnings = 0
    this.lastTurnMessage = 0
  }
  notDoneBetting (currentHighestBet) {
    return (!this.finishedBetting(currentHighestBet)) && this.canBet()
  }
  finishedBetting (currentHighestBet) {
    return (this.betAlready && this.committedBet >= currentHighestBet)// you've already matched the current highest bet
  }
  canBet () {
    return !(this.folded// out of the game
           || this.money == 0// you're all-in
           || this.hand.cards.length == 0)// you never got dealt cards
  }
  commitBet (additionalAmount, currentHighestBet) { // this function is also used to commit little and big blinds
    let newBet = 0
    newBet = Math.min(additionalAmount, this.money)// you can bet at most the amount you have
    if (this.money >= currentHighestBet - this.committedBet) { // if you have enough, bid at least the minimum.
      newBet = Math.max(newBet, currentHighestBet - this.committedBet)
    } else { // if you haven't enough, you must go all in
      newBet = this.money
    }
    this.money -= newBet// subtract new bet from funds
    this.committedBet += newBet// update to new committed bet amount
  }

  update (currentHighestBet) {
    if (this.commit !== undefined) { // commit a bet
      this.commitBet(this.commit - this.committedBet, currentHighestBet)
      this.betAlready = true
      delete this.commit// clear the commit command
    } else if (this.fold !== undefined) { // fold this round
      this.folded = true
      delete this.fold// clear the fold command
    }
  }
}

function update (modifier) {
  let ids = Object.keys(app.players)// get a list of player ids
  if (ids.length == 0) {
    currentPlayerIndex = 0
    app.currentPlayerId = undefined
  } else {
    currentPlayerIndex = currentPlayerIndex % ids.length// wrap current player index
    app.currentPlayerId = ids[currentPlayerIndex]
  }

  let currentPlayer = app.players[app.currentPlayerId]
  if (currentPlayer !== undefined) {
    if (AUTO_BETTING) {
      if (currentPlayer.name == 'Beefsteak') { // ||currentPlayer.name=='Strongarm'){
        currentPlayer.fold = 1
      } else {
        currentPlayer.commit = 10// todo: debug auto-betting
      }
    }
    // remind all players whether it is their turn
    if (Date.now() - lastMessageDate > 500) {
      webtendo.broadcast({whoseTurn: currentPlayer.name, minimumBid: getHighestBet()})// send message to the next player saying it's his turn
      // also send all players their hands
      Object.values(app.players).forEach(player => {
        webtendo.sendToClient(player.id, {handText: player.hand.toString()})
      })
      lastMessageDate = Date.now()
    }
  }
  // check the game phase
  // deal -> get new deck and deal two cards to each player
  if (stages[app.currentStageIndex] == 'Deal') {
    // a new deck is made on host start and after reveal
    // deal two cards to each player that does not have cards
    for (let i = 0; i < ids.length; i++) {
      let player = app.players[ids[i]]
      if (player.hand.cards.length == 0) {
        player.hand = new Hand([deck.drawCard(), deck.drawCard()])
        if (i == bigBlindIndex) {
          player.commitBet(2, 0)// pay big blind
        } else if (i == (bigBlindIndex - 1 + ids.length) % ids.length) {
          player.commitBet(1, 0)// pay little blind
        }
      }
    }
    // wait for more players. A bet commit advances to the next stage.
    if (ids.length > 1) { // if there are at least two players
      if (currentPlayer.commit !== undefined || currentPlayer.fold !== undefined) { // see if the current player has committed
        app.currentStageIndex++
      }
    }
  } else if (stages[app.currentStageIndex] == 'Bet') {
    let currentHighestBet = getHighestBet()
    // check if all players have bet, or folded, or have no hand
    let howManyNotDoneBetting = 0
    let stillInGame = 0
    Object.values(app.players).forEach(function (player) {
      if (player.notDoneBetting(currentHighestBet))howManyNotDoneBetting++
      if (player.canBet())stillInGame++
    })
    if (stillInGame == 1) { // if this player is the only one who could bet more,
      if (currentPlayer.committedBet == currentHighestBet) { // and if he has already matched the highest bet, then skip him.
        howManyNotDoneBetting = 0
      }
    }
    if (howManyNotDoneBetting == 0) {
      // set who is betting first next round
      if (app.currentStageIndex == 1) { // in the first betting round, the big blind
        currentPlayerIndex = bigBlindIndex + 1
        app.currentPlayerId = ids[currentPlayerIndex]
      } else {
        currentPlayerIndex = (bigBlindIndex - 1 + ids.length) % ids.length// the little blind is big blind index -1
        app.currentPlayerId = ids[currentPlayerIndex]
      }
      app.currentStageIndex++// move to next phase
      Object.values(app.players).forEach(function (player) { player.betAlready = false })// reset 'already bet' flags
    } else {
      // todo: setTimeout to limit player betting time
      // process commit from current player
      if (currentPlayer.commit !== undefined || currentPlayer.fold !== undefined) {
        currentPlayer.update(currentHighestBet)// process this player's inputs
        // Object.values(players).forEach(function(player){delete player.commit; delete player.fold;});//clear other players' inputs
      }
    }
    // skip a player who is all-in, or folded, or done (somehow)
    if (!currentPlayer.notDoneBetting(currentHighestBet)) {
      currentPlayerIndex++
      app.currentPlayerId = ids[currentPlayerIndex]
    }
  } else if (stages[app.currentStageIndex] == '3' || stages[app.currentStageIndex] == '1') {
    // reveal some cards
    for (let i = 0; i < Number(stages[app.currentStageIndex]); i++) {
      app.sharedHand.cards.push(deck.drawCard())// add a card without resorting the hand
    }
    app.currentStageIndex++// advance to next stage
  } else if (stages[app.currentStageIndex] == 'Reveal') {
    determineWinners()
  } else if (stages[app.currentStageIndex] == 'Reset') {
    let onePlayerHasNoMoney = false
    // dump recent winnings into money
    Object.values(app.players).forEach(function (player) {
      player.money += player.recentWinnings
      player.recentWinnings = 0
      if (player.money == 0)onePlayerHasNoMoney = true
      player.hand = new Hand([])// clear their hand
      player.finalHand = new Hand([])
      player.folded = false
    })
    // if any one player has zero money, dump money into score and reset money
    if (onePlayerHasNoMoney) {
      Object.values(app.players).forEach(player => {
        player.score += player.money
        player.money = STARTING_MONEY
      })
    }

    // reset the deck
    deck = new Deck()
    // clear the shared cards
    app.sharedHand = new Hand([])
    app.currentStageIndex = 0
    // advance the big blind
    bigBlindIndex = (bigBlindIndex + 1) % ids.length
  } else if (stages[app.currentStageIndex] == 'Post') {
    // wait for a player to push commit to move to the next betting round
    if (currentPlayer.commit !== undefined) {
      app.currentStageIndex++
      delete currentPlayer.commit
    }
  }
}
function findSubPotWinners (players) {
  // the players list is sorted by hand value already
  // search the players list to find the first player who has commitBet>0 (there must be at least one)
  // it may not be the 0th player (best hand), since he may be all-in with a small amount, and not participating in this sub-pot
  let winnerList = []
  for (let i = 0; i < players.length; i++) {
    if (players[i].committedBet > 0 && players[i].folded == false) {
      winnerList.push(players[i])
      break
    }
  }
  //* *a player can only win (part of) a subpot if he is invested in it (i.e. committedBet>0).
  let bestHandValue = winnerList[0].finalHand.handValue
  for (let i = 1; i < players.length; i++) {
    if (players[i].finalHand.handValue == bestHandValue && players[i].committedBet > 0 && players[i].folded == false) { winnerList.push(players[i]) }
  }
  return winnerList
}

function findSmallestCommittedBet (players) {
  // the committedBets are not all zero (checked before this function is called)
  // find the smallest committedBet that is not zero
  let smallestCommittedBet = 10000000
  for (let player of players) {
    if (player.committedBet < smallestCommittedBet && player.committedBet !== 0) {
      smallestCommittedBet = player.committedBet
    }
  }
  return smallestCommittedBet
}
function allCommittedBetsAreZero (players) {
  for (let player of players) {
    if (player.committedBet !== 0) return false
  }
  return true
}

function determineWinners () {
  // determine who has the best hand
  let playerList = []// prepare to sort players by hand quality
  Object.values(app.players).forEach(function (player) { // get each player's best possible hand
    if (player.folded == false && player.hand.cards.length > 0) { // you can only win if you have been dealt a hand and have not folded
      let combinedHand = player.hand.cloneAndCombine(app.sharedHand)
      let bestHand = combinedHand.getBestHand()
      player.finalHand = bestHand
      playerList.push(player)
    } else { // if you're not in the game, you will lose your committedBet. Your hand score is -1.
      player.finalHand = new Hand([])
      player.finalHand.handValue = -1
      playerList.push(player)
    }
  })
  // sort by best hand
  playerList.sort(function (a, b) {
    return b.finalHand.handValue - a.finalHand.handValue
  })
  while (!allCommittedBetsAreZero(playerList)) { // until all the committedBets are zero:
    // find the smallest nonzero committedBet. This is the subpot commit.
    let subPotCommit = findSmallestCommittedBet(playerList)
    // get a list of players who tied for first place; these must have nonzero committedBets
    let subPotWinners = findSubPotWinners(playerList)
    // subtract from each committedBet the subpot commit. Increment the subpot.
    let subPot = 0
    for (let player of playerList) {
      if (player.committedBet >= subPotCommit) {
        player.committedBet -= subPotCommit
        subPot += subPotCommit
      }
    }
    // Divide the subpot among the players who tied for first place. Put money in player.recentWinnings.
    for (let winner of subPotWinners) {
      winner.recentWinnings += subPot / subPotWinners.length
    }
  }
  // todo: round recentWinnings down to zero
  for (let player of playerList) {
    player.recentWinnings = Math.floor(player.recentWinnings)
  }
  // go to next stage
  app.currentStageIndex++
  // setTimeout(function(){app.currentStageIndex++;},6000);//wait a bit so people can see the result
}

// The main game loop
function main () {
  var now = Date.now()
  var delta = now - then

  update(delta / 1000)

  then = now

  // Request to do this again in 100 ms (10 Hz).
  window.setTimeout(main.bind(this), 100)
};

function getHighestBet () {
  // find the minimum bet
  let currentHighestBet = 0
  for (let player of Object.values(app.players)) {
    currentHighestBet = Math.max(currentHighestBet, player.committedBet)
  }
  return currentHighestBet
}

webtendo.callbacks.onMessageReceived = function (x) {
  // console.log(x);
  let player = app.players[x.clientId]
  player[x.controlName] = x.controlValue// expects x.commit and x.fold
  // x.commit carries a controlValue which is the next bet amount
  // x.fold does not use the controlValue
}

webtendo.callbacks.onConnected = function (x) {
  let id = x.clientId
  console.log(id, 'connected')
  if (!app.players[id]) {
    app.$set(app.players, id, new Player(id))
  }
  webtendo.sendToClient(id, {hello: app.players[id].name})
}

webtendo.callbacks.onDisconnected = function (x) {
  let id = x.clientId
  console.log(id, 'disconnected')
  // TODO: find out why ios disconnects. maybe just simulator?
  // delete players[id];
};

(function init () {
  // autorun();
  // start game loop
  then = Date.now()
  main()
})()

function autorun () {
  // initialize deck of cards
  deck = new Deck()
  draw = deck.drawCard()
  console.log(draw.toString())

  // test the hand sorting and evaluation
  app.sharedHand = new Hand([new Card(8, 0), new Card(7, 2), new Card(8, 2), new Card(9, 2), new Card(11, 2)])
  var playerHand = new Hand([new Card(10, 2), new Card(12, 2)])
  var combinedHand = playerHand.addSharedCards(app.sharedHand)
  console.log(playerHand.toString())
  console.log(app.sharedHand.toString())
  console.log(combinedHand.toString())
  console.log(hand)
  var sets = hand.combinations()
  for (let set of sets) {
    console.log(set.toString())
  }
  console.log(combinedHand.getBestHand().toString())
  var hand1 = new Hand([new Card(8, 0), new Card(8, 1), new Card(7, 0), new Card(12, 2), new Card(10, 2)])
  var hand2 = new Hand([new Card(9, 0), new Card(9, 1), new Card(7, 0), new Card(11, 2), new Card(10, 2)])
  hand1.assessFiveCardHand()
  hand2.assessFiveCardHand()
  console.log(hand1.toLine())
  console.log(hand2.toLine())
}


<|start_filename|>public/spacewar/spacewar.js<|end_filename|>
// @flow
import * as webtendo from '../scripts/webtendo'

const numCollisionRows: number = 4
const numCollisionColumns = 6

var then
var ctx
var players = {}
var bullets = []
var stars = []
var colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'brown', 'black', 'gray']
var canvas
var collisionSections

class Circle {
  x: number;
  y: number;
  // Radius
  r: number;

  constructor (x, y, r) {
    this.x = x
    this.y = y
    this.r = r
  }

  get sectionIndices () {
    return [sectionForCoordinate(this.x, this.y)]
  }

  intersects (other) {
    return this.distanceTo(other) < this.r + other.r
  }

  distanceTo (other) {
    var a = this.x - other.x
    var b = this.y - other.y
    return Math.sqrt(a*a + b*b)
  }
}

class Star extends Circle {
  constructor () {
    let p = getRandomPosition()
    super(p.x, p.y, 1)
  }

  render (ctx) {
    ctx.beginPath()
    ctx.fillStyle = 'white'
    ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI)
    ctx.fill()
  }
  update (modifier) {
    this.x += 10 * modifier
    this.x %= canvas.offsetWidth
  }
}

class Player extends Circle {
  color: string;
  theta: number;
  // Base 32 random ID.
  id: string;
  score: number;
  speed: number;
  // ** user inputs **
  a: {value: string, action: string};
  b: {value: string, action: string};
  stick: {value: string, action: string, position: {x: number, y:number}};

  constructor (id) {
    let p = getRandomPosition()
    super(p.x, p.y, 15)

    this.color = colors.pop()
    this.theta = Math.random() * Math.PI * 2
    this.score = 0
    this.speed = 100 // px/second
    this.id = id
  }

  render (ctx) {
    for (var i = 0; i < 2; i++) {
      // Triangle
      ctx.beginPath()

      let h = this.r * 2
      ctx.moveTo(this.x + h * Math.cos(this.theta), this.y + h * Math.sin(this.theta))
      ctx.lineTo(this.x + this.r * Math.cos(this.theta - 2*Math.PI/3), this.y + this.r * Math.sin(this.theta - 2*Math.PI/3))
      ctx.lineTo(this.x + this.r * Math.cos(this.theta + 2*Math.PI/3), this.y + this.r * Math.sin(this.theta + 2*Math.PI/3))
      ctx.lineTo(this.x + h * Math.cos(this.theta), this.y + h * Math.sin(this.theta))
      if (i == 0) {
        ctx.strokeStyle = 'white'
        ctx.lineWidth = 2
        ctx.stroke()
      } else {
        ctx.fillStyle = this.color
        ctx.fill()
      }
    }
  }

  update (modifier) {
    if (this.a) { // Boost!
      this.speed = 350
    } else {
      this.speed = 300
    }
    if (this.b) {
      // Fire weapon.
      bullets.push(new Bullet(
        this.x + Math.cos(this.theta) * 35,
        this.y + Math.sin(this.theta) * 35,
        // Force velocity to sum to 1.
        Math.cos(this.theta),
        Math.sin(this.theta),
        // Who to give points for a kill.
        this.id
      ))
      // Consume touch so we only fire 1 bullet per press.
      delete this.b
    }
    if (this.stick) {
      this.x += this.stick.position.x * this.speed * modifier
      this.y += this.stick.position.y * this.speed * modifier
      this.theta = Math.atan2(this.stick.position.y, this.stick.position.x)
    }
    // Bounds.
    this.x = Math.min(Math.max(this.r, this.x), canvas.offsetWidth - this.r)
    this.y = Math.min(Math.max(this.r, this.y), canvas.offsetHeight - this.r)
  }

  respawn () {
    let p = getRandomPosition()
    this.x = p.x
    this.y = p.y
    this.theta = Math.random() * Math.PI * 2
  }
}

class Bullet extends Circle {
  vx: number;
  vy: number;
  // Player.id that fired this bullet.
  owner: string;
  constructor (x, y, vx, vy, owner) {
    super(x, y, 5)

    this.vx = vx
    this.vy = vy
    this.owner = owner
  }

  render (ctx) {
    // Fill circle.
    ctx.beginPath()
    ctx.fillStyle = 'red'
    ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI)
    ctx.fill()
  }

  update (modifier, index) {
    this.x += this.vx * 300 * modifier
    this.y += this.vy * 300 * modifier

    // Handle first collision
    let collided = false
    for (var i of this.sectionIndices) {
      let section = collisionSections[i]

      if (collided) {
        break
      }

      for (var player of section.players) {
        if (this.owner != player.id && this.intersects(player)) {
          collided = true
          players[this.owner].score++
          player.respawn()
          break
        }
      }

      for (var bullet of section.bullets) {
        if (this.owner === bullet.owner) {
          continue
        }
        if (this.intersects(bullet)) {
          collided = true
        }
      }
    }

    // Despawn bullets that have gone off the map.
    // Consider tracking bullet travel distance and capping range.
    let isOutOfBounds = this.x < 0 || this.x > canvas.offsetWidth || this.y < 0 || this.y > canvas.offsetHeight
    if (isOutOfBounds || collided) {
      bullets.splice(index, 1)
    }
  }
}

function blankCollisionSections () {
  var sections = []
  for (var i = 0; i < numCollisionRows * numCollisionColumns; i++) {
    sections.push({
      'players': [],
      'bullets': []
    })
  }
  return sections
}

function createCollisionSections () {
  var newSections = blankCollisionSections()

  for (var i = bullets.length - 1; i >= 0; i--) {
    let bullet = bullets[i]
    bullet.sectionIndices.forEach(function (i) {
      newSections[i]['bullets'].push(bullet)
    })
  }

  for (var id in players) {
    let player = players[id]
    player.sectionIndices.forEach(function (i) {
      newSections[i]['players'].push(player)
    })
  }

  return newSections
}

// Update positions.
function update (modifier) {
  collisionSections = createCollisionSections()

  for (var i = bullets.length - 1; i >= 0; i--) {
    bullets[i].update(modifier, i)
  }
  Object.keys(players).forEach((id: string) => players[id].update(modifier))
  stars.forEach(star => star.update(modifier))
}

// The main game loop
function main () {
  var now = Date.now()
  var delta = now - then

  update(delta / 1000)
  render()

  then = now

  // Request to do this again ASAP
  window.requestAnimationFrame(main)
};

function getRandomPosition () {
  // TODO: also account for obj size
  return {x: Math.random() * canvas.offsetWidth, y: Math.random() * canvas.offsetHeight}
}

// Draw everything
var render = function () {
  // Clear
  ctx.fillStyle = 'black'
  ctx.fillRect(0, 0, canvas.width, canvas.height)

  stars.forEach(star => star.render(ctx))
  bullets.forEach(bullet => bullet.render(ctx))
  Object.keys(players).forEach((id: string) => players[id].render(ctx))

  // Scoreboard
  ctx.fillStyle = 'white'
  ctx.font = '24px Helvetica'
  ctx.textAlign = 'left'
  ctx.textBaseline = 'top'
  // Wow I'm lazy.
  ctx.fillText(JSON.stringify(Object.keys(players).map(
    (id: string) => `${players[id].color}:${players[id].score}`)), 0, 0)
}

webtendo.callbacks.onMessageReceived = function (x) {
  // console.log(x);
  let player = players[x.clientId]
  if (x.action === 'touchend') {
    delete player[x.value]
  } else {
    player[x.value] = x
  }
}

webtendo.callbacks.onConnected = function (x) {
  let id = x.clientId
  console.log(id, 'connected')
  webtendo.sendToClient(id, {hello: 'client'})
  if (!players[id]) {
    players[id] = new Player(id)
  }
}

webtendo.callbacks.onDisconnected = function (x) {
  let id = x.clientId
  console.log(id, 'disconnected')
  // TODO: find out why ios disconnects. maybe just simulator?
  // delete players[id];
}

function sectionForCoordinate (x, y) {
  // TODO: return a list of sections to handle large objs properly.
  x = Math.max(0, Math.min(x, canvas.offsetWidth - 1))
  y = Math.max(0, Math.min(y, canvas.offsetHeight - 1))

  let rowHeight = canvas.offsetHeight / numCollisionRows
  let columnWidth = canvas.offsetWidth / numCollisionColumns

  let row = Math.floor(y / rowHeight)
  let column = Math.floor(x / columnWidth)

  return column + row * (numCollisionColumns)
};

(function init () {
  canvas = (document.getElementById('canvas'): any)
  ctx = canvas.getContext('2d')
  // Awful hack from stackoverflow to increase canvas resolution.
  const ratio = window.devicePixelRatio
  const w = canvas.offsetWidth
  const h = canvas.offsetHeight
  canvas.width = w * ratio
  canvas.height = h * ratio
  canvas.style.width = w + 'px'
  canvas.style.height = h + 'px'
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0)

  for (var i = 0; i < 100; i++) {
    stars.push(new Star())
  }

  then = Date.now()
  main()
})()


<|start_filename|>public/spacewar/client.css<|end_filename|>
body {
    font-family: sans-serif;
    background-color: #404040;
    color:white;
    margin: 0;
    padding: 0;
    height: 100vh;
    width: 100vw;
}

.container {
    justify-content: space-between;
    display: flex;
    height: 100vh;
    width: 100vw;
    align-items: center;
    touch-action: none
}

.fullscreen {
    position:absolute;
    top:0;
    width: 3em;
    background-color:white;
    margin: 4px;
}

button {
    background: none;
    border: 0;
    color: inherit;
    /* cursor: default; */
    font: inherit;
    line-height: normal;
    overflow: visible;
    padding: 0;
    -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
    -webkit-tap-highlight-color: transparent;  /* i.e. Nexus5,Chrome and Kindle Fire HD 7 */
    -webkit-appearance: button; /* for input */
    -webkit-user-select: none; /* for button */
    -moz-user-select: none;
    -ms-user-select: none;
}

#buttonBox {
    /* right: 0; */
    /* position:absolute; */
    /* 100% doesn't account for status bar.... :( */
    height: 100%;
    width:50%;
    display: flex;
    flex-direction: column;
}

.a,
.b {
    display:block;
    width: 100%;
    flex: 1;
    font-size: 3em;
    border-radius: 5px;
}

.a {
    background-color: #3F51B5;
}
.b {
    background-color: #E91E63;
}

button:active {
    opacity: .5;
}

#joystick {
    width: 80vh;
    height: 80vh;
    border-radius: 50%;
    background-color: #2196F3;
}

#stick {
    width: 2rem;
    height: 2rem;
    position:relative;
    top:calc(50% - 15px);
    left: calc(50% - 15px);
    border-radius: 50%;
    background-color: white;
    border: solid black 1px;
}

.ctrl-left {
    display: flex;
    flex: 1;
    justify-content: center;
}

#latency {
    position: absolute;
    bottom: 0;
    margin: 10px;
    opacity: .5;
}


<|start_filename|>public/poker/client.css<|end_filename|>
body {
  font-family: sans-serif;
  background-color: #404040;
  color:white;
  margin: 0;
  padding: 0;
  justify-content: space-between;
  display: flex;
  height: 100vh;
  width: 100vw;
  flex-direction: column;
  font-size: 1.5em;
}

.row {
    display: flex;
    flex-direction: row;
}

.fullscreen {
  position:absolute;
  top:0;
  right: 0;
  width: 2em;
  background-color:white;
  margin: 4px;
}

button {
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    line-height: normal;
    overflow: visible;
    padding: 0;
    -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 
    -webkit-tap-highlight-color: transparent;  // i.e. Nexus5/Chrome and Kindle Fire HD 7''
    -webkit-appearance: button; /* for input */
    -webkit-user-select: none; /* for button */
       -moz-user-select: none;
        -ms-user-select: none;
}

.bigbutton {
    font-size: .5 em;
}

.button {
    background-color: white;
    padding: 1.5em;
    font-size: 1em;
    color: black;
    text-align: center;
    flex: 1;
    margin-right: 1px;
}

.noedit {
    color: white;
    background-color: #404040;
    margin: 0;
}

.label {
    font-size:.7em;
}
.value {
    font-size: 1.3em;
    white-space: nowrap;
}

#latency {
    position: absolute;
    bottom: 0;
    margin: 0px;
    opacity: .5;
}

.commit {
    background-color: hsl(88, 50%, 76%);
}

.fold {
    background-color: hsl(354, 50%, 76%);
}


<|start_filename|>public/scripts/index.js<|end_filename|>
'use strict'

import io from 'socket.io-client'
import $ from 'jquery'
// import * as analytics from './analytics';

let socket = io.connect()
let el = document.getElementById('room')
socket.on('room', room => {
  el.value = room
  window.location.hash = room
})

// Ask server for default room.
if (!window.location.hash) {
  socket.emit('get room')
} else {
  // Strip off leading hash.
  el.value = window.location.hash.slice(1)
}

// Let user manually override.
el.addEventListener('input', e => {
  window.location.hash = e.target.value.toLowerCase()
})

// Preserve hash on navigation.
let links = document.getElementsByTagName('a')
for (let i = 0; i < links.length; i++) {
  let link = links[i]
  if (!link.dataset.game) {
    continue
  }
  let ontouch = e => {
    window.location.pathname = `/${link.dataset.game}/${currentRole}.html`
  }
  // Both mobile and desktop.
  link.addEventListener('touchend', ontouch)
  link.addEventListener('click', ontouch)
}

const clientButton = document.getElementsByClassName('btn-client')[0]
const hostButton = document.getElementsByClassName('btn-host')[0]

let currentRole = 'client'
function changeRole (role) {
  return () => {
    $('button.selected').removeClass('selected')
    if (role === 'client') {
      $('.btn-client').addClass('selected')
    } else if (role === 'host') {
      $('.btn-host').addClass('selected')
    }
    currentRole = role
  }
}

changeRole('client')()

clientButton.addEventListener('touchend', changeRole('client'))
clientButton.addEventListener('click', changeRole('client'))

hostButton.addEventListener('touchend', changeRole('host'))
hostButton.addEventListener('click', changeRole('host'))


<|start_filename|>server.js<|end_filename|>
'use strict'

var os = require('os')
var nodeStatic = require('node-static')
var http = require('http')
var socketIO = require('socket.io')
var hashwords = require('hashwords')

const hasher = hashwords({salt: '~webtendo~'})

function getRoomId (seed) {
  return hasher.hashStr(seed).toLowerCase().split(' ').slice(0, 2).join('-')
}

var file = new nodeStatic.Server('./public')
var app = http.createServer(function (req, res) {
  file.serve(req, res)
}).listen(process.env.PORT || 8080)

var hosts = {}
var clients = {}
var io = socketIO.listen(app)
io.sockets.on('connection', function (socket) {
  var socketClientId
  var hostId
  var clientIP
  var socketRoom
  if (socket.request.headers['x-forwarded-for']) {
    clientIP = socket.request.headers['x-forwarded-for']
    console.log('clientIP', clientIP, 'aka', getRoomId(clientIP))
  }

  // convenience function to log server messages on the client
  function log () {
    var array = ['Message from server:']
    array.push.apply(array, arguments)
    // socket.emit('log', array);
    console.log(array)
  }

  // Message passthrough for signaling.
  socket.on('message', message => {
    log('Client said: ', message)
    if (message.recipient) {
      io.to(clients[message.recipient]).emit('message', message)
    } else {
      io.to(socketRoom).emit('message', message)
    }
  })

  socket.on('get room', () => {
    socket.emit('room', getRoomId(clientIP))
  })

  socket.on('create or join', function (room, clientId, isHost) {
    socketRoom = room || getRoomId(clientIP)
    room = socketRoom
    log('create or join room ' + room + ' from ' + clientId)
    clients[clientId] = socket.id
    socketClientId = clientId
    if (isHost) {
      socket.join(room)
      hosts[room] = clientId
      io.to(room).emit('created', room, clientId)
      return
    }
    let numClients
    if (!io.sockets.adapter.rooms[room]) {
      socket.emit('nohost', room)
      numClients = 0
    } else {
      numClients = io.sockets.adapter.rooms[room].length
    }
    log('Room ' + room + ' now has ' + numClients + ' client(s)')
    if (numClients < 30) {
      log('Client ID ' + socket.id + ' joined room ' + room)
      socket.join(room)
      hostId = hosts[room]
      if (hostId) {
        // Tell host to send an offer.
        io.to(clients[hostId]).emit('joined', room, clientId)
      }
      // Echo back to sender so they know they joined successfully.
      socket.emit('joined', room, clientId)
    } else { // max clients
      socket.emit('full', room)
    }
  })
  socket.on('disconnect', () => {
    if (!socketClientId) {
      return
    }
    console.log(socketClientId, 'disconnected from ', socketRoom)
    io.to(clients[hostId]).emit('disconnected', socketClientId)
  })

  socket.on('ipaddr', function () {
    var ifaces = os.networkInterfaces()
    for (var dev in ifaces) {
      ifaces[dev].forEach(function (details) {
        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
          socket.emit('ipaddr', details.address)
        }
      })
    }
  })

  socket.on('bye', function () {
    console.log('received bye')
  })
})


<|start_filename|>public/scripts/client.js<|end_filename|>
'use strict'

import * as webtendo from './webtendo'
import $ from 'jquery'

/****************************************************************************
 * Public interface
 ****************************************************************************/
// Called whenever body is touched. Args are event and the data-buttonvalue of
// elements with class touch-region, if any.
export var callbacks = {onTouch: undefined}
// Convenience wrapper for sendToClient (and to avoid confusion).
export var sendToHost = function (obj) {
  return webtendo.sendToClient(webtendo.clientId, obj)
}

// Tell user to rotate screen.
export function checkOrientation (orientation = 'landscape') {
  // if (!window.matchMedia(`(orientation: ${orientation})`).matches) {
  if (orientation === 'portrait'
      ? window.innerHeight < window.innerWidth
      : window.innerHeight > window.innerWidth) {
    window.alert('Please rotate your device')
  }
}

// Simple 3 buzz convenience wrapper.
export function vibrate () {
  window.navigator.vibrate([150, 150, 150, 150, 150])
}

/****************************************************************************
 * Private
 ****************************************************************************/

function getRegion (x, y) {
  let regions = document.getElementsByClassName('touch-region')
  let check = (x, left, width) => x >= left && x <= left + width
  for (var i = 0; i < regions.length; i++) {
    let region = regions[i]
    if (check(x, $(region).offset().left, region.offsetWidth) &&
        check(y, $(region).offset().top, region.offsetHeight)) {
      return {value: region.dataset.buttonvalue, el: region}
    }
  }
}

function touchFeedback (region, type) {
  if (region === undefined) {
    return
  } else if (type === 'touchstart') {
    region.el.style.opacity = 0.6
  } else if (type === 'touchend') {
    region.el.style.opacity = 1
  }
}

function handleTouch (e) {
  e.preventDefault()
  // console.log(e);
  let touches = e.changedTouches
  for (var i = 0; i < touches.length; i++) {
    // Any touches elsewhere while a finger is down on the joystick will
    // block input on the rest of the screen.
    let region = getRegion(touches[i].pageX, touches[i].pageY)
    touchFeedback(region, e.type)
    if (region !== undefined && callbacks.onTouch) {
      callbacks.onTouch(e, touches[i], region.value)
    }
    // return true;
  }
}

const CLICK_TYPES = {
  mousedown: 'touchstart',
  mousemove: 'touchmove',
  mouseup: 'touchend'
}

// Compatibility for testing or sad people.
let mouseDown = false
function handleClick (e) {
  e.preventDefault()
  if (e.type === 'mousedown') {
    mouseDown = true
  } else if (e.type === 'mouseup') {
    mouseDown = false
  } else if (e.type === 'mousemove' && !mouseDown) {
    return
  }
  let region = getRegion(e.pageX, e.pageY)
  // console.log(e)
  var copy = {
    type: CLICK_TYPES[e.type],
    target: e.target,
    pageX: e.pageX,
    pageY: e.pageY
  }
  touchFeedback(region, copy.type)
  if (region !== undefined && callbacks.onTouch) {
    callbacks.onTouch(copy, copy, region.value)
  }
}

document.body.addEventListener('touchstart', handleTouch, {passive:false})
document.body.addEventListener('touchmove', handleTouch, {passive:false})
document.body.addEventListener('touchend', handleTouch, {passive:false})
document.body.addEventListener('mousedown', handleClick, {passive:false})
document.body.addEventListener('mouseup', handleClick, {passive:false})
document.body.addEventListener('mousemove', handleClick, {passive:false})

var fullscreenButton = document.getElementById('fullscreen')
if (fullscreenButton) {
  fullscreenButton.addEventListener('touchend', function (e) {
    if (!document.documentElement.webkitRequestFullScreen) {
      return
    }
    document.documentElement.webkitRequestFullScreen()
    if (!window.screen.orientation.type.includes('landscape')) {
      window.screen.orientation.lock('landscape')
            .then(() => console.log('switched to landscape'),
                  err => {
                    console.error(err)
                    window.alert('please rotate device')
                  })
    }
  })
}

webtendo.callbacks.onMessageReceived = function (x) {
  // TODO: do something with message from host.
  console.log(x)
}

webtendo.callbacks.onConnected = function (id) {
  console.log(id, 'connected')
  sendToHost({hello: 'host'})
}


<|start_filename|>public/scrabble/client.css<|end_filename|>
body {
    font-family: sans-serif;
    background-color: #404040;
    color: white;
    margin: 0;
    padding: 0;
    justify-content: space-between;
    display: flex;
    height: 100vh;
    width: 100vw;
    align-items: center;
}

.fullscreen {
    position: absolute;
    top: 0;
    width: 3em;
    background-color: white;
    margin: 4px;
}

.letter-tile {
    background-color: #ffffff;
    color: #000;
    float: left;
    padding: 15px;
    margin: 2px;
    width: 30px;
    height: 30px;
    text-align: center;
}

.letter-tile.selected {
    background-color: #FFEB3B;
}

.letter {
    font-size: 24px;
}

.ctrl-left {
    flex: 1;
}

.info {
    padding-top: 6px;
    font-size: 1.2em;
}

.info > span:first-child {
    font-size: .8em;
    margin-right: 10px;
    text-transform: uppercase;
    color: lightgray;
}

#name::after {
    content: "✎";
    margin-left: 5px;
}

#info {
    display: flex;
    width: 100%;
    flex: 1;
    flex-direction: column;
}

#hand {
    display: block;
    width: 100%;
    flex: 1;
    flex-direction: row;
}

.ctrl-right {
    height: 100%;
    width: 50%;
    display: flex;
    flex-direction: column;
}

.btn-play {
    background-color: #2196F3;
}

.bottom {
    display: flex;
    flex: 1;
    justify-content: center;
    align-items: center;
}

#latency {
    position: absolute;
    bottom: 0;
    margin: 0px;
    opacity: .5;
}

.triangle-up {
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-bottom: 100px solid white;
    position: absolute;
    bottom: 100px;
}

.triangle-down {
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-top: 100px solid white;
    position: absolute;
    top: 100px;
}

.triangle-left {
    width: 0;
    height: 0;
    border-top: 50px solid transparent;
    border-right: 100px solid white;
    border-bottom: 50px solid transparent;
    position: absolute;
    right: 100px;
}

.triangle-right {
    width: 0;
    height: 0;
    border-top: 50px solid transparent;
    border-left: 100px solid white;
    border-bottom: 50px solid transparent;
    position: absolute;
    left: 100px;
}

.dpad.box {
    width: 100px;
    height: 100px;
    margin: 0 auto;
    position: relative;
}

.ctrl-btn {
    flex: 1;
    height: 50px;
    line-height: 50px;
    margin-right: 2px;
    text-transform: uppercase;
    text-align: center;
}

.btn-danger {
    background-color: #F44336;
}


.butterbar
{
    height: auto;
    background-color: #f9edbe;
    width: auto;
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    top: 0px;
    line-height: 200%;
    color: #555;
    padding-left: 5px;
    padding-right: 5px;
    display: none;
    border-radius: 2px;
}


<|start_filename|>public/poker/libpoker.js<|end_filename|>
// @flow
var suitChars = ['\u2664', '\u2661', '\u2662', '\u2667']
var valueChars = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
var handTypes = ['High Card', 'One Pair', 'Two Pair', 'Three', 'Straight', 'Flush', 'Full House', 'Four', 'Straight Flush', 'Royal Flush']
var sets = []

export class Card {
  constructor (value, suit) {
    // this.value: number  = value;
    this.value = value
    this.suit = suit
  }
  higherThan (otherCard) {
    if (this.value > otherCard.value) return true
    if (this.value < otherCard.value) return false
    if (this.suit > otherCard.suit) return true
    if (this.suit < otherCard.suit) return false
    return undefined// if the two cards are the same, higherThan returns undefined.
  }
  toString () {
    return valueChars[this.value] + suitChars[this.suit]
  }
  toNumber () {
    return this.value * 10 + this.suit
  }
}
export class Deck {
  constructor () {
    this.cards = []
    for (let i = 0; i < valueChars.length; i++) {
      for (let j = 0; j < suitChars.length; j++) {
        this.cards.push(new Card(i, j))
      }
    }
  }
  drawCard () { // draw a card
    let drawIndex = Math.floor(Math.random() * this.cards.length)
    let drawnCard = this.cards[drawIndex]
    this.cards.splice(drawIndex, 1)
    return drawnCard
  }
}
export class Hand {
  constructor (cards) {
    this.cards = this.handSort(cards)
    this.handType = undefined
    this.handValue = undefined
  }
  handSort (cards) {
    return cards.sort(
      function (a, b) {
        return a.toNumber() - b.toNumber()
      })
  }
  toString () {
    let outStr = ''
    let card
    for (card of this.cards) {
      outStr += card.toString()
    }
    return outStr
  }
  getHandTypeString () {
    return handTypes[this.handType]
  }
  toLine () {
    return this.toString() + ' ' + handTypes[this.handType] + ' ' + this.handValue
  }

  combinations () { // performs k choose n to get all combinations
    sets = []
    cutAway(this.cards, 0, 5)
    return sets
  }
  getBestHand () {
    let manyHands = this.combinations()// get all combinations of five hands
    // assess all the hands
    for (let hand of manyHands) {
      hand.assessFiveCardHand()
    }
    manyHands.sort(function (a, b) { return b.handValue - a.handValue })// sort to find the best hand
    // for(let hand of manyHands){//print all the hands
    //  console.log(hand.cards.toString()+" "+handTypes[hand.handType]+" "+hand.handValue);
    // }
    return manyHands[0]// return the best hand (ID'd and scored)
  }
  assessFiveCardHand () { // assumes a 5 card hand
    let valueList = []
    // prepare counting array    
    for (let i = 0; i < valueChars.length; i++) {
      valueList.push([i, 0])
    }
    // count multiples
    for (let card of this.cards) {
      valueList[card.value][1]++
    }
    // sort by most occurring, and then by highest value
    valueList.sort(function (a, b) { return b[1] * 100 + b[0] - a[1] * 100 - a[0] })
    // for(let i=0;i<valueList.length;i++)//print the value list
    // console.log(valueList[i]);
    if (valueList[0][1] == 4) { // check if the top entry has 4
      this.handType = handTypes.indexOf('Four')
    } else if (valueList[0][1] == 3) { // check if the top entry has 3
      if (valueList[1][1] == 2) { // check if it's a full house
        this.handType = handTypes.indexOf('Full House')
      } else {
        this.handType = handTypes.indexOf('Three')
      }
    } else if (valueList[0][1] == 2) { // check if the top entry has 2
      if (valueList[1][1] == 2) { // check if it's a two pair
        this.handType = handTypes.indexOf('Two Pair')
      } else { // one pair
        this.handType = handTypes.indexOf('One Pair')
      }
    } else { // high card, flush, straight, etc
      this.handType = handTypes.indexOf('High Card')
      // is it an ace?
      let straight = this.isStraight()
      let flush = this.isFlush()

      // check if top card is an ace
      let topCard = this.cards.slice(-1)[0]
      if (topCard.value == valueChars.indexOf('A')) { // ace is top card
        // straight may have the ace at the bottom
        let lowAce = new Card(-1, topCard.suit)
        let newCards = this.cards.slice(0, 4)// copies array
        newCards.unshift(lowAce)// adds low ace to start of Array
        let lowAceHand = new Hand(newCards)
        let lowStraight = lowAceHand.isStraight()
        straight = straight || lowStraight
        // royal flush has ace as top card
        if (straight && flush) {
          this.handType = handTypes.indexOf('Royal Flush')
        }
      } else { // ace is not top card
        if (straight && flush) {
          this.handType = handTypes.indexOf('Straight Flush')
        } else if (straight) {
          this.handType = handTypes.indexOf('Straight')
        } else if (flush) {
          this.handType = handTypes.indexOf('Flush')
        }
      }
    }
    // compute relative hand score
    this.handValue = 0
    for (let i = 0; i < 5; i++) { this.handValue += Math.pow(20, 5 - i) * valueList[i][0] }
    //  this.handValue+=Math.pow(20,5-i)*valueList[i+1][0];
    this.handValue += Math.pow(20, 6) * this.handType
  }
  isFlush () { // are all the cards the same suit?
    let suit = this.cards[0].suit
    for (let i = 1; i < this.cards.length; i++) {
      if (this.cards[i].suit != suit) {
        return false// a card has a different suit
      }
    }
    return true// all cards same suit
  }
  isStraight () { // are the cards ascending by increments of 1?
    // cards are already in ascending order
    let previousValue = this.cards[0].value
    for (let i = 1; i < this.cards.length; i++) {
      if (this.cards[i].value != previousValue + 1) {
        return false// a card was not in order, not straight
      }
      previousValue = this.cards[i].value
    }
    return true// all cards in order, it's a straight
  }
  cloneAndCombine (newHand) {
    let allCards = this.cards.concat(newHand.cards)
    return new Hand(allCards)// hand constructor will sort the cards
  }
  // todo: join two hands, the player's and the shared hand.
}

function cutAway (subCards, startIndex, targetCount) {
  if (subCards.length == targetCount) {
    sets.push(new Hand(subCards))
  } else {
    for (let i = startIndex; i < subCards.length; i++) {
      let newCards = subCards.slice()// copies array
      newCards.splice(i, 1)// removes item
      cutAway(newCards, i, targetCount)// recurse
    }
  }
}



<|start_filename|>public/css/index.css<|end_filename|>
.bigbutton {
    font-size: 3em;
}

h1 {
    text-align: center;
    font-size: 3em;
    margin-bottom: 1rem;
}
.linklist > a {
    font-size: 2em;
    text-decoration: none;
    background-color: white;
    margin: 10px;
    padding: 10px;
    border-radius: 1em;
    color: blue;
    cursor: pointer;
}

.container {
  text-align: center;
  width: 100%;
  max-width: 50rem;
  margin: 0 auto;
}

.games-section {
  text-align: center;
  clear: both;
}

.room-input {
  -webkit-appearance: none;
  appearance: none;
  border-radius: .25rem;
  border: 1px solid #ddd;
  padding: .5rem .75rem;
  font-size: 1rem;
  color: #00a1d6;
}

.role-options {
  background-color: #fff;
  color: black;
  width: 100%;
  display: flex;
}

.btn-client,
.btn-host {
  padding: .75rem;
  display: block;
  margin: 0 auto;
  width: 50%;
  float: left;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
  cursor: pointer;
}

.btn-client:hover,
.btn-host:hover {
  background-color: #eee;
}

.btn-client:active,
.btn-host:active {
  outline: none;
}

.selected {
  color: #00a1d6;
  font-weight: bold;
}

.selected rect {
  stroke: #00a1d6;
}

.role-options::after {
  clear: both;
}


.game-list {
  list-style-type: none;
  margin: 0;
  padding: 0;
  font-size: 1.5rem;
}

.game-list li {
  width: 33.3333%;
  float: left;
  padding: 0 1rem;
  box-sizing: border-box;
}

.game-list a {
  display: inline-block;
  width: 100%;
  padding: 1rem;
  border-radius: .25rem;
  border: 1px solid #eee;
  box-sizing: border-box;
  cursor: pointer;
}

.game-list a:hover {
  background-color: #eee;
}

.game-list::after {
  clear: both;
  content: '';
  display: block;
}


@media screen and (max-width: 600px) {
  .game-list li {
    float: none;
    width: 100%;
  }
}

.linklist {
    text-align: center;
    display: flex;
    flex-direction: column;
}

.linkbox {
    display: flex;
    align-items: center;
    width:100%;
    justify-content:center;
}

.icon {
    font-size: 1em;
    height: 1em;
    color: black;
    vertical-align: middle;
}

@media (orientation: portrait) {
    .linkbox {
        flex-direction: column;
    }
}

.room {
    display: flex;
    justify-content: center;
    margin-bottom: 1em;
}

.github-fork-ribbon:before {
    background-color: #f80;
}

.share-box {
    display:flex;
    width: 100%;
    justify-content: center;
    flex-direction: row;
    margin-top: 10px;
}

/******* sharingbuttons.io *******/
.resp-sharing-button__link,
.resp-sharing-button__icon {
  display: inline-block
}

.resp-sharing-button__link {
  text-decoration: none;
  color: #fff;
  margin: 0.5em
}

.resp-sharing-button {
  border-radius: 5px;
  transition: 25ms ease-out;
  padding: 0.5em 0.75em;
  font-family: Helvetica Neue,Helvetica,Arial,sans-serif
}

.resp-sharing-button__icon svg {
  width: 1em;
  height: 1em;
  margin-right: 0.4em;
  vertical-align: top
}

.resp-sharing-button--small svg {
  margin: 0;
  vertical-align: middle
}

/* Non solid icons get a stroke */
.resp-sharing-button__icon {
  stroke: #fff;
  fill: none
}

/* Solid icons get a fill */
.resp-sharing-button__icon--solid,
.resp-sharing-button__icon--solidcircle {
  stroke: none;

  fill: #000;
}

.resp-sharing-button {
  background-color: #eee;
}

.resp-sharing-button:hover .resp-sharing-button__icon--solid {
  fill: #fff;
}

.resp-sharing-button--twitter:hover {
  background-color: #55acee
}

.resp-sharing-button--pinterest:hover {
  background-color: #bd081c
}

.resp-sharing-button--facebook:hover {
  background-color: #3b5998
}

.resp-sharing-button--tumblr:hover {
  background-color: #35465C
}

.resp-sharing-button--reddit:hover {
  background-color: #5f99cf
}

.resp-sharing-button--google:hover {
  background-color: #dd4b39
}

.resp-sharing-button--linkedin:hover {
  background-color: #0077b5
}

.resp-sharing-button--email:hover {
  background-color: #777
}

.resp-sharing-button--xing:hover {
  background-color: #1a7576
}

.resp-sharing-button--whatsapp:hover {
  background-color: #25D366
}

.resp-sharing-button--hackernews:hover, .resp-sharing-button--hackernews:focus {
  background-color: #FF6600
}

.resp-sharing-button--vk:hover {
  background-color: #507299
}

.resp-sharing-button--facebook:hover,
.resp-sharing-button--facebook:active {
  background-color: #3b5998;
  border-color: #3b5998;
}

.resp-sharing-button--twitter:hover,
.resp-sharing-button--twitter:active {
  background-color: #55acee;
  border-color: #55acee;
}

.resp-sharing-button--email:hover,
.resp-sharing-button--email:active {
  background-color: #777777;
  border-color: #777777;
}

.resp-sharing-button--pinterest:hover,
.resp-sharing-button--pinterest:active {
  background-color: #bd081c;
  border-color: #bd081c;
}

.resp-sharing-button--linkedin:hover,
.resp-sharing-button--linkedin:active {
  background-color: #0077b5;
  border-color: #0077b5;
}
/*
.resp-sharing-button--reddit {
  background-color: #5f99cf;
  border-color: #5f99cf;
}*/

.resp-sharing-button--reddit:hover,
.resp-sharing-button--reddit:active {
  background-color: #3a80c1;
  border-color: #3a80c1;
}
/*
.resp-sharing-button--hackernews {
  background-color: #FF6600;
  border-color: #FF6600;
}
*/
.resp-sharing-button--hackernews:hover
.resp-sharing-button--hackernews:active {
  background-color: #FB6200;
  border-color: #FB6200;
}


<|start_filename|>public/scrabble/host.css<|end_filename|>
body {
    height: 100vh;
    display: flex;
    flex-direction: row;
    color: white;
    background-color: #404040;
}
canvas {
    height: 620px;
    width: 620px;
}
.bottomleft {
    opacity: .5;
    position: absolute;
    bottom: 0;
}

.sidebar {
    width: 280px;
    height: 100%;
    /* float: left; */
    padding: 10px;
    font-size: 1.2em;
}

.table > * {
    flex: 1;
    padding: 3px;
}

.row {
    display: flex;
    flex-direction: row;
}

.player.active {
    background-color: #03a9f4;
}

.player.win {
    background-color: #4CAF50;
}

.player > .name {
    flex: 1;
}

.player > .score {
    flex: 0;
}


<|start_filename|>public/scrabble/scrabble.js<|end_filename|>
'use strict';

import * as webtendo from '../scripts/webtendo';
import _ from 'underscore';
import $ from 'jquery';
import Vue from 'vue';

var WORDNIK_API_KEY = "<KEY>";
var WORDNIK_BASE_URL = _.template("http://api.wordnik.com:80/v4/word.json/<%= word %>/definitions?limit=2&includeRelated=false&sourceDictionaries=all&useCanonical=false&includeTags=false&api_key=<%= api_key %>");
var ctx;
var gameStarted = false;
var playersArray = []; // play order.
var scrabbleBag = [];
var currentPlayer = 0;
var currentlyPlayedPositions = [];
var board = [];
var colors = ["#FF0000", // red
              "#0000FF", // blue
              "#008000", // green
              "#FFFF00", // yellow
              "#FFA500", // orange
              "#800080", // purple
             ];
var names = ["<NAME>",
             "<NAME>",
             "<NAME>",
             "<NAME>",
             "<NAME>",
             "<NAME>"
            ];
var currentPosition = {x: 7, y: 7};

let app = new Vue({
  el: '#app',
  data: {
    players: {},
    currentPlayerId: undefined,
  },
});


const letterToPointsMap = {
    'E': 1,
    'A': 1,
    'I': 1,
    'O': 1,
    'N': 1,
    'R': 1,
    'T': 1,
    'L': 1,
    'S': 1,
    'U': 1,
    'D': 2,
    'G': 2,
    'B': 3,
    'C': 3,
    'M': 3,
    'P': 3,
    'F': 4,
    'H': 4,
    'V': 4,
    'W': 4,
    'Y': 4,
    'K': 5,
    'J': 8,
    'X': 8,
    'Q': 10,
    'Z': 10,
};

function initScrabbleBag() {
  var scrabbleFrequencies = {
    'E': 12,
    'A': 9,
    'I': 9,
    'O': 8,
    'N': 6,
    'R': 6,
    'T': 6,
    'L': 4,
    'S': 4,
    'U': 4,
    'D': 4,
    'G': 3,
    'B': 3,
    'C': 3,
    'M': 3,
    'P': 3,
    'F': 2,
    'H': 2,
    'V': 2,
    'W': 2,
    'Y': 2,
    'K': 2,
    'J': 1,
    'X': 1,
    'Q': 1,
    'Z': 1,
  };
  for (var character in scrabbleFrequencies) {
    for (var i = 0; i < scrabbleFrequencies[character]; i++) {
      scrabbleBag.push(character);
    }
  }
}

function shuffle(a) {
  for (let i = a.length; i; i--) {
    let j = Math.floor(Math.random() * i);
    [a[i - 1], a[j]] = [a[j], a[i - 1]];
  }
}

function drawCharacters(count) {
  var chars = [];
  for (var i = 0; i < count; i++) {
    if (scrabbleBag.length) {
      chars.push(scrabbleBag.pop());
    }
  }
  return chars;
}

function maybeStartGame() {
  if (gameStarted || Object.keys(app.players).length < 2) {
    return;
  }

  // check that every player is ready
  if (!_.every(_.pluck(Object.values(app.players), 'ready'))) {
    return;
  }

  gameStarted = true;
  initScrabbleBag();
  shuffle(scrabbleBag);
  for (var id in app.players) {
    app.players[id].hand = drawCharacters(7);
    app.players[id].score = 0;
    webtendo.sendToClient(id, app.players[id]);
  }
  setCurrentPlayer(0);
}

function setCurrentPlayer(playerIndex) {
  currentPlayer = playerIndex;
  app.currentPlayerId = playersArray[playerIndex];
  webtendo.sendToClient(playersArray[playerIndex], {message: "Your turn!"});
}

var touchStartTime = 0;
var lastActionTime = 0;
function handleMoveCursor(direction) {
  if (direction === 'L') {
    for (var i = currentPosition.x - 1; i >= 0; i--) {
      if (maybeMoveCursor({x: i, y: currentPosition.y})) { return; }
    }
  } else if (direction === 'R') {
    for (var i = currentPosition.x + 1; i <= 14; i++) {
      if (maybeMoveCursor({x: i, y: currentPosition.y})) { return; }
    }
  } else if (direction === 'D') {
    for (var i = currentPosition.y + 1; i <= 14; i++) {
      if (maybeMoveCursor({x: currentPosition.x, y: i})) { return; }
    }
  } else if (direction === 'U') {
    for (var i = currentPosition.y - 1; i >= 0; i--) {
      if (maybeMoveCursor({x: currentPosition.x, y: i})) { return; }
    }
  }
}

function maybeMoveCursor(pos) {
  if (isAvailable(pos)) {
    moveCursor(pos);
    return true;
  }
  return false;
}

function isAvailable(position) {
  if (_.isUndefined(board[position.x]) || _.isNull(board[position.x])) {
    return true;
  }
  var b = board[position.x][position.y];
  return _.isUndefined(b) || _.isNull(b);
}

function playLetter(letter) {
  // confirm letter is in current player's hand
  var currPlayerID = playersArray[currentPlayer];
  if (!isAvailable(currentPosition)) {
    webtendo.sendToClient(currPlayerID, {error: "This spot is already taken!"});
    return;
  }
  var hand = app.players[currPlayerID].hand;
  if (!_.contains(hand, letter)) {
    webtendo.sendToClient(currPlayerID, {error: "Player played a letter not in hand. How did this happen?"});
    return;
  }
  hand.splice(_.indexOf(hand, letter), 1);
  app.players[currPlayerID].hand = hand;
  if (_.isUndefined(board[currentPosition.x])) {
    board[currentPosition.x] = [];
  }
  board[currentPosition.x][currentPosition.y] = letter;
  currentlyPlayedPositions.push(currentPosition);
  renderBoard();
  webtendo.sendToClient(currPlayerID, app.players[currPlayerID]);
}

function currentTurn(position) {
  return _.some(currentlyPlayedPositions, function(p) {
    return position.x == p.x && position.y == p.y;
  });
}

function calculatePointsVertical(position) {
  var min = position.y;
  var max = position.y;

  while (min > 0 && !isAvailable({x: position.x, y: min - 1})) {
    min -= 1;
  }
  while (max < 14 && !isAvailable({x: position.x, y: max + 1})) {
    max += 1;
  }
  var word = "";
  var points = 0;
  var wordMultiplier = 1;
  if (max - min < 1) {
    // word needs to be longer than 1 letter
    return {word: null, points: 0};
  }
  for (var i = min; i <= max; i++) {
    var p = {x: position.x, y: i};
    word += board[position.x][i];
    var curr = letterToPointsMap[board[position.x][i]];
    if (currentTurn(p)) {
      // include all the multipliers
      var t = getTileTypeForPoints(p);
      curr *= t.letter_mulltiplier;
      wordMultiplier *= t.word_multiplier;
    }
    points += curr;
  }
  points *= wordMultiplier;
  return {word: word, points: points};
}

function calculatePointsHorizontal(position) {
  var min = position.x;
  var max = position.x;

  while (min > 0 && !isAvailable({x: min - 1, y: position.y})) {
    min -= 1;
  }
  while (max < 14 && !isAvailable({x: max + 1, y: position.y})) {
    max += 1;
  }
  var word = "";
  var points = 0;
  var wordMultiplier = 1;
  if (max - min < 1) {
    return {word: null, points: 0};
  }
  for (var i = min; i <= max; i++) {
    var p = {x: i, y: position.y};
    word += board[i][position.y];
    var curr = letterToPointsMap[board[i][position.y]];
    if (currentTurn(p)) {
      var t = getTileTypeForPoints(p);
      curr *= t.letter_mulltiplier;
      wordMultiplier *= t.word_multiplier
    }
    points += curr
  }
  points *= wordMultiplier;
  return {word: word, points: points};
}

function finishPlayerTurn(playerId) {
  var baseErrorMessage = "Your turn was not valid. Try again.";
  var numberOfLettersPlayed = 0;
  for (var i = 0; i < 15; i++) {
    for (var j = 0; j < 15; j++) {
      numberOfLettersPlayed += (isAvailable({x: i, y: j}) ? 0 : 1);
    }
  }
  if (numberOfLettersPlayed == currentlyPlayedPositions.length &&
      (currentlyPlayedPositions.length < 2 || isAvailable({x: 7, y: 7}))) {
    sendResultToClient(playerId, {error: baseErrorMessage, points: -1});
  }
  if (currentlyPlayedPositions.length < 1) {
    sendResultToClient(playerId, {error: baseErrorMessage, points: -1});
  }

  var xs = _.pluck(currentlyPlayedPositions, 'x');
  var ys = _.pluck(currentlyPlayedPositions, 'y');

  if (_.uniq(xs).length != 1 &&
      _.uniq(ys).length != 1) {
    sendResultToClient(playerId, {error: baseErrorMessage, points: -1});
  }

  // TODO: Make sure there're no gaps between the played characters
  if (_.uniq(xs).length == 1) {
    if (_.some(_.range(_.min(ys), _.max(ys) + 1), function(y) {
      return isAvailable({x: xs[0], y: y});
    })) {
      sendResultToClient(playerId, {error: baseErrorMessage, points: -1});
    }
  } else {
    if (_.some(_.range(_.min(xs), _.max(xs) + 1), function(x) {
      return isAvailable({x: x, y: ys[0]});
    })) {
      sendResultToClient(playerId, {error: baseErrorMessage, points: -1});
    }
  }

  var points = 0;
  var words = [];

  var mainWord = {};
  if (_.uniq(xs).length == 1) {
    mainWord = calculatePointsVertical(currentlyPlayedPositions[0]);
  } else {
    mainWord = calculatePointsHorizontal(currentlyPlayedPositions[0]);
  }
  points += mainWord.points;
  words.push(mainWord.word);
  _.each(currentlyPlayedPositions, function(p) {
    var h;
    if (_.uniq(xs).length == 1) {
      h = calculatePointsHorizontal(p);
    } else {
      h = calculatePointsVertical(p);
    }
    points += h.points;
    words.push(h.word);
  });
  words = _.filter(words, function(w) {
    return !_.isNull(w);
  });

  var invalidWords = [];
  var promises = [];
  for (var i = 0; i < words.length; i++) {
    var url = WORDNIK_BASE_URL({word: words[i].toLowerCase(), api_key: WORDNIK_API_KEY});
    var word = words[i];
    promises.push(fetch(url).then(resp => {
      return resp.json().then(json => {
        return {valid: json.length > 0, word: word};
      });
    }));
  }
  Promise.all(promises).then(values => {
    var invalid = _.pluck(_.filter(values, function(v) {
      return !v.valid;
    }), 'word');
    if (invalid.length > 0) {
      sendResultToClient(playerId, {points: -1, error: `Invalid word(s): ${invalid}`});
    } else {
      sendResultToClient(playerId, {points: points});
    }
  });
  //return {points: points};
}

webtendo.callbacks.onMessageReceived = function(x) {
  console.log(x);
  if (x.hello === "host") {
    // hello!
    return;
  }
  if (!gameStarted) {
    if (x.action === "ready") {
      app.players[x.clientId].ready = x.value;
    } else if (x.action === "start") {
      maybeStartGame();
      return;
    }
    if (_.every(_.values(app.players), function(p) {
      return p.ready;
    })) {
      _.each(_.keys(app.players), function(id) {
        webtendo.sendToClient(id, {ready: true});
      });
    }
    return;
  }
  if (x.name) {
    app.players[x.clientId].name = x.name;
    return;
  }

  if (x.clientId != playersArray[currentPlayer]) {
    return;
  }
  if (x.action === "moveCursor") {
    handleMoveCursor(x.value);
  } else if (x.action === "reset") {
    resetTurn(x.clientId);
  } else if (x.action === "play_letter") {
    playLetter(x.value);
  } else if (x.action === "finish_turn") {
    finishPlayerTurn(x.clientId);
  }
}

function resetTurn(playerId, error) {
  // reject all tiles that were played
  var hand = app.players[playerId].hand;
  _.each(currentlyPlayedPositions, function(p) {
    hand.push(board[p.x][p.y]);
    board[p.x][p.y] = null;
  });
  currentlyPlayedPositions = [];
  if (error) {
    webtendo.sendToClient(playerId, {error: error});
  }
  webtendo.sendToClient(playerId, {hand: hand});
  renderBoard();
}

function sendResultToClient(playerId, result) {
  if (result.points < 0) {
    resetTurn(playerId, result.error);
    return;
  }
  app.players[playerId].score = app.players[playerId].score + result.points;
  webtendo.sendToClient(playerId, {points: result.points, score: app.players[playerId].score, message: `You got ${result.points} points!`});
  if (scrabbleBag.length == 0) {
    var winner = _.max(_.keys(app.players), function(k) {
      return app.players[k].score;
    });
    alert(`Winner is ${app.players[winner].name}!`);
    return;
  }

  // draw new tiles
  var hand = app.players[playerId].hand;
  for (var i = 0; scrabbleBag.length > 0 && i < currentlyPlayedPositions.length; i++) {
    hand.push(scrabbleBag.pop());
  }
  app.players[playerId].hand = hand;
  webtendo.sendToClient(playerId, app.players[playerId]);
  currentlyPlayedPositions = [];
  currentPosition = {x: 7, y: 7};
  renderBoard();
  setCurrentPlayer((currentPlayer + 1) % playersArray.length);
}

webtendo.callbacks.onConnected = function(x) {
  let id = x.clientId;
  console.log(id + " connected");
  if (app.players[id]) {
    // player reconnected. give the player their hand
    webtendo.sendToClient(id, app.players[id]);
    return;
  }
  if (gameStarted) {
    webtendo.sendToClient(id, {error: 'Sorry, a game is already in session.'});
    return;
  }
  if (app.players.length >= 6) {
    webtendo.sendToClient(id, {error: 'Sorry, this game is at capacity.'});
    return;
  }
  // else, add the new player!
  var currNumPlayers = Object.keys(app.players).length;
  app.$set(app.players, id, {
    color: colors[currNumPlayers],
    name: names[currNumPlayers],
    id: id,
  });
  webtendo.sendToClient(id, app.players[id]);
  playersArray.push(id);
}

const offsetX = 0;
function moveCursor(position) {
  currentPosition = position;
  renderBoard();
}

function drawBoard() {
  moveCursor({x: 7, y: 7});
  renderBoard();
}

function renderBoard() {
  for (var i = 0; i < 15; i++) {
    for (var j = 0; j < 15; j++) {
      var pos = {x: i, y: j};
      var letter = undefined;
      if (!isAvailable(pos)) {
        letter = board[i][j];
      }
      renderSquare(pos, "white", letter);
    }
  }
  _.each(currentlyPlayedPositions, function(position) {
    renderSquare(position, "#d89c22", board[position.x][position.y]);
  });

  // stroke current position
  renderSquare(currentPosition, "red", isAvailable(currentPosition) ? null : board[currentPosition.x][currentPosition.y]);
}

var TILE_TYPES = {
  REGULAR: {name: null,
            letter_mulltiplier: 1,
            word_multiplier: 1},
  DOUBLE_LETTER: {positions: [{x: 3, y: 0},
                              {x: 0, y: 3},
                              {x: 11, y: 0},
                              {x: 14, y: 3},
                              {x: 0, y: 11},
                              {x: 14, y: 11},
                              {x: 3, y: 14},
                              {x: 11, y: 14},
                              {x: 6, y: 2},
                              {x: 8, y: 2},
                              {x: 7, y: 3},
                              {x: 2, y: 6},
                              {x: 2, y: 8},
                              {x: 3, y: 7},
                              {x: 6, y: 12},
                              {x: 8, y: 12},
                              {x: 7, y: 11},
                              {x: 12, y: 6},
                              {x: 12, y: 8},
                              {x: 11, y: 7},
                              {x: 6, y: 6},
                              {x: 6, y: 8},
                              {x: 8, y: 6},
                              {x: 8, y: 8},
                              ],
                  bg_color: "#a0aec8",
                  letter_mulltiplier: 2,
                  word_multiplier: 1,
                  name: "DOUBLE LETTER SCORE"},
  DOUBLE_WORD: {positions: [{x: 1, y: 1},
                            {x: 2, y: 2},
                            {x: 3, y: 3},
                            {x: 4, y: 4},
                            {x: 13, y: 1},
                            {x: 12, y: 2},
                            {x: 11, y: 3},
                            {x: 10, y: 4},
                            {x: 13, y: 13},
                            {x: 12, y: 12},
                            {x: 11, y: 11},
                            {x: 10, y: 10},
                            {x: 1, y: 13},
                            {x: 2, y: 12},
                            {x: 3, y: 11},
                            {x: 4, y: 10}],
                bg_color: "#e2f5ff",
                letter_mulltiplier: 1,
                word_multiplier: 2,
                name: "DOUBLE WORD SCORE"},
  TRIPLE_LETTER: {positions: [{x: 1, y: 5},
                              {x: 1, y: 9},
                              {x: 13, y: 5},
                              {x: 13, y: 9},
                              {x: 5, y: 1},
                              {x: 9, y: 1},
                              {x: 5, y: 13},
                              {x: 9, y: 13},
                              {x: 5, y: 5},
                              {x: 5, y: 9},
                              {x: 9, y: 5},
                              {x: 9, y: 9}],
                  bg_color: "#7199b0",
                  letter_mulltiplier: 3,
                  word_multiplier: 1,
                  name: "TRIPLE LETTER SCORE"},
  TRIPLE_WORD: {positions: [{x: 0, y: 0},
                            {x: 0, y: 7},
                            {x: 0, y: 14},
                            {x: 7, y: 0},
                            {x: 7, y: 14},
                            {x: 14, y: 0},
                            {x: 14, y: 7},
                            {x: 14, y: 14}],
                bg_color: "#d07b7b",
                letter_mulltiplier: 1,
                word_multiplier: 3,
                name: "TRIPLE WORD SCORE"},
  CENTER: {positions: [{x: 7, y: 7}],
           bg_color: "#ff9bf3",
           name: "DOUBLE WORD SCORE",
           letter_mulltiplier: 1,
           word_multiplier: 2},
}

function getTileTypeForPoints(position) {
  // first check if position is one of the currently played letters
  if (!currentTurn(position)) {
    return TILE_TYPES.REGULAR;
  }
  return getTileType(position);
}

function getTileType(position) {
  var tileTypes = Object.keys(TILE_TYPES);
  for (var i = 0; i < tileTypes.length; i++) {
    var type = TILE_TYPES[tileTypes[i]];
    if (!_.isUndefined(type.positions) && _.some(type.positions, function(p) {
      return position.x == p.x && position.y == p.y;
    })) {
      return type;
    }
  }
  // default..
  return TILE_TYPES.REGULAR;
}

function renderSquare(position, edgeStyle, letter) {
  var tileType = getTileType(position);
  if (!_.isUndefined(letter) && !_.isNull(letter)) {
    // black background
    ctx.fillStyle = "black";
    ctx.fillRect(offsetX + 40 * position.x + 10 + 1, 40 * position.y + 10 + 1, 38, 38);
    // render the character
    ctx.font = "18px Courier New";
    ctx.fillStyle = "white";
    ctx.fillText(letter, offsetX + 40 * position.x + 24 + 1, 40 * position.y + 22 + 1);

    ctx.font = "12px Courier New";
    ctx.fillStyle = "white";
    ctx.fillText(letterToPointsMap[letter], offsetX + 40 * position.x + 24 + 1, 40 * position.y + 33 + 1);
  } else if (tileType == TILE_TYPES.REGULAR) {
    ctx.fillStyle = "#fae8dc";
    ctx.fillRect(offsetX + 40 * position.x + 10 + 1, 40 * position.y + 10 + 1, 38, 38);
  } else {
    ctx.fillStyle = tileType.bg_color;
    ctx.fillRect(offsetX + 40 * position.x + 10 + 1, 40 * position.y + 10 + 1, 38, 38);

    ctx.font = "10px Courier New";
    ctx.fillStyle = "black";
    var words = tileType.name.split(' ');
    if (words.length === 3) {
      for (var i = 0; i < words.length; i++) {
        var w = words[i];
        ctx.fillText(w, offsetX + 40 * position.x + 10 + 1, 40 * position.y + 18 + 10 * i + 1);
      }
    } else if (words.length === 1) {
      ctx.fillText(words[0], offsetX + 40 * position.x + 20 + 1, 40 * position.y + 18 + 20 + 1);
    }
  }
  ctx.beginPath();
  ctx.moveTo(offsetX + 40 * position.x + 10, 40 * position.y + 10);
  ctx.lineTo(offsetX + 40 * position.x + 10 + 40, 40 * position.y + 10);
  ctx.lineTo(offsetX + 40 * position.x + 10 + 40, 40 * position.y + 10 + 40);
  ctx.lineTo(offsetX + 40 * position.x + 10, 40 * position.y + 10 + 40);
  ctx.lineTo(offsetX + 40 * position.x + 10, 40 * position.y + 10);
  ctx.closePath();
  ctx.strokeStyle = edgeStyle;
  ctx.stroke();
}

function main() {
  drawBoard();
}

(function init() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext) {
    ctx = canvas.getContext('2d');
    // Awful hack from stackoverflow to increase canvas resolution.
    const ratio = window.devicePixelRatio;
    let w = canvas.offsetWidth;
    let h = canvas.offsetHeight;
    canvas.width = w * ratio;
    canvas.height = h * ratio;
    canvas.style.width = w + "px";
    canvas.style.height = h + "px";
    ctx.setTransform(ratio, 0, 0, ratio, 0, 0);

    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    main();
  }
})();


<|start_filename|>public/scripts/webtendo.js<|end_filename|>
'use strict'

import io from 'socket.io-client'
import 'webrtc-adapter'

/****************************************************************************
 * Public interface
 ****************************************************************************/

// TODO: make onConnected/Disconnected take an obj
export var callbacks = {
  // Called when a message is received. Host can check message.clientId for sender.
  onMessageReceived: undefined,
  // Called when a data channel opens, passing clientId as argument.
  onConnected: undefined,
  // Called when a data channel closes, passing clientId as argument.
  onDisconnected: undefined
}

// Am I the host?
export var isHost
// My ID.
export var clientId
// Send a message to a particular client.
export function sendToClient (recipientId, obj) {
  try {
    return dataChannels[recipientId].send(JSON.stringify(obj))
  } catch (e) {
    console.log('couldnt send', e, e.stack)
  }
}
// Send a message to all clients.
export function broadcast (obj) {
  return getClients().map(client => sendToClient(client, obj))
}
// Get a list of all the clients connected.
export function getClients () {
  return Object.keys(dataChannels)
}

// Running dev server.
export const IS_LOCAL = window.location.hostname.match(/localhost|127\.0\.0/)

// Measure latency at 1Hz.
const AUTO_PING = false
const VERBOSE = false

/****************************************************************************
 * Initial setup
 ****************************************************************************/

let useBridge = false
var configuration = {
  'iceServers': [
    {
      'urls': [
        'turn:172.16.58.3:19305?transport=udp',
        'turn:[2607:Ffc00:e968:6179::de52:7100]:19305?transport=udp',
        'turn:172.16.58.3:443?transport=tcp',
        'turn:[2fd00:a516:7c1b:17cd:6d81:2137:bd2a:2c5b]:443?transport=tcp'
      ],
      'username': 'CKnUk8IFEgbDVfpkrmUYzc/s6OMT',
      'credential': '<KEY>},
    {'urls': ['stun:stun.l.google.com:19302']}
  ]
}

// Create a random room if not already present in the URL.
isHost = window.location.pathname.includes('host')
var room = window.location.hash.replace('#', '')
// Use session storage to maintain connections across refresh but allow
// multiple tabs in the same browser for testing purposes.
// Not to be confused with socket ID.
// TODO: use localStorage instead for ReactNative?
let storage = IS_LOCAL ? window.sessionStorage : window.localStorage
clientId = storage.getItem('clientId')
if (!clientId) {
  clientId = Math.random().toString(36).substr(2, 10)
  storage.setItem('clientId', clientId)
}
// Allow the same browser to function as client and host even in prod.
clientId += isHost ? 'h' : 'c'
maybeLog()('Session clientId ' + clientId)

/****************************************************************************
 * Signaling server
 ****************************************************************************/

// Connect to the signaling server if we have webrtc access.
let socket
try {
  new RTCSessionDescription()
  socket = io.connect()
  attachListeners(socket)
} catch (e) {
  console.log('No WebRTC detected, checking for ReactNative.')
  setTimeout(() => {
    try {
      WebViewBridge.onMessage = function (stringifiedMessage) {
        // Awful hack since Safari doesn't like parsing "sdp" field.
        let sdp = /"sdp"\:"(:?[\s\S]*)",/g.exec(stringifiedMessage)
        if (sdp) {
          stringifiedMessage = stringifiedMessage.replace(sdp[0], '')
        }
        let x = JSON.parse(stringifiedMessage)
        if (sdp) {
          x.rtcSessionDescription.sdp = sdp[1]
        }

        if (callbacks[x.webrtcAction] !== undefined) {
          let action = x.webrtcAction
          delete x.webrtcAction
          callbacks[action](x)
          if (action === 'onMessageReceived' && x.hello) {
            document.getElementById('latency').innerText = 'Connected'
          }
        } else if (x.webrtcAction == 'signal') {
          sendMessage(x.rtcSessionDescription, x.recipient)
        } else {
          console.warn('unexpected message', x)
        }
      }
      sendToClient = function (recipientId, message) {
        message.recipientId = recipientId
        message.webrtcAction = 'send'
        WebViewBridge.send(JSON.stringify(message))
      }
      console.log('Found ReactNative.')

      socket = io.connect()
      useBridge = true
      attachListeners(socket)
    } catch (e) {
      console.warn('This doesn\'t look like ReactNative')
      // TODO: redirect to app store?
      window.alert('This browser is not supported. Please use Android Chrome or iOS native app.')
    }
  }, 500)
}

// If useBridge is true, use WebViewBridge for WebRTC.
function attachListeners (socket) {
  socket.on('ipaddr', function (ipaddr) {
    maybeLog()('Server IP address is: ' + ipaddr)
    if (isHost) {
      document.getElementById('ip').innerText = 'Clients connect to ' + ipaddr
    }
  })

  socket.on('created', function (room, hostClientId) {
    maybeLog()('Created room', room, '- my client ID is', clientId)
    if (!isHost) {
      // Get dangling clients to reconnect if a host stutters.
      if (useBridge) {
        WebViewBridge.send(JSON.stringify({webrtcAction: 'created', isHost: isHost, clientId: clientId}))
      } else {
        peerConns = {}
        dataChannels = {}
      }
      socket.emit('create or join', room, clientId, isHost)
    }
  })

  socket.on('full', function (room) {
    maybeLog()('server thinks room is full')
    window.alert(`Room ${room} looks full`)
  })

  socket.on('joined', function (room, clientId) {
    maybeLog()(clientId, 'joined', room)
    if (useBridge) {
      WebViewBridge.send(JSON.stringify({webrtcAction: 'joined', configuration: configuration, clientId: clientId, isHost: isHost}))
    } else {
      createPeerConnection(isHost, configuration, clientId)
    }
  })

  socket.on('log', function (array) {
    console.log.apply(console, array)
  })

  socket.on('disconnected', clientId => {
    if (callbacks.onDisconnected) {
      callbacks.onDisconnected({clientId: clientId})
    }
  })

  socket.on('message', signalingMessageCallback)

  socket.on('nohost', room => {
    console.error('No host for', room)
    window.alert(`No host for room ${room}. Please open a host on your laptop.`)
  })

  // Join a room
  socket.emit('create or join', room, clientId, isHost)

  if (IS_LOCAL) {
    socket.emit('ipaddr')
  }
}

/**
 * Send message to signaling server
 */
function sendMessage (message, recipient) {
  var payload = {
    recipient: recipient,
    sender: clientId,
    rtcSessionDescription: message
  }
  maybeLog()('Client sending message: ', payload)
  socket.emit('message', payload)
}

/****************************************************************************
 * WebRTC peer connection and data channel
 ****************************************************************************/

// Map from clientId to RTCPeerConnection.
// For clients this will have only the host.
var peerConns = {}
// dataChannel.label is the clientId of the recipient. useful in onmessage.
var dataChannels = {}

function signalingMessageCallback (message) {
  maybeLog()('Client received message:', message)
  if (useBridge) {
    message.webrtcAction = 'message'
    WebViewBridge.send(JSON.stringify(message))
    return
  }

  var peerConn = peerConns[isHost ? message.sender : clientId]
  // TODO: if got an offer and isHost, ignore?
  if (message.rtcSessionDescription.type === 'offer') {
    maybeLog()('Got offer. Sending answer to peer.')
    peerConn.setRemoteDescription(new RTCSessionDescription(message.rtcSessionDescription), function () {},
                                  logError())
    peerConn.createAnswer(onLocalSessionCreated(message.sender), logError())
  } else if (message.rtcSessionDescription.type === 'answer') {
    maybeLog()('Got answer.')
    peerConn.setRemoteDescription(new RTCSessionDescription(message.rtcSessionDescription), function () {},
                                  logError())
  } else if (message.rtcSessionDescription.type === 'candidate') {
    peerConn.addIceCandidate(new RTCIceCandidate({
      candidate: message.rtcSessionDescription.candidate
    }))
  } else if (message === 'bye') {
    // TODO: cleanup RTC connection?
  }
}

// clientId: who to connect to?
// isHost: Am I the initiator?
// config: for RTCPeerConnection, contains STUN/TURN servers.
function createPeerConnection (isHost, config, recipientClientId) {
  maybeLog()('Creating Peer connection. isHost?', isHost, 'recipient', recipientClientId, 'config:',
             config)
  peerConns[recipientClientId] = new RTCPeerConnection(config)

  // send any ice candidates to the other peer
  peerConns[recipientClientId].onicecandidate = function (event) {
    maybeLog()('icecandidate event:', event)
    if (event.candidate) {
      sendMessage({
        type: 'candidate',
        label: event.candidate.sdpMLineIndex,
        id: event.candidate.sdpMid,
        candidate: event.candidate.candidate
      }, recipientClientId)
    } else {
      maybeLog()('End of candidates.')
    }
  }

  if (isHost) {
    maybeLog()('Creating Data Channel')
    dataChannels[recipientClientId] = peerConns[recipientClientId].createDataChannel(recipientClientId)
    onDataChannelCreated(dataChannels[recipientClientId])

    maybeLog()('Creating an offer')
    peerConns[recipientClientId].createOffer(onLocalSessionCreated(recipientClientId), logError())
  } else {
    peerConns[recipientClientId].ondatachannel = (event) => {
      maybeLog()('ondatachannel:', event.channel)
      dataChannels[recipientClientId] = event.channel
      onDataChannelCreated(dataChannels[recipientClientId])
    }
  }
}

function onLocalSessionCreated (recipientClientId) {
  return (desc) => {
    var peerConn = peerConns[isHost ? recipientClientId : clientId]
    maybeLog()('local session created:', desc)
    peerConn.setLocalDescription(desc, () => {
      maybeLog()('sending local desc:', peerConn.localDescription)
      sendMessage(peerConn.localDescription, recipientClientId)
    }, logError())
  }
}

function onDataChannelCreated (channel) {
  maybeLog()('onDataChannelCreated:', channel)

  channel.onclose = () => {
    if (callbacks.onDisconnected) {
      callbacks.onDisconnected({clientId: channel.label})
    }
  }
  channel.onopen = () => {
    if (callbacks.onConnected) {
      callbacks.onConnected({clientId: channel.label})
    }
    if (AUTO_PING) {
      // As long as the channel is open, send a message 1/sec to
      // measure latency and verify everything works
      var cancel = window.setInterval(() => {
        try {
          channel.send(JSON.stringify({
            action: 'echo',
            time: window.performance.now()
          }))
        } catch (e) {
          console.error(e)

          window.clearInterval(cancel)
        }
      }, 1000)
    } else {
      document.getElementById('latency').innerText = 'Connected'
    }
  }

  channel.onmessage = (event) => {
    // maybeLog()(event);
    var x = JSON.parse(event.data)
    if (x.action === 'echo') {
      x.action = 'lag'
      channel.send(JSON.stringify(x))
    } else if (x.action == 'text') {
      maybeLog()(x.data)
    } else if (x.action == 'lag') {
      var str = 'round trip latency ' + (window.performance.now() - x.time).toFixed(2) + ' ms'
      // maybeLog()(str);
      document.getElementById('latency').innerText = str
    } else if (callbacks.onMessageReceived) {
      if (x.hello) {
        document.getElementById('latency').innerText = 'Connected'
      }
      x.clientId = channel.label
      callbacks.onMessageReceived(x)
    } else {
      maybeLog()('unknown action')
    }
  }
}

/****************************************************************************
 * Aux functions
 ****************************************************************************/

function logError () {
  return console.error
}

function maybeLog () {
  if (VERBOSE) {
    if (useBridge) {
      return function (...args) {
        console.log(args)
        WebViewBridge.send(JSON.stringify({
          webrtcAction: 'log', flat: JSON.stringify(args), log: args}))
      }
    }
    return console.log
  }
  return function () {}
}


<|start_filename|>public/poker/poker-client.js<|end_filename|>
import * as client from '../scripts/client';
import * as webtendo from '../scripts/webtendo';

let bid = 0;
let minimumBid = 0;
let name = 'Connecting...';
let isMyTurn = false;

client.callbacks.onTouch = function(e, touch, region) {
  // Ignore everything other than touchend
  if (e.type !== 'touchend') {
    return;
  }
  console.log(region);
  if (region === 'toggleHand') {
    var handEl = document.getElementById('hand');
    handEl.style.opacity = handEl.style.opacity !== '0' ? '0' : '1';
    document.getElementById('handVerb').innerText = handEl.style.opacity == '0' ? 'VIEW' : 'HIDE';
    return;
  }
  if (region === 'commit' || region === 'fold') {
    if (!isMyTurn)return;
    client.sendToHost({
      controlName: region,
      controlValue: bid,
    });
    document.getElementById('namebox').style.backgroundColor = '#404040';
    updateBid(0);
    isMyTurn = false;
    return;
  }
  if (region !== undefined) {
    let newBid = parseInt(region);
    if (newBid === 0) {//see
      updateBid(minimumBid);
    } else {
      updateBid(newBid + bid);
    }
  }
}

function updateBid(newBid) {
  bid = newBid;
  let text = `$${bid}`;
  if (newBid >= 1e6) {
    text = 'ALL IN';
  }
  document.getElementById('bet').innerText = text;
}

webtendo.callbacks.onMessageReceived = function(x) {
  console.log(x);
  if (x.minimumBid !== undefined) {//turn indicator message
    if (x.whoseTurn == name) {//if it's my turn
      if (x.minimumBid !== minimumBid) {
        bid = x.minimumBid;
      }
      minimumBid = x.minimumBid;
      updateBid(bid);
      document.getElementById('namebox').style.backgroundColor = '#03a9f4';//blue
      if (!isMyTurn) {
        client.vibrate();
      }
      isMyTurn = true;
    } else {
      isMyTurn = false;
      document.getElementById('namebox').style.backgroundColor = '#404040';//gray
    }
  } else if (x.hello!==undefined) {//name sending message
    name = x.hello;
    document.getElementById('name').innerText = name;
  } else if (x.handText !== undefined) {
    document.getElementById('hand').innerText = x.handText;
  }
}

client.checkOrientation();


<|start_filename|>public/css/main.css<|end_filename|>
body {
  font-family: sans-serif;
  margin: 0;
  padding: 0;
  width: 100vw;
}

button {
    background: none;
    border: 0;
    margin-right: 4px;
    color: inherit;
    /* cursor: default; */
    font: inherit;
    line-height: normal;
    overflow: visible;
    padding: 0;
    -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
    -webkit-tap-highlight-color: transparent;  /* i.e. Nexus5/Chrome and Kindle Fire HD 7'' */
    -webkit-appearance: button; /* for input */
    -webkit-user-select: none; /* for button */
    -moz-user-select: none;
    -ms-user-select: none;
}


<|start_filename|>public/spacewar/joystick.js<|end_filename|>
import * as client from '../scripts/client'

var joystick = document.getElementById('joystick')
var stick = document.getElementById('stick')
function moveStick (x, y) {
  let radius = joystick.offsetWidth / 2
  let relativeX = x - radius
  let relativeY = y - radius
  // Check to see if the point is out of bounds and if so, clip.
  let distance = Math.sqrt(Math.pow(relativeX, 2) + Math.pow(relativeY, 2))
  let theta = Math.atan2(relativeY, relativeX)
  let maxDistance = radius - stick.offsetWidth / 2
  if (distance > maxDistance) {
    stick.style.backgroundColor = 'red'
    x = maxDistance * Math.cos(theta) + radius
    y = maxDistance * Math.sin(theta) + radius
  } else {
    stick.style.backgroundColor = 'white'
  }

  let newLeft = x - stick.offsetWidth / 2
  let newTop = y - stick.offsetHeight / 2

  stick.style.left = newLeft + 'px'
  stick.style.top = newTop + 'px'

  return {
    x: Math.min(maxDistance, distance) * Math.cos(theta) / maxDistance,
    y: Math.min(maxDistance, distance) * Math.sin(theta) / maxDistance
  }
}

client.callbacks.onTouch = function (e, touch, region) {
  // In joystick part of screen.
  if (region === 'stick') {
    let position = {x: 0, y: 0}
    if (e.type === 'touchend') {
      resetStick()
    } else {
      position = moveStick(touch.pageX - joystick.offsetLeft, touch.pageY - joystick.offsetTop)
    }
    client.sendToHost({
      action: e.type,
      value: region,
      position: position
    })
  } else {
    // In button part of screen. Ignore moves.
    // TODO: fix highlighting.
    if (e.type !== 'touchmove') {
      client.sendToHost({
        action: e.type,
        value: region
      })
    }
  }
}

// Listen for orientation changes
window.addEventListener('orientationchange', () => {
  window.setTimeout(() => {
    resetStick()
  }, 200)
}, false)

function resetStick () {
  moveStick(joystick.offsetHeight / 2, joystick.offsetWidth / 2)
}

client.checkOrientation()


<|start_filename|>webpack.config.js<|end_filename|>
var path = require('path');

module.exports = {
  entry: {
    'index': './public/scripts/index.js',
    'spacewar-client': './public/spacewar/joystick.js',
    'spacewar-host': './public/spacewar/spacewar.js',
    'poker-client': './public/poker/poker-client.js',
    'poker-host': './public/poker/poker.js',
    'scrabble-client': './public/scrabble/scrabble-client.js',
    'scrabble-host': './public/scrabble/scrabble.js',
  },
  output: {
    path: path.join(__dirname, 'public/out'),
    filename: '[name]-bundle.js'
  },
  devtool: 'cheap-module-eval-source-map',
  module: {
    loaders: [{
      test: /.js$/,
      loader: 'babel-loader'
    }
    ]
  },
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.common.js'
    }
  }
};


<|start_filename|>public/scripts/analytics.js<|end_filename|>
require('autotrack')

export function init () {
  console.log('ga')
  // In the analytics.js tracking snippet
  ga('create', 'UA-88759837-1', 'auto')

  // Start plugins
  ga('require', 'cleanUrlTracker', {
    stripQuery: true,
    indexFilename: 'index.html',
    trailingSlash: 'remove'
  })
  ga('require', 'outboundLinkTracker')
  ga('require', 'urlChangeTracker')
  ga('require', 'pageVisibilityTracker', {
    fieldsObj: {
      nonInteraction: null
    }
  })

  ga('send', 'pageview')
}

init()


<|start_filename|>public/scrabble/scrabble-client.js<|end_filename|>
import * as webtendo from '../scripts/webtendo';
import * as client from '../scripts/client';
import _ from 'underscore';
import $ from 'jquery';

var name = "Your name here";
var letters = [];
var points = 0;
var numTilesLeft = 100;
var playerReady = false;
var everyoneReady = false;
var gameStarted = false;

var letterToPointsMap = {
    'E': 1,
    'A': 1,
    'I': 1,
    'O': 1,
    'N': 1,
    'R': 1,
    'T': 1,
    'L': 1,
    'S': 1,
    'U': 1,
    'D': 2,
    'G': 2,
    'B': 3,
    'C': 3,
    'M': 3,
    'P': 3,
    'F': 4,
    'H': 4,
    'V': 4,
    'W': 4,
    'Y': 4,
    'K': 5,
    'J': 8,
    'X': 8,
    'Q': 10,
    'Z': 10,
};

function updateHand(hand) {
  $("#hand").empty();
  if (hand.length > 0) {
    gameStarted = true;
  }

  _.each(hand, function(character, index) {
    var $letter = createCharacterTile(character);
    $("#hand").append($letter);
  });
  updateInfo();
  updateActionButtons();
}

function createCharacterTile(character) {
  var $tile = $("<div />", {
    class: 'letter-tile touch-region',
  });
  $tile.attr('data-buttonvalue', 'character=' + character);
  var $character = $("<div />", {
    class: 'letter',
  });
  $character.text(character);
  $tile.append($character);

  var $points = $("<div />", {
    class: 'points',
  });
  $points.text(letterToPointsMap[character]);
  $tile.append($points);
  return $tile;
}

function updateInfo() {
  $("#info").empty();
  $("#info").append(`<div class="info touch-region" data-buttonvalue="name"><span>Name</span><span id="name">${name}</span></div>`);
  if (gameStarted) {
    $("#info").append(`
        <div class=info><span>Points</span><span>${points}</span></div>
        <div class=info><span>Tiles left</span><span>${numTilesLeft}</div>
      `);
  } else { // game hasn't started. display player ready state
    if (playerReady) {
      $("#info").append("<div class=info>Waiting for other players..</div>");
    } else {
      $("#info").append("<div class=info>Press Ready when ready to play!</div>");
    }
  }
}

function updateActionButtons() {
  $("#action_buttons").empty();
  if (gameStarted) { //$("#hand").children().length > 0) {
    // play buttons
    $("#action_buttons").append(" \
        <div class='ctrl-btn btn-danger touch-region' data-buttonvalue='reset=true' aria-hidden='true'>UNDO</div> \
        <div class='ctrl-btn btn-play touch-region' data-buttonvalue='play=true' aria-hidden='true'>PLAY</div> \
        <div class='ctrl-btn btn-play touch-region' data-buttonvalue='submit=true'>Done</div>");
  } else if (everyoneReady) {
    $("#action_buttons").append(" \
        <button class='ctrl-btn btn-play touch-region' data-buttonvalue='start=true'>Start Game!</button>");
  } else {
    $("#action_buttons").append(" \
        <button class='ctrl-btn btn-play touch-region' data-buttonvalue='ready=true'>Ready</button>");
  }
}
updateInfo();
updateActionButtons();

webtendo.callbacks.onMessageReceived = function(x) {
  console.log(x);

  if (x.error) {
    showMessage(x.error);
    return;
  }

  if (x.ready) {
    everyoneReady = true;
    updateActionButtons();
  }

  if (x.hand) {
    // render new hand
    updateHand(x.hand);
  }

  if (x.name) {
    name = x.name;
    updateInfo();
  }

  if (x.points) {
    // TODO points animation
    points = x.score;
    updateInfo();
  }

  if (x.message) {
    client.vibrate();
    showMessage(x.message);
  }
}

function showMessage(message) {
  var butterbar = $(`<div class="butterbar">${message}</div>`);
  $("body").prepend(butterbar);
  butterbar.slideDown("slow");
  setTimeout(function() {
    butterbar.slideUp("slow", function() {
      butterbar.remove();
    });
  }, 2000);
}

function setName() {
  let newName = prompt('Edit name');
  if (newName) {
    newName = newName.trim();
  }
  if (newName.length > 40) {
    alert('Pick a shorter name!');
    return;
  } else if (!newName) {
    return;
  }
  client.sendToHost({name: newName});
  $('#name').text(newName);  
}

var dpad = document.getElementById('dpad');

client.callbacks.onTouch = function(e, touch, region) {
  if (region.indexOf('dpad-') === 0) {
    if (e.type === 'touchend') {
      client.sendToHost({
        action: "moveCursor",
        value: region.substring(5),
      });
    }
  } else {
    // character tiles part of screen
    if (e.type !== 'touchend' || region === '') {
      return;
    }
    if (region === 'name') {
      setName();
      return;
    }
    // which character?
    var params = {};
    _.each(region.split(','), function(a) {
      var b = a.split('=');
      params[b[0]] = b[1];
    });

    if (params['ready']) {
      client.sendToHost({
        action: 'ready',
        value: true
      });
      playerReady = true;
      updateInfo();
    } else if (params['character']) {
      selectCharacter(e, params['character']);
    } else if (params['reset']) {
      client.sendToHost({
        action: 'reset',
        value: true,
      });
      return;
    } else if (params['play']) {
      if (_.isUndefined(selectedCharacter) || _.isNull(selectedCharacter)) {
        alert("Please select a character first!");
        return;
      }
      client.sendToHost({
        action: 'play_letter',
        value: selectedCharacter
      });
      unselectAllCharacters();
      selectedCharacter = null;
    } else if (params['start']) {
      client.sendToHost({
        action: 'start',
        value: true,
      });
    } else if (params['submit']) {
      client.sendToHost({
        action: 'finish_turn',
        value: true
      })
    }
  }
}

function unselectAllCharacters() {
  _.each($("#hand").children(), function(charDiv) {
    $(charDiv).removeClass("selected");
  });
}
var selectedCharacter;
function selectCharacter(e, character) {
  unselectAllCharacters();
  selectedCharacter = character;
  $(e.target).closest('.letter-tile').toggleClass('selected');
}
