JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
Let drags go off the edge
[peach-cgt.git] / server.coffee
1 listen_port = 8333
2 sys = require 'sys'
3 fs = require 'fs'
4 http = require 'http'
5 querystring = require 'querystring'
6 url = require 'url'
7 less = require 'less'
8 coffee = require 'coffee-script'
9 model = require './common.coffee'
10
11 games = {}
12
13 # timeout function with args in convenient order
14 timeout = (ms, func) -> setTimeout func, ms
15
16 css_handler = (args, out, request, url_parts) ->
17         fs.readFile 'style.less', 'utf8', (err, data) ->
18                 if err
19                         return out.end 'Server failed to read style.less'
20                 less.render data, (err, css) ->
21                         if err
22                                 return out.end "Server failed to make css: #{err}"
23                         out.end css
24
25 js_handler = (args, out, request, url_parts) ->
26         convert = false
27         basename = url_parts.pathname.substr 1, (url_parts.pathname.length - 4)
28         if basename is 'client'
29                 filename = 'client.coffee'
30                 convert = true
31         else if basename is 'common'
32                 filename = 'common.coffee'
33                 convert = true
34         else if basename is 'cs_cards'
35                 filename = 'cs_cards.js'
36                 convert = false
37         else
38                 error = "Unknown js basename: #{basename}"
39                 console.log error
40                 out.end(error)
41                 return
42
43         fs.readFile filename, 'utf8', (err, data) ->
44                 if err
45                         return out.end "Server failed to read #{filename}"
46                 if convert
47                         out.end coffee.compile data
48                 else
49                         out.end data
50
51 html_handler = (args, out, request, url_parts) ->
52         fs.readFile 'index.html', 'utf8', (err, data) ->
53                 if err
54                         return out.end 'Server failed to read index.html'
55                 out.end data
56
57 clean_pathname_regex = new RegExp('[^a-zA-Z/_.-]')
58 clean_pathname_regex2 = new RegExp('/[.]')
59 clean_pathname_regex3 = new RegExp('^[.-]')
60 clean_pathname = (str) ->
61         str = str.replace clean_pathname_regex, '_'
62         str = str.replace clean_pathname_regex2, '/_'
63         return str.replace clean_pathname_regex3, '_'
64
65 # serve javascript files from within /usr/share/javascript
66 javascript_handler = (args, out, request, url_parts) ->
67         filename = clean_pathname "/usr/share/#{url_parts.pathname}"
68         fs.readFile filename, 'utf8', (err, data) ->
69                 if err
70                         out.writeHead 404
71                         return out.end "Server failed to read #{filename}"
72                 out.writeHead 200, 'Content-Type': 'text/javascript'
73                 out.end data
74
75
76 get_handler = (args, out, request, url_parts) ->
77         unless args.game?.length
78                 out.writeHead 404, "Content-Type": 'text/plain'
79                 out.end 'Missing (or empty) "game" argument'
80                 return
81
82         unless args.agent is 'p1' or args.agent is 'p2'
83                 out.writeHead 404, "Content-Type": 'text/plain'
84                 out.end '"agent" argument must be set to p1 or p2'
85                 return
86
87         unless games[args.game]?
88                 out.writeHead 404, "Content-Type": 'text/plain'
89                 out.end 'Game not found'
90                 return
91
92         game = games[args.game]
93
94         waiter = games["#{args.agent}_waiter"]
95         if waiter?
96                 waiter.writeHead 200, 'Content-Type': 'text/javascript'
97                 waiter.end '[]'
98
99         game["#{args.agent}_waiter"] = out
100
101         answer_soon game # in case there's something queued already
102
103 set_handler = (args, out, request, url_parts) ->
104         unless args.game?.length
105                 out.writeHead 404, "Content-Type": 'text/plain'
106                 out.end 'Missing (or empty) "game" argument'
107                 return
108
109         unless args.agent is 'p1' or args.agent is 'p2'
110                 out.writeHead 404, "Content-Type": 'text/plain'
111                 out.end '"agent" argument must be set to p1 or p2'
112                 return
113
114         unless args.messages?
115                 out.writeHead 404, "Content-Type": 'text/plain'
116                 out.end '"messages" argument must be set'
117                 return
118
119         unless games[args.game]?
120                 out.writeHead 404, "Content-Type": 'text/plain'
121                 out.end 'Game not found'
122                 return
123
124         game = games[args.game]
125
126         # FIXME add error checking (json validity at least)
127         game.process_messages JSON.parse args.messages
128
129         out.writeHead 200, "Content-Type": 'text/plain'
130         out.end '{"status":0,"text_status":"Success"}'
131
132 # don't call this directly, call answer_soon instead
133 answer_now = (game) ->
134         if game.p1_waiter and game.p1_queue.length
135                 waiter = game.p1_waiter
136                 queue = game.p1_queue
137                 game.p1_waiter = false
138                 game.p1_queue = []
139                 waiter.writeHead 200, 'Content-Type': 'text/javascript'
140                 waiter.end JSON.stringify queue
141         if game.p2_waiter and game.p2_queue.length
142                 waiter = game.p2_waiter
143                 queue = game.p2_queue
144                 game.p2_waiter = false
145                 game.p2_queue = []
146                 waiter.writeHead 200, 'Content-Type': 'text/javascript'
147                 waiter.end JSON.stringify queue
148
149 # this marks a game as "dirty" and makes sure there's exactly one timeout
150 # that'll respond to any clients that are waiting, and now have messages.
151 answer_soon = (game) ->
152         unless game.replier_id
153                 game.replier_id = timeout 1, ->
154                         delete game.replier_id
155                         answer_now game
156
157 forward_events = (message...) ->
158         unless message[1] is 'p1'
159                 @p1_queue.push message
160                 answer_soon this
161         unless message[1] is 'p2'
162                 @p2_queue.push message
163                 answer_soon this
164
165 new_game = (id) ->
166         game = games[id] = model.new 'server'
167         game.p1_waiter = false
168         game.p2_waiter = false
169         game.p1_queue = []
170         game.p2_queue = []
171
172         game.on 'move', (agent, card, x, y, z, pile) ->
173                 forward_events.call this, 'move', agent, card, x, y, z, pile
174         game.on 'mark', (agent, card, state) ->
175                 forward_events.call this, 'mark', agent, card, state
176         game.on 'flip', (agent, card, state) ->
177                 forward_events.call this, 'flip', agent, card, state
178         game.on 'new_cards', (agent, cards) ->
179                 # server assigns card numbers, and tells both clients
180                 # (unlike all other api calls, sending agent expects to get this one back)
181                 forward_events.call this, 'new_cards', 'server', cards
182         game.on 'set_cards', (agent, cards) ->
183                 forward_events.call this, 'set_cards', agent, cards
184         game.on 'send_state', (agent) ->
185                 timeout 10, =>
186                         if agent is 'p1'
187                                 @p1_queue.push ['set_cards', 'server', @cards]
188                                 answer_soon this
189                         if agent is 'p2'
190                                 @p2_queue.push ['set_cards', 'server', @cards]
191                                 answer_soon this
192
193         return game
194
195 test_init = ->
196         test_game = new_game 'test'
197 test_init()
198
199
200 http_server = http.createServer (req, res) ->
201         url_parts = url.parse req.url, true
202         if url_parts.query is undefined
203                 url_parts.query = {}
204
205         rel_path = url_parts.pathname.substr 1
206
207         if rel_path.substr(0, 11) is 'javascript/'
208                 return javascript_handler url_parts.query, res, req, url_parts
209         else if rel_path.substr(rel_path.length - 4) is '.css'
210                 res.writeHead 200, 'Content-Type': 'text/css'
211                 return css_handler url_parts.query, res, req, url_parts
212         else if rel_path.substr(rel_path.length - 3) is '.js'
213                 res.writeHead 200, 'Content-Type': 'text/javascript'
214                 return js_handler url_parts.query, res, req, url_parts
215         else if rel_path.substr(rel_path.length - 4) is '/set'
216                 data = ''
217                 req.on 'data', (chunk) ->
218                         data += chunk
219                 req.on 'end', ->
220                         query = url_parts.query
221                         post_args = querystring.parse data
222                         for key, parg of post_args
223                                 query[key] = parg
224                         return set_handler query, res, req, url_parts
225                 return
226         else if rel_path.substr(rel_path.length - 4) is '/get'
227                 return get_handler url_parts.query, res, req, url_parts
228         else if rel_path.substr(rel_path.length - 4) is '.ico'
229                 res.writeHead 404
230                 return res.end()
231
232         return html_handler url_parts.query, res, req, url_parts
233
234 http_server.listen listen_port, "127.0.0.1"
235 console.log "Server running at http://127.0.0.1:#{listen_port}/"