Browse Source

WebSocket Based GamePlay; with Game Win History (only data no UI yet)

master
Levi Olson 5 years ago
parent
commit
3961168538
11 changed files with 415 additions and 2242 deletions
  1. +9
    -9
      cache/cache.go
  2. +5
    -22
      cache/structs.go
  3. +5
    -1820
      dist/bundle.js
  4. +4
    -7
      dist/index.html
  5. +0
    -6
      dist/join.html
  6. +13
    -14
      dist/main.css
  7. +27
    -57
      main.go
  8. +152
    -48
      src/classes.js
  9. +179
    -237
      src/index.js
  10. +2
    -6
      src/services.js
  11. +19
    -16
      src/style.scss

+ 9
- 9
cache/cache.go View File

@ -35,7 +35,7 @@ func newCache(de time.Duration, m map[string]Item) *cache {
// Add an item to the cache, replacing any existing item. If the duration is 0 // Add an item to the cache, replacing any existing item. If the duration is 0
// (DefaultExpiration), the cache's default expiration time is used. If it is -1 // (DefaultExpiration), the cache's default expiration time is used. If it is -1
// (NoExpiration), the item never expires. // (NoExpiration), the item never expires.
func (c *cache) Set(k string, x Game, d time.Duration) {
func (c *cache) Set(k string, x Series, d time.Duration) {
// "Inlining" of set // "Inlining" of set
var e int64 var e int64
if d == DefaultExpiration { if d == DefaultExpiration {
@ -46,7 +46,7 @@ func (c *cache) Set(k string, x Game, d time.Duration) {
} }
c.mu.Lock() c.mu.Lock()
c.items[k] = Item{ c.items[k] = Item{
Game: x,
Series: x,
Expiration: e, Expiration: e,
} }
// TODO: Calls to mu.Unlock are currently not deferred because defer // TODO: Calls to mu.Unlock are currently not deferred because defer
@ -54,7 +54,7 @@ func (c *cache) Set(k string, x Game, d time.Duration) {
c.mu.Unlock() c.mu.Unlock()
} }
func (c *cache) set(k string, x Game, d time.Duration) {
func (c *cache) set(k string, x Series, d time.Duration) {
var e int64 var e int64
if d == DefaultExpiration { if d == DefaultExpiration {
d = c.defaultExpiration d = c.defaultExpiration
@ -63,35 +63,35 @@ func (c *cache) set(k string, x Game, d time.Duration) {
e = time.Now().Add(d).UnixNano() e = time.Now().Add(d).UnixNano()
} }
c.items[k] = Item{ c.items[k] = Item{
Game: x,
Series: x,
Expiration: e, Expiration: e,
} }
} }
// Add an item to the cache, replacing any existing item, using the default // Add an item to the cache, replacing any existing item, using the default
// expiration. // expiration.
func (c *cache) SetDefault(k string, x Game) {
func (c *cache) SetDefault(k string, x Series) {
c.Set(k, x, DefaultExpiration) c.Set(k, x, DefaultExpiration)
} }
// Get an item from the cache. Returns the item or nil, and a bool indicating // Get an item from the cache. Returns the item or nil, and a bool indicating
// whether the key was found. // whether the key was found.
func (c *cache) Get(k string) (Game, bool) {
func (c *cache) Get(k string) (Series, bool) {
c.mu.RLock() c.mu.RLock()
// "Inlining" of get and Expired // "Inlining" of get and Expired
item, found := c.items[k] item, found := c.items[k]
if !found { if !found {
c.mu.RUnlock() c.mu.RUnlock()
return item.Game, false
return item.Series, false
} }
if item.Expiration > 0 { if item.Expiration > 0 {
if time.Now().UnixNano() > item.Expiration { if time.Now().UnixNano() > item.Expiration {
c.mu.RUnlock() c.mu.RUnlock()
return item.Game, false
return item.Series, false
} }
} }
c.mu.RUnlock() c.mu.RUnlock()
return item.Game, true
return item.Series, true
} }
type keyAndValue struct { type keyAndValue struct {

+ 5
- 22
cache/structs.go View File

@ -9,39 +9,22 @@ import (
// Item for caching // Item for caching
type Item struct { type Item struct {
Game Game
Series Series
Expiration int64 Expiration int64
} }
// Game object for binding with JSON POST body
type Game struct {
// Series object for binding with JSON POST body
type Series struct {
ID string `json:"id"` ID string `json:"id"`
Player1 *Player `json:"player1"`
Player2 *Player `json:"player2"`
Turn *uuid.UUID `json:"turn,omitempty"` Turn *uuid.UUID `json:"turn,omitempty"`
Draw *bool `json:"draw,omitempty"`
Winner *uuid.UUID `json:"winner,omitempty"`
Matrix Matrix `json:"matrix"` Matrix Matrix `json:"matrix"`
Tally []*Tally `json:"tally,omitempty"`
NextGame string `json:"next_game"`
PrevGame string `json:"previous_game"`
}
// Tally is a log of games won
type Tally struct {
Player Player `json:"player"`
Matrix Matrix `json:"matrix"`
Tally []*Matrix `json:"tally,omitempty"`
LogTally *Matrix `json:"log_match,omitempty"`
} }
// Matrix is the game board and player uuids // Matrix is the game board and player uuids
type Matrix [9]*uuid.UUID type Matrix [9]*uuid.UUID
// Player object for binding with JSON POST body
type Player struct {
UUID uuid.UUID `json:"id"`
Name string `json:"name,omitempty"`
}
// Cache is a simple in-memory cache for storing things // Cache is a simple in-memory cache for storing things
type Cache struct { type Cache struct {
*cache *cache

+ 5
- 1820
dist/bundle.js
File diff suppressed because it is too large
View File


+ 4
- 7
dist/index.html View File

@ -10,8 +10,10 @@
<body> <body>
<div class=wrapper> <div class=wrapper>
<h1>TicTacToe</h1> <h1>TicTacToe</h1>
<h2 id=player class=hide></h2>
<div id=opponent></div>
<div id="message"></div>
<div id=status></div> <div id=status></div>
<div id="tally"></div>
<div id=gameTable class=hide> <div id=gameTable class=hide>
<table> <table>
<tr> <tr>
@ -31,12 +33,7 @@
</tr> </tr>
</table> </table>
</div> </div>
<form id=startGameForm>
<label for=name id=nameLabel>To start a new game, enter your name:</label>
<input type=text name=name id=name placeholder="Enter your name/nickname" size=64>
<button type=submit id=start>Start a new game</button>
</form>
<div id=instructions class=hide>
<div id=instructions>
<p>Share this link with a friend to begin:</p> <p>Share this link with a friend to begin:</p>
<div class=link> <div class=link>
<div class=tooltip> <div class=tooltip>

+ 0
- 6
dist/join.html View File

@ -10,7 +10,6 @@
<body> <body>
<div class=wrapper> <div class=wrapper>
<h1>TicTacToe</h1> <h1>TicTacToe</h1>
<h2 id=player class=hide></h2>
<div id=status></div> <div id=status></div>
<div id=gameTable class=hide> <div id=gameTable class=hide>
<table> <table>
@ -31,11 +30,6 @@
</tr> </tr>
</table> </table>
</div> </div>
<form id=joinGameForm>
<label for=name id=nameLabel>To join, enter your name:</label>
<input type=text name=name id=name placeholder="Enter your name/nickname" size=64>
<button type=submit id=join>Join Game</button>
</form>
</div> </div>
<script src="/static/bundle.js"></script> <script src="/static/bundle.js"></script>
</body> </body>

+ 13
- 14
dist/main.css View File

@ -13,20 +13,14 @@ h2 {
.record #log, .record #inprogress { .record #log, .record #inprogress {
font-size: 10px; } font-size: 10px; }
form {
padding-top: 20px; }
form input#name {
font: normal normal normal 14px/30px 'Lato', sans-serif;
height: 30px;
display: block; }
form button#start, form button#join {
font: normal normal normal 14px/30px 'Lato', sans-serif;
margin: 20px 0;
height: 30px;
background: #4d16e4;
border: none;
border-radius: 2px;
color: white; }
button#start {
font: normal normal normal 14px/30px 'Lato', sans-serif;
margin: 20px 0;
height: 30px;
background: #4d16e4;
border: none;
border-radius: 2px;
color: white; }
input#shareLink { input#shareLink {
font-family: monospace; font-family: monospace;
@ -39,6 +33,11 @@ input#shareLink {
#status { #status {
font: normal normal bold 16px/20px 'Lato', sans-serif; } font: normal normal bold 16px/20px 'Lato', sans-serif; }
#tally .gameTable tr td {
height: 10px;
width: 10px;
content: ""; }
#gameTable tr td { #gameTable tr td {
border-bottom: 1px solid #333; border-bottom: 1px solid #333;
border-right: 1px solid #333; border-right: 1px solid #333;

+ 27
- 57
main.go View File

@ -3,7 +3,6 @@ package main
import ( import (
"errors" "errors"
"log" "log"
"math/rand"
"net/http" "net/http"
"os" "os"
Cache "tictactoe-api/cache" Cache "tictactoe-api/cache"
@ -21,16 +20,6 @@ func setupRender() multitemplate.Renderer {
return r return r
} }
const charBytes = "abcdefghijkmnopqrstuvwxyz023456789"
func generateRandom(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = charBytes[rand.Int63()%int64(len(charBytes))]
}
return string(b)
}
func main() { func main() {
hub := newHub() hub := newHub()
go hub.run() go hub.run()
@ -67,73 +56,54 @@ func main() {
}) })
router.POST("/game", func(c *gin.Context) { router.POST("/game", func(c *gin.Context) {
var game Cache.Game
err := c.BindJSON(&game)
var series Cache.Series
err := c.BindJSON(&series)
if err != nil { if err != nil {
log.Printf("Error Binding Request %s\n", err.Error()) log.Printf("Error Binding Request %s\n", err.Error())
} }
count := 0
generate:
game.ID = generateRandom(6)
_, found := cache.Get(game.ID)
if found {
count = count + 1
log.Printf("GAME FOUND, trying again: %s\n", game.ID)
if count >= 3 {
err = errors.New("Could not generate a new game (too many games in progress)")
} else {
goto generate
}
if len(series.ID) == 0 {
err = errors.New("id is required")
} }
if err != nil { if err != nil {
c.JSON(http.StatusConflict, gin.H{
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(), "error": err.Error(),
}) })
} else {
game.Turn = &game.Player1.UUID
cache.Set(game.ID, game, 0)
c.JSON(http.StatusOK, game)
return
} }
cache.Set(series.ID, series, 0)
c.JSON(http.StatusOK, series)
}) })
router.GET("/game/:gameID", func(c *gin.Context) {
gameID := c.Params.ByName("gameID")
game, found := cache.Get(gameID)
router.GET("/game/:seriesID", func(c *gin.Context) {
seriesID := c.Params.ByName("seriesID")
series, found := cache.Get(seriesID)
if !found { if !found {
c.Status(http.StatusNotFound) c.Status(http.StatusNotFound)
return return
} }
c.JSON(http.StatusOK, game)
c.JSON(http.StatusOK, series)
}) })
router.POST("/game/:gameID", func(c *gin.Context) {
gameID := c.Params.ByName("gameID")
game, found := cache.Get(gameID)
router.POST("/game/:seriesID", func(c *gin.Context) {
seriesID := c.Params.ByName("seriesID")
series, found := cache.Get(seriesID)
if !found { if !found {
c.Status(http.StatusNotFound) c.Status(http.StatusNotFound)
return return
} }
var updateGame Cache.Game
err := c.BindJSON(&updateGame)
var updateSeries Cache.Series
err := c.BindJSON(&updateSeries)
if err != nil { if err != nil {
log.Printf("Error: %s\n", err) log.Printf("Error: %s\n", err)
} }
if updateGame.ID != "" {
game.ID = updateGame.ID
if updateSeries.Turn != nil {
series.Turn = updateSeries.Turn
} }
if updateGame.Player1 != nil {
game.Player1 = updateGame.Player1
}
if updateGame.Player2 != nil {
game.Player2 = updateGame.Player2
}
if updateGame.Turn != nil {
game.Turn = updateGame.Turn
}
if (Cache.Matrix{}) != updateGame.Matrix {
game.Matrix = updateGame.Matrix
if (Cache.Matrix{}) != updateSeries.Matrix {
series.Matrix = updateSeries.Matrix
} }
// if updateGame.Draw != nil { // if updateGame.Draw != nil {
// game.Draw = updateGame.Draw // game.Draw = updateGame.Draw
@ -141,11 +111,11 @@ func main() {
// if updateGame.Winner != nil { // if updateGame.Winner != nil {
// game.Winner = updateGame.Winner // game.Winner = updateGame.Winner
// } // }
// if updateGame.Tally != nil {
// game.Tally = append(game.Tally, updateGame.Tally[0])
// }
cache.Set(gameID, game, 0)
c.JSON(http.StatusOK, game)
if updateSeries.LogTally != nil {
series.Tally = append(series.Tally[1:], updateSeries.LogTally)
}
cache.Set(seriesID, series, 0)
c.JSON(http.StatusOK, series)
}) })
// //

+ 152
- 48
src/classes.js View File

@ -1,20 +1,28 @@
'use strict' 'use strict'
import * as HTTP from './services.js'
import { timingSafeEqual } from 'crypto';
/**
* Logging
* Set to true to enable console.debug() messages
*/
const Logging = true
class BaseUtils { class BaseUtils {
constructor() {} constructor() {}
generateRandom (len) {
let chars = "023456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghikmnopqrstuvwxyz"
let randomstring = ''
for (var i = 0; i < len; i++) {
var rnum = Math.floor(Math.random() * chars.length)
randomstring += chars.substring(rnum, rnum + 1)
}
return randomstring
}
getUUID () { getUUID () {
function s4 () { function s4 () {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
} }
return (s4() + s4() + '-' + s4() + '-4' + s4().slice(1) + '-8' + s4().slice(1) + '-' + s4() + s4() + s4()) return (s4() + s4() + '-' + s4() + '-4' + s4().slice(1) + '-8' + s4().slice(1) + '-' + s4() + s4() + s4())
} }
setId (id) {
this.id = id
return this
}
setCookie (cname, cvalue) { setCookie (cname, cvalue) {
document.cookie = cname + "=" + cvalue + ";path=/" document.cookie = cname + "=" + cvalue + ";path=/"
return this return this
@ -34,81 +42,177 @@ class BaseUtils {
} }
return "" return ""
} }
log(msg) {
if (Logging) {
if (msg) {
console.debug("Method " + msg + "()", this)
} else {
console.debug(this)
}
}
}
} }
/**
* Player object
*
* @export
* @class Player
* @property {string} ID - Unique player identifier
* @extends {BaseUtils}
*/
export class Player extends BaseUtils { export class Player extends BaseUtils {
constructor(id, name) {
constructor() {
super() super()
// if `id` and `name` are passed in, we don't want to set cookies
if (id) {
this.id = id
} else {
this.setId(id || this.getCookie("player_id") || this.getUUID())
}
if (name) {
this.name = name
} else {
this.setName(name || this.getCookie("player_name"))
}
this.setId(this.getCookie("player_id") || this.getUUID())
this.log()
return this return this
} }
getId () { getId () {
return this.id.toLowerCase()
return this.ID.toLowerCase()
} }
setId (id) { setId (id) {
this.id = id
this.ID = id
this.setCookie("player_id", id) this.setCookie("player_id", id)
this.log("setId")
return this return this
} }
getName () {
return this.name
getName() {
return this.getCookie("player_name")
} }
setName (name) {
this.name = name
setName(name) {
this.setCookie("player_name", name) this.setCookie("player_name", name)
return this
} }
} }
export class Game extends BaseUtils {
export class Series extends BaseUtils {
constructor() { constructor() {
super() super()
this.setId((new URLSearchParams(window.location.search)).get('id'))
this.player1 = undefined // person who started the game
this.player2 = undefined // person who joined the game
this.winner = undefined // player.id
this.draw = undefined // true/false
this.matrix = [
null, null, null, // player.id
this.setId((new URLSearchParams(window.location.search)).get('sid') || this.generateRandom(6))
this.gameMatrix = [
null, null, null,
null, null, null, null, null, null,
null, null, null null, null, null
] ]
this.next_game = undefined
this.turn = undefined
this.tally = []
this.log()
return this return this
} }
getId() {
getId () {
return this.id return this.id
} }
setId(id) {
console.debug("Set Game ID", id)
setId (id) {
if (id) if (id)
history.replaceState(null, "", "?id=" + id)
history.replaceState(null, "", "?sid=" + id)
this.id = id this.id = id
this.log("setId")
return this
}
getGameMatrix () {
return this.gameMatrix
}
setGameMatrix (matrix) {
this.gameMatrix = matrix
return this return this
} }
setTurn (player_id) {
console.debug("Set Game Turn", player_id)
emptyGameMatrix() {
this.gameMatrix = [
null, null, null,
null, null, null,
null, null, null
]
}
getTurn() {
return this.turn
}
setTurn(player_id) {
console.debug("Set Player Turn", player_id)
this.turn = player_id this.turn = player_id
this.log("setTurn")
return this
}
logGame() {
this.tally.push(this.gameMatrix)
this.log("logGame")
}
}
/**
* Payload for use with websocket
*
* @export
* @class Payload
* @extends {BaseUtils}
* @typedef {string} PlayerID
* @property {string} SeriesID - Taken from the URL query param ?sid
* @property {PlayerID} Sender - The sender of the socket payload
* @property {EventEnum} Event - The payload event type
* @property {PlayerID[]} Matrix - An array of length 9 containing PlayerIDs or null
* @property {string} Message - Chat message
* @property {Object} Data - Any additional payload
*/
export class Payload extends BaseUtils {
constructor(series_id, player_id) {
super()
if (!series_id || !player_id) {
throw "SeriesID and PlayerID are required to create a new WebSocket Payload"
}
this.SeriesID = series_id
this.Sender = player_id
this.log()
return this
}
/**
* Set the payload event type
* @param {EventType} event - Event type
* @returns Payload object
*/
setEventType(event) {
this.Event = event
this.log("setEventType")
return this return this
} }
getOpponent() {
let player_id = this.getCookie("player_id")
return this.player1.id == player_id ? this.player2 : this.player1
/**
* Set the payload game matrix array
* @param {PlayerID[]} matrix - An array of length 9 containing PlayerIDs or null
* @returns Payload object
*/
setMatrix(matrix) {
this.Matrix = matrix
this.log("setMatrix")
return this
} }
setOpponentsTurn() {
let opponent = this.getOpponent()
this.setTurn(opponent.id)
/**
* Set the payload chat message
* @param {string} message - Chat Message
* @returns Payload object
*/
setMessage(message) {
this.Message = message
this.log("setMessage")
return this return this
} }
logResult(winnersName) {
this.winners.push(winnersName)
/**
* Exports the payload using JSON.stringify()
* @returns {string} Serialized payload object
*/
serialize() {
return JSON.stringify({
SeriesID: this.SeriesID,
Sender: this.Sender,
Event: this.Event,
Matrix: this.Matrix,
Message: this.Message,
})
} }
}
export const EventEnum = {
ANYBODYHOME: 1,
IAMHERE: 2,
CHAT: 3,
SNAP: 4,
MOVE: 5,
NEW: 6,
} }

+ 179
- 237
src/index.js View File

@ -1,84 +1,37 @@
'use strict' 'use strict'
import {Player, Game} from "./classes.js"
import * as HTTP from "./services.js"
import { Player, Series, Payload, EventEnum } from "./classes.js"
import * as lib from "./services.js"
import style from './style.scss' import style from './style.scss'
import { join } from "path";
/** /**
* Initialize DOM variables * Initialize DOM variables
*/ */
let name = document.getElementById('name')
let nameLabel = document.getElementById('nameLabel')
let playerH1 = document.getElementById('player')
let opponent = document.getElementById('opponent')
let message = document.getElementById('message')
let status = document.getElementById('status') let status = document.getElementById('status')
let tally = document.getElementById('tally')
let gameTable = document.getElementById('gameTable') let gameTable = document.getElementById('gameTable')
let instructions = document.getElementById('instructions') let instructions = document.getElementById('instructions')
let shareLink = document.getElementById('shareLink') let shareLink = document.getElementById('shareLink')
let tooltip = document.getElementById("tooltip") let tooltip = document.getElementById("tooltip")
let startGameForm = document.getElementById('startGameForm')
let joinGameForm = document.getElementById('joinGameForm')
let opponentTileColor = "#992222" let opponentTileColor = "#992222"
let tileColor = "#229922" let tileColor = "#229922"
let gameOver = false
let firstgame = true
let opponentName
let conn let conn
let payload
let player = new Player() let player = new Player()
console.debug(player)
if (player.getName()) {
name.value = player.getName()
_hideConfig()
_showGreeting()
}
let game = new Game()
console.debug(game)
if (game.getId()) {
firstgame = false
// we need to fetch the game data and resume the game
_retrieveGameFromServer().then(resp => {
console.debug("_retrieveGameFromServer() success", resp)
game.player1 = new Player(resp.player1.id, resp.player1.name)
game.turn = resp.turn
game.matrix = resp.matrix
if (!resp.player2 || typeof resp.player2 === "undefined") {
_updateGameOnServer({
player2: player
}).then(resp => {
console.debug("_updateGame() success", resp)
console.debug(game)
}).catch(err => {
console.debug("_updateGame() err", err)
})
} else {
game.player2 = new Player(resp.player2.id, resp.player2.name)
}
if (document.location.pathname != "/join") {
_renderGame()
_showGame()
}
console.debug(game)
}).catch(err => {
console.debug("_retrieveGameFromServer() error", err)
})
}
export function Copy (context) {
console.debug(context.innerText)
context.select()
document.execCommand("Copy")
tooltip.innerHTML = "Copied!"
}
export function TooltipBlur () {
tooltip.innerHTML = "Copy"
}
let series = new Series()
shareLink.value = _getURL() + "/game?sid=" + series.getId()
if (window.WebSocket) { if (window.WebSocket) {
conn = new WebSocket("wss://" + document.location.host + "/ws")
conn.onclose = function (evt) {
conn = new WebSocket("ws://" + document.location.host + "/ws")
conn.onclose = function () {
console.debug("Websocket Closed") console.debug("Websocket Closed")
} }
conn.onmessage = function (evt) { conn.onmessage = function (evt) {
@ -88,213 +41,216 @@ if (window.WebSocket) {
try { try {
data = JSON.parse(data) data = JSON.parse(data)
} catch (e) { } catch (e) {
console.error("Error parsing", data)
console.debug("WebSocket Payload Parse Error", data)
} }
console.debug("Message", data)
if (data.sender.toUpperCase() === player.getId().toUpperCase()) {
// console.debug("My own message received and ignored")
if (data.Sender === player.getId() && data.SeriesID === series.getId()) {
console.debug("WebSocket Payload Ignored", data)
return return
}
if (data.game_id.toUpperCase() !== game.getId().toUpperCase()) {
// console.debug("Not my game")
return
}
switch(data.event) {
case "joining":
console.debug("Event: joining")
instructions.classList.add("hide")
_safeRetrieve()
_showGame()
break
case "new":
console.debug("Event: new")
let newGame = confirm("Would you like to play again?")
if (newGame) {
history.replaceState(null, "", "?id=" + data.new_game_id)
game.setId(data.new_game_id)
_retrieveGameFromServer().then(resp => {
console.debug("_retrieveGameFromServer() success", resp)
game.player1 = new Player(resp.player1.id, resp.player1.name)
game.player2 = new Player(resp.player2.id, resp.player2.name)
game.matrix = resp.matrix
game.turn = resp.turn
console.debug(game)
gameOver = false
_renderGame()
}).catch(err => {
console.debug("_retrieveGameFromServer() error", err)
})
}
break
case "move":
console.debug("Event: move")
_safeRetrieve()
break
case "winner":
console.debug("Event: winner")
game.winner = data.sender
_safeRetrieve()
break
} else if (data.SeriesID === series.getId()) {
console.debug("WebSocket Payload Received", Object.keys(EventEnum).find(k => EventEnum[k] === data.Event), data)
switch (data.Event) {
case EventEnum.ANYBODYHOME:
_showGame()
opponentName = data.Message
opponent.innerText = "Your opponent is: " + opponentName
series.setTurn(null)
status.innerText = _possessivizer(opponentName) + " Turn"
if (player.getName()) {
payload = new Payload(series.getId(), player.getId())
.setEventType(EventEnum.IAMHERE)
.setMessage(player.getName())
.setMatrix(series.getGameMatrix())
.serialize()
conn.send(payload)
} else {
let nick = prompt("Nickname?", "")
player.setName(nick)
payload = new Payload(series.getId(), player.getId())
.setEventType(EventEnum.IAMHERE)
.setMessage(player.getName())
.setMatrix(series.getGameMatrix())
.serialize()
conn.send(payload)
}
break;
case EventEnum.IAMHERE:
_showGame()
opponentName = data.Message
opponent.innerText = "Your opponent is: " + opponentName
series.setTurn(player.getId())
status.innerText = "Your Turn"
series.setGameMatrix(data.Matrix)
_renderGame()
break;
case EventEnum.CHAT:
message.innerText = "Your opponent says: " + data.Message
break;
case EventEnum.MOVE:
series.setGameMatrix(data.Matrix)
series.setTurn(player.getId())
status.innerText = "Your Turn"
_renderGame()
break;
case EventEnum.NEW:
series.logGame()
series.emptyGameMatrix()
series.setTurn(player.getId())
status.innerText = "Your Turn"
_renderGame()
break;
}
} }
} }
} }
conn.onopen = function (evt) {
if (player.getName()) {
payload = new Payload(series.getId(), player.getId())
.setEventType(EventEnum.ANYBODYHOME)
.setMessage(player.getName())
.serialize()
conn.send(payload)
} else {
let nick = prompt("Nickname?", "")
player.setName(nick)
payload = new Payload(series.getId(), player.getId())
.setEventType(EventEnum.ANYBODYHOME)
.setMessage(player.getName())
.serialize()
conn.send(payload)
}
}
} else { } else {
console.error("TicTacToe can only be played in a browser that supports a WebSocket connection.") console.error("TicTacToe can only be played in a browser that supports a WebSocket connection.")
} }
/** /**
* BEGIN Document Listeners * BEGIN Document Listeners
*/ */
if (startGameForm) {
startGameForm.addEventListener('submit', event => {
event.preventDefault()
if (!_validateForm()) {
return false
}
if (firstgame) {
_safeCreate()
firstgame = false
} else {
_createGame().then(resp => {
console.debug("_createGame() success", resp)
history.replaceState(null, "", "?id=" + resp.id)
let old_game_id = game.getId()
game.setId(resp.id)
_updateGameOnServer({
player2: game.player2,
turn: game.player1.id == game.winner ? game.player2.id : game.player1.id,
previous_game: old_game_id
}).then(resp => {
console.debug("_updateGameOnServer() success", resp)
gameOver = false
game.turn = resp.turn
game.matrix = resp.matrix
conn.send(JSON.stringify({
sender: player.getId(),
game_id: old_game_id,
event: "new",
new_game_id: resp.id
}))
_renderGame()
}).catch(err => {
console.debug("_updateGameOnServer() err", err)
})
}).catch(err => {
console.debug("_createGame() err", err)
})
}
})
}
if (joinGameForm) {
joinGameForm.addEventListener('submit', event => {
event.preventDefault()
if (!_validateForm()) {
return false
}
conn.send(JSON.stringify({
sender: player.getId(),
game_id: game.getId(),
event: "joining"
}))
joinGameForm.classList.add("hide")
_theirTurn()
_showGame()
})
}
function tileListener (event) { function tileListener (event) {
if (gameOver) {
event.preventDefault()
return false
}
let pos = event.target.id.split('_')[1] let pos = event.target.id.split('_')[1]
// validate its my turn // validate its my turn
if (game.turn !== player.getId()) {
if (series.getTurn() !== player.getId()) {
console.debug("Not my turn") console.debug("Not my turn")
event.preventDefault() event.preventDefault()
return return
} }
// validate position available // validate position available
if (game.matrix[pos]) {
if (series.getGameMatrix()[pos]) {
console.debug("Tile not free") console.debug("Tile not free")
event.preventDefault() event.preventDefault()
return return
} }
// set move // set move
game.matrix[pos] = player.getId()
let matrix = series.getGameMatrix()
matrix[pos] = player.getId()
series.setGameMatrix(matrix)
series.setTurn(null)
status.innerText = _possessivizer(opponentName) + " Turn"
// calculate if 3 tiles in a row
let [done, winner, positions] = _threeInARow()
_renderGame()
_updateGameOnServer({
id: game.getId(),
player1: game.player1,
player2: game.player2,
turn: game.player1.id == game.turn ? game.player2.id : game.player1.id,
winner: winner,
matrix: game.matrix
}).then(resp => {
console.debug("_updateGameOnServer() success", resp)
conn.send(JSON.stringify({
sender: player.getId(),
game_id: game.getId(),
event: done ? "winner" : "move"
}))
game.turn = resp.turn
_renderGame()
}).catch(err => {
console.debug("_updateGameOnServer() error", err)
})
payload = new Payload(series.getId(), player.getId())
.setEventType(EventEnum.MOVE)
.setMatrix(series.getGameMatrix())
.serialize()
conn.send(payload)
} }
if (gameTable) { if (gameTable) {
document.querySelectorAll('.tile').forEach(element => { document.querySelectorAll('.tile').forEach(element => {
element.addEventListener('click', tileListener) element.addEventListener('click', tileListener)
}) })
} }
/**
* END Document Listeners
*/
function _validateForm() {
if (!name.value) {
status.innerText = "Name is required."
status.style.color = "#FF0000"
return false
}
status.style.color = "#000000"
player.setName(name.value)
_hideConfig()
_showGreeting()
return true
}
function _showInstructions() {
shareLink.value = _getUrl() + "/join?id=" + game.getId()
instructions.classList.remove("hide")
function _getURL () {
return document.location.protocol + '//' + document.location.host
} }
function _hideInstructions () {
function _showGame() {
gameTable.classList.remove("hide")
instructions.classList.add("hide") instructions.classList.add("hide")
} }
function _showGreeting() {
if (player.getName()) {
playerH1.innerText = "Hi " + player.getName()
playerH1.classList.remove("hide")
function _renderGame () {
document.querySelectorAll('#gameTable td').forEach(el => el.classList.remove("dim"))
document.querySelectorAll('#gameTable td').forEach(el => el.classList.remove("highlight"))
let matrix = series.getGameMatrix()
for (let i = 0; i < matrix.length; i++) {
let playerAt = matrix
if (playerAt[i] === player.getId()) {
document.getElementById('pos_' + i).style.backgroundColor = tileColor
} else if (playerAt[i]) {
document.getElementById('pos_' + i).style.backgroundColor = opponentTileColor
} else {
document.getElementById('pos_' + i).style.backgroundColor = "unset"
}
}
let [me, positions] = _analyzeBoard()
if (positions)
_highlightBoard(positions)
if (me) {
setTimeout(() => {
let playagain = confirm("Play another round?")
if (playagain) {
series.logGame()
series.emptyGameMatrix()
_renderGame()
payload = new Payload(series.getId(), player.getId())
.setEventType(EventEnum.NEW)
.serialize()
conn.send(payload)
}
}, 1000)
} }
} }
function _showGame () {
gameTable.classList.remove("hide")
function _analyzeBoard() {
let matrix = series.getGameMatrix()
for (let i = 0; i <= 8; i) {
if (matrix[i] && (matrix[i] === matrix[i + 1] && matrix[i] === matrix[i + 2])) {
return [matrix[i] == player.getId(), [i, i + 1, i + 2]]
}
i = i + 3
}
for (let i = 0; i <= 2; i++) {
if (matrix[i] && (matrix[i] === matrix[i + 3] && matrix[i] === matrix[i + 6])) {
return [matrix[i] == player.getId(), [i, i + 3, i + 6]]
}
}
if (matrix[0] && (matrix[0] === matrix[4] && matrix[0] === matrix[8])) {
return [matrix[0] == player.getId(), [0, 4, 8]]
}
if (matrix[2] && (matrix[2] === matrix[4] && matrix[2] === matrix[6])) {
return [matrix[2] == player.getId(), [2, 4, 6]]
}
return [false, null]
} }
function _resumeGame () {
startGameForm.classList.add("hide")
_showGame()
function _highlightBoard(positions) {
document.querySelectorAll('#gameTable td').forEach(el => el.classList.add("dim"))
for (let pos of positions) {
document.querySelector('#gameTable #pos_' + pos).classList.add("highlight")
}
} }
function _hideConfig () {
name.classList.add("hide")
nameLabel.classList.add("hide")
function _possessivizer (name) {
if (name.charAt(name.length - 1) === "s") {
return name + "'"
} else {
return name + "'s"
}
} }
export function Copy (context) {
context.select()
document.execCommand("Copy")
tooltip.innerHTML = "Copied!"
}
export function TooltipBlur () {
tooltip.innerHTML = "Copy"
}
/*
function _renderGame() { function _renderGame() {
console.debug("Render Game Board") console.debug("Render Game Board")
_dimBoard(true) _dimBoard(true)
for (let i = 0; i < game.matrix.length; i++) {
for (let i = 0; i < matrix.length; i++) {
let playerAt = game.matrix let playerAt = game.matrix
if (playerAt[i] === player.getId()) { if (playerAt[i] === player.getId()) {
document.getElementById('pos_' + i).style.backgroundColor = tileColor document.getElementById('pos_' + i).style.backgroundColor = tileColor
@ -328,9 +284,6 @@ function _renderGame() {
} }
} }
function _getUrl () {
return document.location.protocol + '//' + document.location.host
}
function _safeCreate() { function _safeCreate() {
_createGame().then(resp => { _createGame().then(resp => {
console.debug("_createGame() success", resp) console.debug("_createGame() success", resp)
@ -374,13 +327,6 @@ function _yourTurn () {
function _theirTurn () { function _theirTurn () {
status.innerText = _possessivizer(game.getOpponent().getName()) + " Turn" status.innerText = _possessivizer(game.getOpponent().getName()) + " Turn"
} }
function _possessivizer(name) {
if (name.charAt(name.length - 1) === "s") {
return name + "'"
} else {
return name + "'s"
}
}
function _threeInARow() { function _threeInARow() {
for (let i = 0; i <= 8; i) { for (let i = 0; i <= 8; i) {
if (game.matrix[i] && (game.matrix[i] === game.matrix[i + 1] && game.matrix[i] === game.matrix[i + 2]) ) { if (game.matrix[i] && (game.matrix[i] === game.matrix[i + 1] && game.matrix[i] === game.matrix[i + 2]) ) {
@ -417,8 +363,4 @@ function _dimBoard(reverse) {
document.querySelectorAll('td').forEach(el => el.classList.add("dim")) document.querySelectorAll('td').forEach(el => el.classList.add("dim"))
} }
} }
function _highlightBoard (positions) {
for (let pos of positions) {
document.querySelector('#pos_' + pos).classList.add("highlight")
}
}
*/

+ 2
- 6
src/services.js View File

@ -1,6 +1,5 @@
'use strict' 'use strict'
// GET HTTP call
export function GET (theUrl) { export function GET (theUrl) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var req = new XMLHttpRequest() var req = new XMLHttpRequest()
@ -15,8 +14,6 @@ export function GET (theUrl) {
req.send(null) req.send(null)
}) })
} }
// POST HTTP call
export function POST (theUrl, data) { export function POST (theUrl, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let req = new XMLHttpRequest() let req = new XMLHttpRequest()
@ -34,13 +31,12 @@ export function POST (theUrl, data) {
req.send(data) req.send(data)
}) })
} }
function _parseOrNot(str) {
export function _parseOrNot(str) {
let data = str let data = str
try { try {
data = JSON.parse(str) data = JSON.parse(str)
} catch (e) { } catch (e) {
console.debug("Error Parsing Response JSON:", str)
console.debug("Error Parsing JSON:", str)
} }
return data return data
} }

+ 19
- 16
src/style.scss View File

@ -16,22 +16,14 @@ h2 {
font-size: 10px; font-size: 10px;
} }
} }
form {
padding-top: 20px;
input#name {
font: normal normal normal 14px/30px 'Lato', sans-serif;
height: 30px;
display: block;
}
button#start, button#join {
font: normal normal normal 14px/30px 'Lato', sans-serif;
margin: 20px 0;
height: 30px;
background: rgb(77, 22, 228);
border: none;
border-radius: 2px;
color: white;
}
button#start {
font: normal normal normal 14px/30px 'Lato', sans-serif;
margin: 20px 0;
height: 30px;
background: rgb(77, 22, 228);
border: none;
border-radius: 2px;
color: white;
} }
input#shareLink { input#shareLink {
font-family: monospace; font-family: monospace;
@ -44,6 +36,17 @@ input#shareLink {
#status { #status {
font: normal normal bold 16px/20px 'Lato', sans-serif; font: normal normal bold 16px/20px 'Lato', sans-serif;
} }
#tally {
.gameTable {
tr {
td {
height: 10px;
width: 10px;
content: "";
}
}
}
}
#gameTable { #gameTable {
tr { tr {
td { td {

Loading…
Cancel
Save