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) -> if err return out.end 'Server failed to read style.less' less.render data, (err, css) -> if err return out.end "Server failed to make css: #{err}" out.end css js_handler = (args, out, request, url_parts) -> basename = url_parts.pathname.substr 1, (url_parts.pathname.length - 4) if basename is 'client' filename = 'client.coffee' else if basename is 'common' filename = 'common.coffee' else error = "Unknown js basename: #{basename}" console.log error out.end(error) return fs.readFile filename, 'utf8', (err, data) -> if err return out.end "Server failed to read #{filename}" out.end coffee.compile data html_handler = (args, out, request, url_parts) -> fs.readFile 'index.html', 'utf8', (err, data) -> if err return out.end 'Server failed to read index.html' out.end data clean_pathname_regex = new RegExp('[^a-zA-Z/_.-]') clean_pathname_regex2 = new RegExp('/[.]') clean_pathname_regex3 = new RegExp('^[.-]') clean_pathname = (str) -> str = str.replace clean_pathname_regex, '_' str = str.replace clean_pathname_regex2, '/_' return str.replace clean_pathname_regex3, '_' # serve javascript files from within /usr/share/javascript javascript_handler = (args, out, request, url_parts) -> filename = clean_pathname "/usr/share/#{url_parts.pathname}" fs.readFile filename, 'utf8', (err, data) -> if err out.writeHead 404 return out.end "Server failed to read #{filename}" out.writeHead 200, 'Content-Type': 'text/javascript' out.end data get_handler = (args, out, request, url_parts) -> 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) -> 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 '{"status":0,"text_status":"Success"}' # 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 = [] timeout 2000, -> # FIXME remove this delay for player 2 (just here to test lag handling) 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 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, z) -> forward_events.call this, 'move', agent, card, x, y, z 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', (agent, cards) -> forward_events.call this, 'set_cards', agent, cards game.on 'send_state', (agent) -> timeout 10, => if agent is 'p1' @p1_queue.push ['set_cards', 'server', @cards] answer_soon this if agent is 'p2' @p2_queue.push ['set_cards', 'server', @cards] answer_soon this return game test_init = -> test_game = new_game 'test' timeout 2, -> test_game.set_cards 'server', [ { text: "Wildabeast 2/2", x: 220, y: 200, owner: 'p2'} { text: "Boar 2/2", x: 360, y: 200, owner: 'p2', pile: 'p2_discard'} { text: "Angora bunny 1/1", x: 500, y: 200, owner: 'p2'} { text: "Ambulatory Cactus 2/1", x: 660, y: 200, owner: 'p2'} { text: "Ent 0/5", x: 800, y: 200, owner: 'p2'} { text: "Carnivore 2/1", x: 220, y: 420, owner: 'p1'} { text: "Herbivore 1/2", x: 360, y: 420, owner: 'p1'} { text: "Stone Wall 0/10", x: 500, y: 420, owner: 'p1', pile: 'p1_draw', flipped: true} { text: "Log 0/1", x: 660, y: 420, owner: 'p1', pile: 'p1_draw', flipped: true} { text: "Ent 0/5", x: 800, y: 420, owner: 'p1', pile: 'p1_draw', flipped: true} { text: "Barricade 0/10", x: 500, y: 420, owner: 'p1', pile: 'p1_draw', flipped: true} { text: "O(log(n)) 0/1", x: 660, y: 420, owner: 'p1', pile: 'p1_draw', flipped: true} { text: "Fence 0/5", x: 800, y: 420, owner: 'p1', pile: 'p1_draw', flipped: true} ] test_init() http_server = http.createServer (req, res) -> url_parts = url.parse req.url, true if url_parts.query is undefined url_parts.query = {} rel_path = url_parts.pathname.substr 1 if rel_path.substr(0, 11) is 'javascript/' return javascript_handler url_parts.query, res, req, url_parts 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' 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 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 http_server.listen listen_port, "127.0.0.1" console.log "Server running at http://127.0.0.1:#{listen_port}/"