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'
29 # timeout function with args in convenient order
30 timeout = (ms, func) -> setTimeout func, ms
32 css_handler = (args, out, request, url_parts) ->
33 fs.readFile 'style.less', 'utf8', (err, data) ->
35 return out.end 'Server failed to read style.less'
36 less.render data, (err, css) ->
38 return out.end "Server failed to make css: #{err}"
41 js_handler = (args, out, request, url_parts) ->
43 basename = url_parts.pathname.substr 1, (url_parts.pathname.length - 4)
44 if basename is 'client'
45 filename = 'client.coffee'
47 else if basename is 'common'
48 filename = 'common.coffee'
50 else if basename is 'cs_cards'
51 filename = 'cs_cards.js'
54 error = "Unknown js basename: #{basename}"
59 fs.readFile filename, 'utf8', (err, data) ->
61 return out.end "Server failed to read #{filename}"
64 converted = coffee.compile data
66 out.end "alert(\"server faild to compile #{filename}\");"
71 html_handler = (args, out, request, url_parts) ->
72 fs.readFile 'index.html', 'utf8', (err, data) ->
74 return out.end 'Server failed to read index.html'
77 clean_pathname_regex = new RegExp('[^a-zA-Z/_.-]')
78 clean_pathname_regex2 = new RegExp('/[.]')
79 clean_pathname_regex3 = new RegExp('^[.-]')
80 clean_pathname = (str) ->
81 str = str.replace clean_pathname_regex, '_'
82 str = str.replace clean_pathname_regex2, '/_'
83 return str.replace clean_pathname_regex3, '_'
85 # serve javascript files from within /usr/share/javascript
86 javascript_handler = (args, out, request, url_parts) ->
87 filename = clean_pathname "/usr/share/#{url_parts.pathname}"
88 fs.readFile filename, 'utf8', (err, data) ->
91 return out.end "Server failed to read #{filename}"
92 out.writeHead 200, 'Content-Type': 'text/javascript'
96 get_handler = (args, out, request, url_parts) ->
97 unless args.game?.length
98 out.writeHead 404, "Content-Type": 'text/plain'
99 out.end 'Missing (or empty) "game" argument'
102 unless args.agent is 'p1' or args.agent is 'p2'
103 out.writeHead 404, "Content-Type": 'text/plain'
104 out.end '"agent" argument must be set to p1 or p2'
107 unless games[args.game]?
108 out.writeHead 404, "Content-Type": 'text/plain'
109 out.end 'Game not found'
112 game = games[args.game]
114 waiter = games["#{args.agent}_waiter"]
116 waiter.writeHead 200, 'Content-Type': 'text/javascript'
119 game["#{args.agent}_waiter"] = out
121 answer_soon game # in case there's something queued already
123 set_handler = (args, out, request, url_parts) ->
124 unless args.game?.length
125 out.writeHead 404, "Content-Type": 'text/plain'
126 out.end '{"status":1,"text_status":"Missing (or empty) game argument"}'
129 unless args.agent is 'p1' or args.agent is 'p2'
130 out.writeHead 404, "Content-Type": 'text/plain'
131 out.end '{"status":2,"text_status":"agent argument must be set to p1 or p2"}'
134 unless args.messages?
135 out.writeHead 404, "Content-Type": 'text/plain'
136 out.end '{"status":3,"text_status":"messages argument must be set"}'
140 messages = JSON.parse args.messages
142 out.writeHead 400, "Content-Type": 'text/plain'
143 out.end '{"status":4,"text_status":"Invalid JSON"}'
146 # special handling of 'new_game' api, because for this one we don't have a
147 # game object to pass the message to
148 if messages?[0]?[0] is 'new_game'
149 message = messages.shift()
152 out.writeHead 403, "Content-Type": 'text/plain'
153 out.end '{"status":6,"text_status":"Game already exists"}'
155 game = games[slug] = new_game slug, 'server'
157 unless games[args.game]?
158 out.writeHead 404, "Content-Type": 'text/plain'
159 out.end '{"status":5,"text_status":"Game not found"}'
162 game = games[args.game]
164 game.process_messages messages
166 out.writeHead 200, "Content-Type": 'text/plain'
167 out.end '{"status":0,"text_status":"Success"}'
169 # don't call this directly, call answer_soon instead
170 answer_now = (game) ->
171 if game.p1_waiter and game.p1_queue.length
172 waiter = game.p1_waiter
173 queue = game.p1_queue
174 game.p1_waiter = false
176 waiter.writeHead 200, 'Content-Type': 'text/javascript'
177 waiter.end JSON.stringify queue
178 if game.p2_waiter and game.p2_queue.length
179 waiter = game.p2_waiter
180 queue = game.p2_queue
181 game.p2_waiter = false
183 waiter.writeHead 200, 'Content-Type': 'text/javascript'
184 waiter.end JSON.stringify queue
186 # this marks a game as "dirty" and makes sure there's exactly one timeout
187 # that'll respond to any clients that are waiting, and now have messages.
188 answer_soon = (game) ->
189 unless game.replier_id
190 game.replier_id = timeout 1, ->
191 delete game.replier_id
194 forward_events = (message...) ->
195 unless message[1] is 'p1'
196 @p1_queue.push message
198 unless message[1] is 'p2'
199 @p2_queue.push message
203 game = games[slug] = model.new slug, 'server'
204 game.p1_waiter = false
205 game.p2_waiter = false
209 game.on 'move', (agent, card, x, y, z, pile) ->
210 forward_events.call this, 'move', agent, card, x, y, z, pile
211 game.on 'mark', (agent, card, state) ->
212 forward_events.call this, 'mark', agent, card, state
213 game.on 'flip', (agent, card, state) ->
214 forward_events.call this, 'flip', agent, card, state
215 game.on 'new_cards', (agent, cards) ->
216 # server assigns card numbers, and tells both clients
217 # (unlike all other api calls, sending agent expects to get this one back)
218 forward_events.call this, 'new_cards', 'server', cards
219 game.on 'set_cards', (agent, cards) ->
220 forward_events.call this, 'set_cards', agent, cards
221 game.on 'send_state', (agent) ->
224 @p1_queue.push ['set_cards', 'server', @cards]
227 @p2_queue.push ['set_cards', 'server', @cards]
232 http_server = http.createServer (req, res) ->
233 url_parts = url.parse req.url, true
234 if url_parts.query is undefined
237 rel_path = url_parts.pathname.substr 1
239 if rel_path.substr(0, 11) is 'javascript/'
240 return javascript_handler url_parts.query, res, req, url_parts
241 else if rel_path.substr(rel_path.length - 4) is '.css'
242 res.writeHead 200, 'Content-Type': 'text/css'
243 return css_handler url_parts.query, res, req, url_parts
244 else if rel_path.substr(rel_path.length - 3) is '.js'
245 res.writeHead 200, 'Content-Type': 'text/javascript'
246 return js_handler url_parts.query, res, req, url_parts
247 else if rel_path.substr(rel_path.length - 4) is '/set'
249 req.on 'data', (chunk) ->
252 query = url_parts.query
253 post_args = querystring.parse data
254 for key, parg of post_args
256 return set_handler query, res, req, url_parts
258 else if rel_path.substr(rel_path.length - 4) is '/get'
259 return get_handler url_parts.query, res, req, url_parts
260 else if rel_path.substr(rel_path.length - 4) is '.ico'
264 return html_handler url_parts.query, res, req, url_parts
266 http_server.listen listen_port, "127.0.0.1"
267 console.log "Server running at http://127.0.0.1:#{listen_port}/"