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/>.
21 querystring = require 'querystring'
24 coffee = require 'coffee-script'
25 model = require './common.coffee'
28 max_concurrent_games = 50
29 max_game_idle = 2 * 60 * 60 * 1000 # two hours (in miliseconds)
31 # timeout function with args in convenient order
32 timeout = (ms, func) -> setTimeout func, ms
43 oldest_seen = g.last_seen
45 return unless count > 0
50 too_old = now_s() - max_game_idle
53 if g.last_seen < oldest_seen
54 oldest_seen = g.last_seen
56 if g.last_seen < too_old
58 if count > max_concurrent_games and kills.length is 0
59 console.log "hit max_concurrent_games, destroying oldest"
60 kills.push oldest_slug
62 console.log "killing game #{slug}"
65 css_handler = (args, out, request, url_parts) ->
66 fs.readFile 'style.less', 'utf8', (err, data) ->
68 return out.end 'Server failed to read style.less'
69 less.render data, (err, css) ->
71 return out.end "Server failed to make css: #{err}"
74 js_handler = (args, out, request, url_parts) ->
76 basename = url_parts.pathname.substr 1, (url_parts.pathname.length - 4)
77 if basename is 'client'
78 filename = 'client.coffee'
80 else if basename is 'common'
81 filename = 'common.coffee'
83 else if basename is 'cs_cards'
84 filename = 'cs_cards.js'
87 error = "Unknown js basename: #{basename}"
92 fs.readFile filename, 'utf8', (err, data) ->
94 return out.end "Server failed to read #{filename}"
97 converted = coffee.compile data
99 out.end "alert(\"server faild to compile #{filename}\");"
104 html_handler = (args, out, request, url_parts) ->
105 fs.readFile 'index.html', 'utf8', (err, data) ->
107 return out.end 'Server failed to read index.html'
110 clean_pathname_regex = new RegExp('[^a-zA-Z/_.-]')
111 clean_pathname_regex2 = new RegExp('/[.]')
112 clean_pathname_regex3 = new RegExp('^[.-]')
113 clean_pathname = (str) ->
114 str = str.replace clean_pathname_regex, '_'
115 str = str.replace clean_pathname_regex2, '/_'
116 return str.replace clean_pathname_regex3, '_'
118 # serve javascript files from within /usr/share/javascript
119 javascript_handler = (args, out, request, url_parts) ->
120 filename = clean_pathname "/usr/share/#{url_parts.pathname}"
121 fs.readFile filename, 'utf8', (err, data) ->
124 return out.end "Server failed to read #{filename}"
125 out.writeHead 200, 'Content-Type': 'text/javascript'
129 get_handler = (args, out, request, url_parts) ->
130 unless args.game?.length
131 out.writeHead 404, "Content-Type": 'text/plain'
132 out.end 'Missing (or empty) "game" argument'
135 unless args.agent is 'p1' or args.agent is 'p2'
136 out.writeHead 404, "Content-Type": 'text/plain'
137 out.end '"agent" argument must be set to p1 or p2'
140 unless games[args.game]?
141 out.writeHead 404, "Content-Type": 'text/plain'
142 out.end 'Game not found'
145 game = games[args.game]
147 waiter = games["#{args.agent}_waiter"]
149 waiter.writeHead 200, 'Content-Type': 'text/javascript'
152 game["#{args.agent}_waiter"] = out
154 answer_soon game # in case there's something queued already
156 set_handler = (args, out, request, url_parts) ->
157 unless args.game?.length
158 out.writeHead 404, "Content-Type": 'text/plain'
159 out.end '{"status":1,"text_status":"Missing (or empty) game argument"}'
162 unless args.agent is 'p1' or args.agent is 'p2'
163 out.writeHead 404, "Content-Type": 'text/plain'
164 out.end '{"status":2,"text_status":"agent argument must be set to p1 or p2"}'
167 unless args.messages?
168 out.writeHead 404, "Content-Type": 'text/plain'
169 out.end '{"status":3,"text_status":"messages argument must be set"}'
173 messages = JSON.parse args.messages
175 out.writeHead 400, "Content-Type": 'text/plain'
176 out.end '{"status":4,"text_status":"Invalid JSON"}'
179 # special handling of 'new_game' api, because for this one we don't have a
180 # game object to pass the message to
181 if messages?[0]?[0] is 'new_game'
182 message = messages.shift()
185 out.writeHead 403, "Content-Type": 'text/plain'
186 out.end '{"status":6,"text_status":"Game already exists"}'
188 game = games[slug] = new_game slug, 'server'
189 game.last_seen = now_s()
190 console.log "new game: #{slug}"
193 unless games[args.game]?
194 out.writeHead 404, "Content-Type": 'text/plain'
195 out.end '{"status":5,"text_status":"Game not found"}'
198 game = games[args.game]
200 game.last_seen = now_s()
202 game.process_messages messages
204 out.writeHead 200, "Content-Type": 'text/plain'
205 out.end '{"status":0,"text_status":"Success"}'
207 # don't call this directly, call answer_soon instead
208 answer_now = (game) ->
209 if game.p1_waiter and game.p1_queue.length
210 waiter = game.p1_waiter
211 queue = game.p1_queue
212 game.p1_waiter = false
214 waiter.writeHead 200, 'Content-Type': 'text/javascript'
215 waiter.end JSON.stringify queue
216 if game.p2_waiter and game.p2_queue.length
217 waiter = game.p2_waiter
218 queue = game.p2_queue
219 game.p2_waiter = false
221 waiter.writeHead 200, 'Content-Type': 'text/javascript'
222 waiter.end JSON.stringify queue
224 # this marks a game as "dirty" and makes sure there's exactly one timeout
225 # that'll respond to any clients that are waiting, and now have messages.
226 answer_soon = (game) ->
227 unless game.replier_id
228 game.replier_id = timeout 1, ->
229 delete game.replier_id
232 forward_events = (message...) ->
233 unless message[1] is 'p1'
234 @p1_queue.push message
236 unless message[1] is 'p2'
237 @p2_queue.push message
241 game = games[slug] = model.new slug, 'server'
242 game.p1_waiter = false
243 game.p2_waiter = false
247 game.on 'move', (agent, card, x, y, z, pile) ->
248 forward_events.call this, 'move', agent, card, x, y, z, pile
249 game.on 'mark', (agent, card, state) ->
250 forward_events.call this, 'mark', agent, card, state
251 game.on 'flip', (agent, card, state) ->
252 forward_events.call this, 'flip', agent, card, state
253 game.on 'new_cards', (agent, cards) ->
254 # server assigns card numbers, and tells both clients
255 # (unlike all other api calls, sending agent expects to get this one back)
256 forward_events.call this, 'new_cards', 'server', cards
257 game.on 'set_cards', (agent, cards) ->
258 forward_events.call this, 'set_cards', agent, cards
259 game.on 'send_state', (agent) ->
262 @p1_queue.push ['set_cards', 'server', @cards]
265 @p2_queue.push ['set_cards', 'server', @cards]
270 http_server = http.createServer (req, res) ->
271 url_parts = url.parse req.url, true
272 if url_parts.query is undefined
275 rel_path = url_parts.pathname.substr 1
277 if rel_path.substr(0, 11) is 'javascript/'
278 return javascript_handler url_parts.query, res, req, url_parts
279 else if rel_path.substr(rel_path.length - 4) is '.css'
280 res.writeHead 200, 'Content-Type': 'text/css'
281 return css_handler url_parts.query, res, req, url_parts
282 else if rel_path.substr(rel_path.length - 3) is '.js'
283 res.writeHead 200, 'Content-Type': 'text/javascript'
284 return js_handler url_parts.query, res, req, url_parts
285 else if rel_path.substr(rel_path.length - 4) is '/set'
287 req.on 'data', (chunk) ->
290 query = url_parts.query
291 post_args = querystring.parse data
292 for key, parg of post_args
294 return set_handler query, res, req, url_parts
296 else if rel_path.substr(rel_path.length - 4) is '/get'
297 return get_handler url_parts.query, res, req, url_parts
298 else if rel_path.substr(rel_path.length - 4) is '.ico'
302 return html_handler url_parts.query, res, req, url_parts
304 setInterval expire_old_games, 2 * 60 * 1000 # check every 2 minutes for expired games
306 http_server.listen listen_port, "127.0.0.1"
307 console.log "Server running at http://127.0.0.1:#{listen_port}/"