JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
client sync working (and spitting out debug cruft)
authorJason Woofenden <jason@jasonwoof.com>
Thu, 27 Oct 2011 06:17:11 +0000 (02:17 -0400)
committerJason Woofenden <jason@jasonwoof.com>
Thu, 27 Oct 2011 06:17:11 +0000 (02:17 -0400)
client.coffee
common.coffee
server.coffee

index 774e29e..fbe2815 100644 (file)
@@ -1,16 +1,13 @@
+# globals
 $table = null
+state = null
+server_url = null
 
-state = {
-       card_types: [
-               {text: "Rusty Camel"}
-               {text: "Angry Ocelot"}
-               {text: "Unruly Parsnip"}
-       ],
-       # values are indexes into card_types array
-       my_cards: [0, 0, 0, 1, 1, 2],
-       your_cards: [0, 1, 1, 2, 2, 2],
-       auto_shuffle: true
-}
+message = (txt) ->
+       # FIXME implement chat box or something
+
+# timeout function with args in convenient order
+timeout = (ms, func) -> setTimeout func, ms
 
 unless Array::shuffle?
        Array::shuffle = ->
@@ -26,17 +23,18 @@ unless Array::shuffle?
 
 new_button = (text) -> $ $ "<div class=\"button\">#{text}</div>"
 
-add_card = (text, x, y) ->
+instantiate_card = (model) ->
+       text = model.text
+       x = model.x
+       y = model.y
        card = $ $ "<div class=\"card\" style=\"left: #{x}px; top: #{y}px\"><span class=\"cardtext\">#{text}</span></div>"
        button_box = $ $ '<div/>'
        flip_button = new_button "flip over"
        mark_button = new_button "mark"
        flip_button.bind 'click', ->
-               card.toggleClass 'flipped'
-               # FIXME tell server
+               state.flip state.agent, model.number, ! card.hasClass 'flipped'
        mark_button.bind 'click', ->
-               card.toggleClass 'marked'
-               # FIXME tell server
+               state.mark state.agent, model.number, ! card.hasClass 'marked'
        button_box.append flip_button
        button_box.append mark_button
        card.append button_box
@@ -44,26 +42,93 @@ add_card = (text, x, y) ->
        card.draggable stack: '.card'
        card.bind 'dragstop', (event, ui) ->
                p = card.position()
-               # FIXME tell server (p.left, p.top)
+               state.move state.agent, model.number, p.left, p.top
+       model.view = card
+
+error_lag = 3
+
+outgoing_messages = []
+# message should be [agent, method, args...]
+# don't forget the agent (state.agent)
+tell_server = (message) ->
+       outgoing_messages.push message
+       send_updates()
+
+send_updates = ->
+       return if outgoing_messages.length is 0
+
+       messages = outgoing_messages
+       outgoing_messages = []
+
+       $.ajax "#{server_url}/set", {
+               cache: false
+               data: {
+                       agent: state.agent
+                       game: 'test' # FIXME, and it the /get call too
+                       messages: JSON.stringify(messages)
+               }
+               type: 'POST'
+               dataType: 'json'
+               error: (xhr, status, error) ->
+                       message "Network error while sending, you might want to refresh. Trying again in #{error_lag} seconds. (Status: #{status}, Error: #{error})"
+                       for message in messages
+                               outgoing_messages.unshift message
+                       timeout error_lag * 1000, send_updates
+                       error_lag *= 2
+               success: (data, status, xhr) ->
+                       message "update sent"
+                       error_lag = 3
+       }
+
+error_lag = 3
+poll_for_updates = ->
+       $.ajax "#{server_url}/get?agent=#{state.agent}&game=test", {
+               cache: false
+               type: 'GET'
+               dataType: 'json'
+               error: (xhr, status, error) ->
+                       message "Network error, you might want to refresh. Trying again in #{error_lag} seconds. (Status: #{status}, Error: #{error})"
+                       timeout error_lag * 1000, poll_for_updates
+                       error_lag *= 2
+               success: (data, status, xhr) ->
+                       state.process_messages data
+                       timeout 100, poll_for_updates
+                       error_lag = 3
+       }
 
 init = ->
-       if state.auto_shuffle
-               state.my_cards.shuffle()
-               state.your_cards.shuffle() # FIXME have the server or other player do this
-               state.auto_shuffle = false
-               # FIXME tell server
-       left = 15
-       top = 450
-       for card in state.my_cards
-               add_card state.card_types[card].text, left, top
-               left += 120
-       left = 15
-       top = 250
-       for card in state.your_cards
-               add_card state.card_types[card].text, left, top
-               left += 120
+       if window.location.hash? and window.location.hash.length > 0
+               me = window.location.hash.substr 1
+               winloc = "#{window.location}"
+               server_url = winloc.substr 0, winloc.length - window.location.hash.length
+       else
+               me = 'p1'
+               server_url = window.location
+
+       state = window.game_model.new me
+       state.on 'move', (agent, card, x, y) ->
+               # FIXME add/handle pile argument
+               if agent is me
+                       tell_server ['move', agent, card, x, y]
+               else
+                       state.cards[card].view.animate { left: "#{x}px", top: "#{y}px"}, 800
+       state.on 'mark', (agent, card, state) ->
+               @cards[card].view.toggleClass 'marked', state
+               if agent is me
+                       tell_server ['mark', agent, card, state]
+       state.on 'flip', (agent, card, state) ->
+               @cards[card].view.toggleClass 'flipped', state
+               if agent is me
+                       tell_server ['flip', agent, card, state]
+       state.on 'set_cards', (cards) ->
+               # FIXME add agent arg and tell server if it's not us
+               $('.card').remove()
+               for card in cards
+                       instantiate_card card
+
+       # timeout so browser will stop showing that we're loading
+       timeout 1, poll_for_updates
 
 $ ->
        $table = $ '#table'
        init()
-
index 2a146af..ce09f13 100644 (file)
@@ -1,50 +1,52 @@
 # this file is used by the client and server.
 
+# work around lack of module system in the browser:
+if exports?
+       my_exports = exports
+else
+       window.game_model = {}
+       my_exports = window.game_model
+
 class GameState
        constructor: (agent) ->
                @agent = agent
-               state = {
-                       hooks: {}
-                       card_types: []
-                       auto_shuffle: false
-                       cards: []
-                       piles: {}
-               }
+               @hooks = {}
+               @cards = []
+               @piles = {}
        on: (event, callback) ->
-               unless hooks[event]?
-                       hooks[event] = []
-               hooks[event].push callback
+               unless @hooks[event]?
+                       @hooks[event] = []
+               @hooks[event].push callback
        trigger: (event, args...) ->
-               return unless hooks[event]?
-               for callback in hooks[event]
+               return unless @hooks[event]?
+               for callback in @hooks[event]
                        callback.apply this, args
        move: (agent, card, x, y) -> # FIXME add pile argument
                # FIXME what to do on error?
-               return unless cards[card]? #?.pile?
+               return unless @cards[card]? #?.pile?
                #cur_pile = cards[card].pile
                #if pile isnt cur_pile
 
-               cards[card].x = x
-               cards[card].y = y
+               @cards[card].x = x
+               @cards[card].y = y
 
                @trigger 'move', agent, card, x, y # FIXME add pile argument
 
        mark: (agent, card, state) ->
                # FIXME what to do on error?
-               return unless cards[card]?.marked? #?.pile?
-               card.marked = state
-               @trigger 'mark', agent, state
+               return unless @cards[card]?
+               @cards[card].marked = state
+               @trigger 'mark', agent, card, state
 
        flip: (agent, card, state) ->
                # FIXME what to do on error?
-               return unless cards[card]?.flipped? #?.pile?
-               card.flipped = state
-               @trigger 'flip', agent, state
+               return unless @cards[card]?
+               @cards[card].flipped = state
+               @trigger 'flip', agent, card, state
 
        # FIXME implement set_pile(pile, card_order_array)
 
        set_cards: (cards) ->
-               trigger 'delete_all_cards'
                @cards = []
                @piles = {}
                for card in cards
@@ -54,5 +56,20 @@ class GameState
                                unless @piles[card.pile]?
                                        @piles[card.pile] = []
                                @piles[card.pile].push card
-               trigger 'all_new_cards'
+               @trigger 'set_cards', @cards
+
+       process_messages: (messages) ->
+               unless typeof messages is 'array' or typeof messages is 'object'
+                       # FIXME what to do on error?
+                       return typeof messages
+
+               for message in messages
+                       unless message instanceof Array and message[0]? and message[0] in ['move', 'mark', 'flip', 'set_cards']
+                               # FIXME what to do on error?
+                               return 2
+                       method = message.shift()
+                       @[method].apply this, message
+               return
+
 
+my_exports.new = (agent) -> new GameState agent
index 9beb901..ab0ca9f 100644 (file)
@@ -2,9 +2,16 @@ listen_port = 8333
 sys = require 'sys'
 fs = require 'fs'
 http = require 'http'
+querystring = require 'querystring'
 url = require 'url'
 less = require 'less'
 coffee = require 'coffee-script'
+model = require './common.coffee'
+
+games = {}
+
+# timeout function with args in convenient order
+timeout = (ms, func) -> setTimeout func, ms
 
 css_handler = (args, out, request, url_parts) ->
        fs.readFile 'style.less', 'utf8', (err, data) ->
@@ -57,6 +64,130 @@ javascript_handler = (args, out, request, url_parts) ->
                out.end data
 
 
+get_handler = (args, out, request, url_parts) ->
+       console.log "get handler: ", args
+       unless args.game?.length
+               out.writeHead 404, "Content-Type": 'text/plain'
+               out.end 'Missing (or empty) "game" argument'
+               return
+
+       unless args.agent is 'p1' or args.agent is 'p2'
+               out.writeHead 404, "Content-Type": 'text/plain'
+               out.end '"agent" argument must be set to p1 or p2'
+               return
+
+       unless games[args.game]?
+               out.writeHead 404, "Content-Type": 'text/plain'
+               out.end 'Game not found'
+               return
+
+       game = games[args.game]
+
+       waiter = games["#{args.agent}_waiter"]
+       if waiter?
+               waiter.writeHead 200, 'Content-Type': 'text/javascript'
+               waiter.end '[]'
+
+       game["#{args.agent}_waiter"] = out
+
+       answer_soon game # in case there's something queued already
+
+set_handler = (args, out, request, url_parts) ->
+       console.log "set handler: ", args
+       unless args.game?.length
+               out.writeHead 404, "Content-Type": 'text/plain'
+               out.end 'Missing (or empty) "game" argument'
+               return
+
+       unless args.agent is 'p1' or args.agent is 'p2'
+               out.writeHead 404, "Content-Type": 'text/plain'
+               out.end '"agent" argument must be set to p1 or p2'
+               return
+
+       unless args.messages?
+               out.writeHead 404, "Content-Type": 'text/plain'
+               out.end '"messages" argument must be set'
+               return
+
+       unless games[args.game]?
+               out.writeHead 404, "Content-Type": 'text/plain'
+               out.end 'Game not found'
+               return
+
+       game = games[args.game]
+
+       # FIXME add error checking (json validity at least)
+       game.process_messages JSON.parse args.messages
+
+       out.writeHead 200, "Content-Type": 'text/plain'
+       out.end 'ok'
+
+# don't call this directly, call answer_soon instead
+answer_now = (game) ->
+       if game.p1_waiter and game.p1_queue.length
+               waiter = game.p1_waiter
+               queue = game.p1_queue
+               game.p1_waiter = false
+               game.p1_queue = []
+               waiter.writeHead 200, 'Content-Type': 'text/javascript'
+               waiter.end JSON.stringify queue
+       if game.p2_waiter and game.p2_queue.length
+               waiter = game.p2_waiter
+               queue = game.p2_queue
+               game.p2_waiter = false
+               game.p2_queue = []
+               waiter.writeHead 200, 'Content-Type': 'text/javascript'
+               waiter.end JSON.stringify queue
+
+# this marks a game as "dirty" and makes sure there's exactly one timeout
+# that'll respond to any clients that are waiting, and now have messages.
+answer_soon = (game) ->
+       unless game.replier_id
+               game.replier_id = timeout 1, ->
+                       delete game.replier_id
+                       answer_now game
+
+forward_events = (message...) ->
+       unless message[1] is 'p1'
+               @p1_queue.push message
+               answer_soon this
+       unless message[1] is 'p2'
+               @p2_queue.push message
+               answer_soon this
+       console.log this
+
+new_game = (id) ->
+       game = games[id] = model.new 'server'
+       game.p1_waiter = false
+       game.p2_waiter = false
+       game.p1_queue = []
+       game.p2_queue = []
+
+       game.on 'move', (agent, card, x, y) ->
+               forward_events.call this, 'move', agent, card, x, y
+       game.on 'mark', (agent, card, state) ->
+               forward_events.call this, 'mark', agent, card, state
+       game.on 'flip', (agent, card, state) ->
+               forward_events.call this, 'flip', agent, card, state
+       game.on 'set_cards', (cards) ->
+               forward_events.call this, 'set_cards', cards
+
+       return game
+
+test_init = ->
+       test_game = new_game 'test'
+       timeout 4000, ->
+               test_game.set_cards [
+                       { text: "Wildabeast 2/2", x: 20, y: 140}
+                       { text: "Wild beast 2/2", x: 150, y: 140}
+                       { text: "Angora bunny 1/1", x: 300, y: 140}
+                       { text: "Ambulatory Cactus 2/1", x: 450, y: 140}
+                       { text: "Ent 0/5", x: 600, y: 140}
+               ]
+
+test_init()
+
+
 http_server = http.createServer (req, res) ->
        url_parts = url.parse req.url, true
        if url_parts.query is undefined
@@ -69,9 +200,25 @@ http_server = http.createServer (req, res) ->
        else if rel_path.substr(rel_path.length - 4) is '.css'
                res.writeHead 200, 'Content-Type': 'text/css'
                return css_handler url_parts.query, res, req, url_parts
-       else if rel_path.substr rel_path.length - 3 is '.js'
+       else if rel_path.substr(rel_path.length - 3) is '.js'
                res.writeHead 200, 'Content-Type': 'text/javascript'
                return js_handler url_parts.query, res, req, url_parts
+       else if rel_path.substr(rel_path.length - 4) is '/set'
+               data = ''
+               req.on 'data', (chunk) ->
+                       data += chunk
+               req.on 'end', ->
+                       query = url_parts.query
+                       post_args = querystring.parse data
+                       console.log data, post_args
+                       for key, parg of post_args
+                               query[key] = parg
+                       return set_handler query, res, req, url_parts
+       else if rel_path.substr(rel_path.length - 4) is '/get'
+               return get_handler url_parts.query, res, req, url_parts
+       else if rel_path.substr(rel_path.length - 4) is '.ico'
+               res.writeHead 404
+               return res.end()
 
        return html_handler url_parts.query, res, req, url_parts