1 # Peach CGT -- Card Game Table simulator
2 # Copyright (C) 2011 Jason Woofenden
4 # This program is free software: you can redistribute it and/or modify it under
5 # the terms of the GNU Affero General Public License as published by the Free
6 # Software Foundation, either version 3 of the License, or (at your option) any
9 # This program is distributed in the hope that it will be useful, but WITHOUT
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 listen_port = process.env.app_port ? 9988
21 querystring = require 'querystring'
23 console.log "required builtins"
25 console.log "required less"
26 coffee = require 'coffee-script'
27 console.log "required coffee-script"
28 model = require './common.coffee'
29 console.log "required common.coffee"
32 max_concurrent_games = 50
33 max_game_idle = 3 * 60 * 60 * 1000 # three hours (in miliseconds)
35 # timeout function with args in convenient order
36 timeout = (ms, func) -> setTimeout func, ms
47 oldest_seen = g.last_seen
49 return unless count > 0
54 too_old = now_s() - max_game_idle
57 if g.last_seen < oldest_seen
58 oldest_seen = g.last_seen
60 if g.last_seen < too_old
62 if count > max_concurrent_games and kills.length is 0
63 console.log "hit max_concurrent_games, destroying oldest"
64 kills.push oldest_slug
66 console.log "killing game #{slug}"
69 css_handler = (args, out, request, url_parts) ->
70 fs.readFile 'style.less', 'utf8', (err, data) ->
72 return out.end 'Server failed to read style.less'
73 less.render data, (err, css) ->
75 return out.end "Server failed to make css: #{err}"
78 js_handler = (args, out, request, url_parts) ->
80 basename = url_parts.pathname.substr 1, (url_parts.pathname.length - 4)
81 if basename is 'client'
82 filename = 'client.coffee'
84 else if basename is 'common'
85 filename = 'common.coffee'
87 else if basename is 'cs_cards'
88 filename = 'cs_cards.js'
91 error = "Unknown js basename: #{basename}"
96 fs.readFile filename, 'utf8', (err, data) ->
98 return out.end "Server failed to read #{filename}"
101 converted = coffee.compile data
103 out.end "alert(\"server faild to compile #{filename}\");"
108 html_handler = (args, out, request, url_parts) ->
109 fs.readFile 'index.html', 'utf8', (err, data) ->
111 return out.end "Server failed to read index.html: #{err}"
114 clean_pathname_regex = new RegExp('[^a-zA-Z/_.-]')
115 clean_pathname_regex2 = new RegExp('/[.]')
116 clean_pathname_regex3 = new RegExp('^[.-]')
117 clean_pathname = (str) ->
118 str = str.replace clean_pathname_regex, '_'
119 str = str.replace clean_pathname_regex2, '/_'
120 return str.replace clean_pathname_regex3, '_'
122 # serve javascript files from within external/
123 external_javascript_handler = (args, out, request, url_parts) ->
124 filename = clean_pathname "external/#{url_parts.pathname.substr 10}"
125 fs.readFile filename, 'utf8', (err, data) ->
128 return out.end "Server failed to read #{filename}"
129 out.writeHead 200, 'Content-Type': 'text/javascript'
133 get_handler = (args, out, request, url_parts) ->
134 unless args.game?.length
135 out.writeHead 404, "Content-Type": 'text/plain'
136 out.end 'Missing (or empty) "game" argument'
139 unless args.agent is 'p1' or args.agent is 'p2'
140 out.writeHead 404, "Content-Type": 'text/plain'
141 out.end '"agent" argument must be set to p1 or p2'
144 unless games[args.game]?
145 out.writeHead 404, "Content-Type": 'text/plain'
146 out.end 'Game not found'
149 game = games[args.game]
151 waiter = games["#{args.agent}_waiter"]
153 waiter.writeHead 200, 'Content-Type': 'text/javascript'
156 game["#{args.agent}_waiter"] = out
158 answer_soon game # in case there's something queued already
160 set_handler = (args, out, request, url_parts) ->
161 unless args.game?.length
162 out.writeHead 404, "Content-Type": 'text/plain'
163 out.end '{"status":1,"text_status":"Missing (or empty) game argument"}'
166 unless args.agent is 'p1' or args.agent is 'p2'
167 out.writeHead 404, "Content-Type": 'text/plain'
168 out.end '{"status":2,"text_status":"agent argument must be set to p1 or p2"}'
171 unless args.messages?
172 out.writeHead 404, "Content-Type": 'text/plain'
173 out.end '{"status":3,"text_status":"messages argument must be set"}'
177 messages = JSON.parse args.messages
179 out.writeHead 400, "Content-Type": 'text/plain'
180 out.end '{"status":4,"text_status":"Invalid JSON"}'
183 # special handling of 'new_game' api, because for this one we don't have a
184 # game object to pass the message to
185 if messages?[0]?[0] is 'new_game'
186 message = messages.shift()
189 out.writeHead 403, "Content-Type": 'text/plain'
190 out.end '{"status":6,"text_status":"Game already exists"}'
192 game = games[slug] = new_game slug, 'server'
193 game.last_seen = now_s()
194 console.log "new game: #{slug}"
197 unless games[args.game]?
198 out.writeHead 404, "Content-Type": 'text/plain'
199 out.end '{"status":5,"text_status":"Game not found"}'
202 game = games[args.game]
204 game.last_seen = now_s()
206 game.process_messages messages
208 out.writeHead 200, "Content-Type": 'text/plain'
209 out.end '{"status":0,"text_status":"Success"}'
211 # don't call this directly, call answer_soon instead
212 answer_now = (game) ->
213 if game.p1_waiter and game.p1_queue.length
214 waiter = game.p1_waiter
215 queue = game.p1_queue
216 game.p1_waiter = false
218 waiter.writeHead 200, 'Content-Type': 'text/javascript'
219 waiter.end JSON.stringify queue
220 if game.p2_waiter and game.p2_queue.length
221 waiter = game.p2_waiter
222 queue = game.p2_queue
223 game.p2_waiter = false
225 waiter.writeHead 200, 'Content-Type': 'text/javascript'
226 waiter.end JSON.stringify queue
228 # this marks a game as "dirty" and makes sure there's exactly one timeout
229 # that'll respond to any clients that are waiting, and now have messages.
230 answer_soon = (game) ->
231 unless game.replier_id
232 game.replier_id = timeout 1, ->
233 delete game.replier_id
236 forward_events = (message...) ->
237 unless message[1] is 'p1'
238 @p1_queue.push message
240 unless message[1] is 'p2'
241 @p2_queue.push message
245 game = games[slug] = model.new slug, 'server'
246 game.p1_waiter = false
247 game.p2_waiter = false
251 game.on 'move', (agent, card, x, y, z, pile) ->
252 forward_events.call this, 'move', agent, card, x, y, z, pile
253 game.on 'mark', (agent, card, state) ->
254 forward_events.call this, 'mark', agent, card, state
255 game.on 'flip', (agent, card, state) ->
256 forward_events.call this, 'flip', agent, card, state
257 game.on 'new_cards', (agent, cards) ->
258 # server assigns card numbers, and tells both clients
259 # (unlike all other api calls, sending agent expects to get this one back)
260 forward_events.call this, 'new_cards', 'server', cards
261 game.on 'set_cards', (agent, cards) ->
262 forward_events.call this, 'set_cards', agent, cards
263 game.on 'send_state', (agent) ->
266 @p1_queue.push ['set_cards', 'server', @cards]
269 @p2_queue.push ['set_cards', 'server', @cards]
274 http_server = http.createServer (req, res) ->
275 url_parts = url.parse req.url, true
276 if url_parts.query is undefined
279 rel_path = url_parts.pathname.substr 1
281 if rel_path.substr(0, 9) is 'external/'
282 return external_javascript_handler url_parts.query, res, req, url_parts
283 else if rel_path.substr(rel_path.length - 4) is '.css'
284 res.writeHead 200, 'Content-Type': 'text/css'
285 return css_handler url_parts.query, res, req, url_parts
286 else if rel_path.substr(rel_path.length - 3) is '.js'
287 res.writeHead 200, 'Content-Type': 'text/javascript'
288 return js_handler url_parts.query, res, req, url_parts
289 else if rel_path.substr(rel_path.length - 4) is '/set'
291 req.on 'data', (chunk) ->
294 query = url_parts.query
295 post_args = querystring.parse data
296 for key, parg of post_args
298 return set_handler query, res, req, url_parts
300 else if rel_path.substr(rel_path.length - 4) is '/get'
301 return get_handler url_parts.query, res, req, url_parts
302 else if rel_path.substr(rel_path.length - 4) is '.ico'
306 return html_handler url_parts.query, res, req, url_parts
308 ################## INIT ####################
309 # make sure the current working directory is correct
310 process.chdir __dirname
312 setInterval expire_old_games, 2 * 60 * 1000 # check every 2 minutes for expired games
314 http_server.listen listen_port
315 console.log "Server running at http://127.0.0.1:#{listen_port}/"