JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
added server.js wrapper
[peach-cgt.git] / client.coffee
1 ###
2 Peach CGT -- Card Game Table simulator
3 Copyright (C) 2011  Jason Woofenden
4 Lincensed under AGPLv3. Source here: https://gitorious.org/peach-cgt
5 ###
6
7 # globals
8 $table = null
9 table_width = 0
10 table_height = 0
11 card_width = 0
12 card_height = 0
13 state = null
14 server_url = null
15 top_card_z = 0 # css z-index of front-most card
16 piles = null
17
18 window.log = []
19 show_message = (txt) ->
20         window.log.push txt
21         if window.log.length > 20
22                 window.log.shift()
23         return
24
25 # timeout function with args in convenient order
26 timeout = (ms, func) -> setTimeout func, ms
27
28 unless Array::shuffle?
29         Array::shuffle = ->
30                 return if @length is 0
31                 top = @length
32
33                 while --top
34                         current = Math.floor(Math.random() * (top + 1))
35                         tmp = @[current]
36                         @[current] = @[top]
37                         @[top] = tmp
38                 return
39
40 new_button = (text) -> $ $ "<div class=\"button\">#{text}</div>"
41
42 # transform coordinates from client-side coords to server-side coords (or back)
43 # this makes it so p2 view everything upside down (mirrored), but still sends coords rightside up
44 flip_x = (x) -> table_width - card_width - x
45 flip_y = (y) -> table_height - card_height - y
46 transform_x = (x) ->
47         return x unless state.agent is 'p2'
48         return flip_x x
49 transform_y = (y) ->
50         return y unless state.agent is 'p2'
51         return flip_y y
52
53 next_card_z = ->
54         top_card_z += 1
55         # p1 gets even numbers, p2 gets odd numbers
56         if state.agent is 'p1'
57                 top_card_z += top_card_z % 2
58         else
59                 top_card_z += 1 - (top_card_z % 2)
60
61         show_message "new z: #{top_card_z}"
62
63         return top_card_z
64
65
66 new_blank_card = (x, y, css_class) ->
67         view = $ $ "<div class=\"blank_card #{css_class}\" style=\"left: #{transform_x x}px; top: #{transform_y y}px; z-index: 0\"></div>"
68         $table.append view
69         return view
70
71 find_pile = (x, y) ->
72         fudge = 40
73         for pile in piles
74                 if -fudge < pile.x - x < fudge and -fudge < pile.y - y < fudge
75                         return pile
76         return null
77
78 in_your_hand = (card) ->
79         return (not (card.pile?)) and ((transform_y card.y) < (card_height * 0.8))
80
81 uninstantiate_card = (card) ->
82         show_message "uninstantiate card #{card.number}"
83         card.view.remove()
84         delete card.view
85
86 instantiate_card = (card) ->
87         show_message "instantiate card #{card.number}"
88         if card.view
89                 die.a.horrible.death()
90
91         text = card.text
92         if card.owner is state.agent
93                 card_class = 'my_card'
94         else
95                 card_class = 'your_card'
96
97         if in_your_hand card
98                 card_class = "#{card_class} your_hand"
99
100         if card.z > top_card_z
101                 top_card_z = card.z
102
103         view = $ $ "<div class=\"card #{card_class}\" style=\"left: #{transform_x(card.x)}px; top: #{transform_y(card.y)}px; z-index: #{card.z}\"><span class=\"cardtext\">#{text}</span></div>"
104         card.view = view
105         button_box = $ $ '<div/>'
106         flip_button = new_button "flip over"
107         mark_button = new_button "mark"
108         flip_button.bind 'click', ->
109                 state.flip state.agent, card.number, ! view.hasClass 'flipped'
110         mark_button.bind 'click', ->
111                 state.mark state.agent, card.number, ! view.hasClass 'marked'
112         button_box.append flip_button
113         button_box.append mark_button
114         view.append button_box
115         if card.marked
116                 view.addClass 'marked'
117         if card.flipped
118                 view.addClass 'flipped'
119         $table.append view
120         view.draggable grid: [20, 20]
121         view.bind 'dragstart', (event, ui) ->
122                 view.css 'z-index': card.z = next_card_z()
123                 if card.pile?
124                         delete card.pile
125                 update_pile_views()
126         view.bind 'dragstop', (event, ui) ->
127                 p = view.position()
128                 x = transform_x(p.left)
129                 y = transform_y(p.top)
130                 pile = find_pile x, y
131                 if pile?
132                         x = pile.x
133                         y = pile.y
134                         pile = pile.key
135                         view.css {left: transform_x(x), top: transform_y(y)}
136                 state.move state.agent, card.number, x, y, card.z, pile
137
138 error_lag = 3
139
140 outgoing_messages = []
141 # message should be [agent, method, args...]
142 # don't forget the agent (state.agent)
143 tell_server = (message) ->
144         outgoing_messages.push message
145         send_updates()
146
147 send_updates = ->
148         return if outgoing_messages.length is 0
149
150         messages = outgoing_messages
151         outgoing_messages = []
152
153         show_message "#{server_url}/set"
154         $.ajax "#{server_url}/set", {
155                 cache: false
156                 data: {
157                         agent: state.agent
158                         game: state.slug
159                         messages: JSON.stringify(messages)
160                 }
161                 type: 'POST'
162                 dataType: 'json'
163                 error: (xhr, status, error) ->
164                         show_message "Network error while sending, you might want to refresh. Trying again in #{error_lag} seconds. (Status: #{status}, Error: #{error})"
165                         for message in messages
166                                 outgoing_messages.unshift message
167                         timeout error_lag * 1000, send_updates
168                         error_lag *= 2
169                 success: (data, status, xhr) ->
170                         show_message "update sent"
171                         error_lag = 3
172         }
173
174 error_lag = 3
175 poll_for_updates = ->
176         $.ajax "#{server_url}/get?agent=#{state.agent}&game=#{state.slug}", {
177                 cache: false
178                 type: 'GET'
179                 dataType: 'json'
180                 error: (xhr, status, error) ->
181                         show_message "Network error, you might want to refresh. Trying again in #{error_lag} seconds. (Status: #{status}, Error: #{error})"
182                         timeout error_lag * 1000, poll_for_updates
183                         error_lag *= 2
184                 success: (data, status, xhr) ->
185                         state.process_messages data
186                         timeout 100, poll_for_updates
187                         error_lag = 3
188         }
189
190 n_cards = (count) ->
191         return "#{count} cards" unless count is 1
192         return "1 card"
193
194 initialize_cards = () ->
195         show_message 'initialize_cards'
196         $('.card').remove()
197         top_card_z = 0
198         # instantiate cards in play
199         hide_deck_designer = false
200         for card in state.cards
201                 if card.owner is state.agent
202                         hide_deck_designer = true
203                 delete card.view
204
205         if hide_deck_designer
206                 $('#deck_designer').remove()
207
208         unless piles?
209                 piles = [ # global
210                         {key: 'p2_draw', x: 140, y: 20, name: "Draw Pile"}
211                         {key: 'p2_discard', x: 20, y: 20, name: "Discard Pile"}
212                         {key: 'p1_draw', x: flip_x(140), y: flip_y(20), name: "Draw Pile"}
213                         {key: 'p1_discard', x: flip_x(20), y: flip_y(20), name: "Discard Pile"}
214                 ]
215                 for pile in piles
216                         if pile.key.substr(0, 2) is state.agent
217                                 css_class = 'my_card'
218                         else
219                                 css_class = 'your_card'
220                         pile.$blank = new_blank_card pile.x, pile.y, css_class
221                         pile.$caption = $ $ "<div class=\"pile_caption\"><div>#{pile.name}:</div><div class=\"n_cards\">#{n_cards 0}</div></div>"
222
223         update_pile_views()
224
225 # also makes sure all non-piled cards are instantiated
226 update_pile_views = ->
227         ps = {}
228         for card in state.cards
229                 if card.pile?
230                         if ps[card.pile]?
231                                 ps[card.pile].total += 1
232                                 if card.z > ps[card.pile].top_z
233                                         if ps[card.pile].top_card.view?
234                                                 uninstantiate_card ps[card.pile].top_card
235                                         ps[card.pile].top_card = card
236                                         ps[card.pile].top_z = card.z
237                                 else if card.view
238                                         uninstantiate_card card
239                         else
240                                 ps[card.pile] = { total: 1, top_card: card, top_z: card.z }
241                 else
242                         # not in a pile
243                         instantiate_card card unless card.view?
244
245         for pile in piles
246                 # where should the caption be?
247                 if ps[pile.key]?
248                         unless ps[pile.key].top_card.view?
249                                 ps[pile.key].top_card.x = pile.x
250                                 ps[pile.key].top_card.y = pile.y
251                                 instantiate_card ps[pile.key].top_card
252                         caption_dest = ps[pile.key].top_card.view
253                 else
254                         caption_dest = pile.$blank
255                 if caption_dest isnt pile.caption_loc
256                         pile.$caption.detach()
257                         caption_dest.append pile.$caption
258                         pile.caption_loc = caption_dest
259
260                 # update caption to show correct number of cards in the pile
261                 card_count = 0
262                 card_count = ps[pile.key].total if ps[pile.key]?
263                 pile.$caption.children('.n_cards').html n_cards card_count
264
265 possible_cards = {}
266
267 valumenous = (val) -> return true unless val is '' or val is ' '
268
269 init_possible_cards = ->
270         for card in window.cs_cards
271                 text = "#{card.cardname} (#{card.faction})"
272                 if valumenous card.attack or valumenous card.defense
273                         text += "  #{card.attack}/#{card.defense}"
274                 text += "<br>#{card.type}"
275                 if valumenous card.subtype
276                         text += " &bull; #{card.subtype}"
277                 text += "<br>cost: #{card.cost} thresh: #{card.threshold}<br>"
278                 text += card.rules
279
280                 summary = text.replace(/<br>/g, "\n")
281
282                 possible_cards[card.id] = {id: card.id, text: text, summary: summary}
283
284
285 init_card_designer = ->
286         show_message 'init_card_designer'
287         cards_in_deck = {}
288         container = $ '#deck_designer'
289         init_possible_cards()
290         ul = $ $ '<ul/>'
291         for key, card of possible_cards
292                 view = $ $ "<li>#{card.summary}</li>"
293                 view.data 'id', card.id
294                 view.bind 'click', ->
295                         $el = $ this
296                         id = $el.data 'id'
297                         if cards_in_deck[id]?
298                                 delete cards_in_deck[id]
299                                 value = false
300                         else
301                                 value = true
302                                 cards_in_deck[id] = true
303                         $el.toggleClass 'in_deck', value
304                 ul.append view
305
306         container.append ul
307
308         submit = $ $ "<div style=\"border: 1px solid black; margin: 0 auto 10px 10px; width: 40px; text-align: center\">Done</div>"
309         submit.bind 'click', ->
310                 $('#deck_designer').remove()
311                 show_message cards_in_deck
312                 cards = []
313                 for key, value of cards_in_deck
314                         card = {
315                                 text: possible_cards[key].text
316                                 owner: state.agent
317                                 pile: "#{state.agent}_draw"
318                                 x: 0
319                                 y: 0
320                                 flipped: true
321                         }
322                         cards.push card
323                         cards.push $.extend {}, card # clone
324                         cards.push $.extend {}, card # clone
325                         cards.push $.extend {}, card # clone
326
327                 # asign z-index in random order
328                 cards.shuffle()
329                 for card in cards
330                         card.z = next_card_z()
331                 show_message cards
332
333                 # let server assign card numbers
334                 tell_server ['new_cards', state.agent, cards]
335
336
337         container.append submit
338
339 new_game_slug = ->
340         charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
341         ret = ''
342         while ret.length < 10
343                 ret += charset[Math.floor(Math.random() * charset.length)]
344         return ret
345
346 init = ->
347         is_new_game = false
348         if window.location.hash? and window.location.hash.length > 0
349                 winloc = window.location.toString()
350                 server_url = winloc.substr 0, winloc.length - window.location.hash.length
351                 game_agent = window.location.hash.substr 1
352                 s = game_agent.split /_/g
353                 if s.length isnt 2
354                         window.location = server_url
355                         # in case that doesn't cause a redirect:
356                         timeout 20, init
357                         return
358                 game = s[0]
359                 me = s[1]
360         else
361                 me = 'p1'
362                 game = new_game_slug()
363                 server_url = window.location.toString()
364                 window.location.hash = "#{game}_#{me}" # TODO should I put a # at the beginning?
365                 is_new_game = true
366
367         if me is 'p1'
368                 other_player = 'p2'
369         else
370                 me = 'p2'
371                 other_player = 'p1'
372
373         $('#main_header').append $ " <span id=\"other_player_url\">Here's the address for the other player in this game with you: <code>#{server_url}##{game}_#{other_player}</code></span>"
374
375         state = window.game_model.new game, me
376         state.on 'move', (agent, card, x, y, z, pile) ->
377                 if z > top_card_z
378                         top_card_z = z
379                 update_pile_views() # ensures instantiation of all visible cards
380                 if @cards[card].view? # the card is visible
381                         # show it face down if it's in the other player's hand
382                         @cards[card].view.toggleClass 'your_hand', in_your_hand @cards[card]
383
384                 if agent is me
385                         tell_server ['move', agent, card, x, y, z, pile]
386                 else
387                         if @cards[card].view?
388                                 @cards[card].view.css "z-index": z
389                                 @cards[card].view.animate { left: "#{transform_x x}px", top: "#{transform_y y}px"}, 800
390         state.on 'mark', (agent, card, state) ->
391                 if @cards[card].view?
392                         @cards[card].view.toggleClass 'marked', state
393                 if agent is me
394                         tell_server ['mark', agent, card, state]
395         state.on 'flip', (agent, card, state) ->
396                 if @cards[card].view?
397                         @cards[card].view.toggleClass 'flipped', state
398                 if agent is me
399                         tell_server ['flip', agent, card, state]
400         state.on 'set_cards', (agent, cards) ->
401                 if agent is me
402                         tell_server ['set_cards', agent, cards]
403                 initialize_cards()
404         state.on 'new_cards', (agent, cards) ->
405                 initialize_cards()
406
407         # timeouts are so browser will stop showing that we're loading
408         if is_new_game
409                 timeout 1, ->
410                         tell_server ['new_game', state.slug, state.agent]
411         timeout 2, init_card_designer
412         timeout 3, poll_for_updates
413         timeout 4, ->
414                 # ask for initial state
415                 tell_server ['send_state', state.agent]
416
417 $ ->
418         $table = $ '#table'
419         table_width = $table.width()
420         table_height = $table.height()
421         card_width = $('#loading_card').outerWidth()
422         card_height = $('#loading_card').outerHeight()
423
424         init()