1 /**
  2  * @fileOverview
  3  * socket.enchant.js
  4  * @version beta (2011/12/03)
  5  * @require enchant.js v0.4.1+
  6  * @author UEI Corporation
  7  *
  8  * @description
  9  * enchant.js extension for online game
 10  *
 11  * @usage
 12  * see http://wise9.jp/archives/5659
 13  */
 14 
 15 (function() {
 16     enchant.socket = {};
 17     enchant.socket.Socket = enchant.Class.create({
 18         initialize: function(gameID, twitterID) {
 19             // timeout(ms)
 20             this.timeout = 10000;
 21 
 22             if (gameID === undefined) {
 23                 if (location.hostname === 'r.jsgames.jp') {
 24                     gameID = location.pathname.match(/^\/games\/(\d+)/)[1];
 25                 } else {
 26                     alert('gameID required. (it will be autodetected if you upload it to 9leap.net)');
 27                 }
 28             }
 29             this.gameID = gameID;
 30             // デバッグ用引数があるかどうか
 31             if (twitterID !== undefined) {
 32                 this.twitterID = twitterID;
 33             }
 34 
 35             /*
 36              * 変数初期化
 37              */
 38             var socket = this;
 39             this.matching = false; // マッチ中かどうか
 40             this.timer = undefined; // timeout用タイマー
 41 
 42 
 43             /*
 44              * 通信系定義
 45              * 1番アウトプットに近いレイヤーです
 46              * XHRとPusherを隠蔽します
 47              * @type {connection}
 48              */
 49             var connection = function(pusherKey, url) {
 50                 var pusher = new Pusher(pusherKey);
 51 
 52                 var channelList = {};
 53                 var callbackList = {};
 54 
 55                 // JSONP用CallBack
 56                 window._onlineCallback = function(callbackPath) {
 57                     return function(data) {
 58                         if (typeof data === 'string') data = JSON.parse(data);
 59                         callbackList[callbackPath](data);
 60                     };
 61                 };
 62 
 63                 return {
 64                     // JSONPでAPIアクセス
 65                     send: function(roomType, apiName, option, func) {
 66                         callbackList[roomType + '-' + apiName] = func;
 67                         var jsonpCallback = '?callback=_onlineCallback("' + roomType + '-' + apiName + '")';
 68                         console.log(socket.gameID);
 69                         var src = [url, 'api/online/', socket.gameID, '/', roomType, '/', apiName, '.json', jsonpCallback, '&twitterID=' + socket.twitterID].join('');
 70 
 71                         if (option)
 72                             for (var key in option) src += '&' + key + '=' + option[key];
 73 
 74                         var script = document.createElement('script');
 75                         script.type = 'text/javascript';
 76                         script.src = src;
 77                         document.head.appendChild(script);
 78                     },
 79                     // WebSocketから受信する
 80                     receiveBinder: function(roomType, channelID) {
 81                         var pusherChannelID = socket.gameID + '-' + roomType + '-' + channelID;
 82                         channelList[pusherChannelID] = pusher.subscribe(pusherChannelID);
 83                         return function(channel) {
 84                             return function(eventName, func) {
 85                                 channel.bind(eventName, function(data) {
 86                                     if (typeof data === 'string') data = JSON.parse(data);
 87                                     func(data.data);
 88                                 });
 89                             };
 90                         }(channelList[pusherChannelID]);
 91                     },
 92                     releaseBinder: function(roomType, channelID) {
 93                         var pusherChannelID = socket.gameID + '-' + roomType + '-' + channelID;
 94                         channelList[pusherChannelID] = pusher.unsubscribe(pusherChannelID);
 95                     }
 96                 };
 97             }('551141852cce2fe668d5', 'http://9leap.net/');
 98 
 99 
100             /*
101              * Online APIのラッパー
102              * サーバーで実装しているAPIを関数化しています
103              * また、Pusherのbinderをpatt throughしています
104              */
105             var apiClosure = function(connection) {
106                 return function(roomType) {
107                     var send = function(apiName, option, callback) {
108                         connection.send(roomType, apiName, option, callback);
109                     };
110 
111                     var retObject = {
112                         join: function(channelID, callback) {
113                             if (channelID === undefined) channelID = -1;
114 
115                             if (channelID === -1)
116                                 send('join', {}, callback);
117                             else
118                                 send('join', {channelID: channelID}, callback);
119                         },
120                         list: function(callback) {
121                             send('list', {}, callback);
122                         },
123                         broadcast: function(data, channelID, callback) {
124                             send('broadcast', {data: JSON.stringify(data), channelID: channelID}, callback);
125                         },
126                         quit: function(channelID, callback) {
127                             send('quit', {channelID: channelID}, callback);
128                             connection.releaseBinder(roomType, channelID);
129                         },
130                         pong: function(channelID, callback) {
131                             send('pong', {channelID: channelID}, callback);
132                         },
133                         binder: function(channelID) {
134                             return connection.receiveBinder(roomType, channelID);
135                         }
136                     };
137                     return retObject;
138                 };
139             }(connection);
140 
141             /*
142              * room処理のベースになるオブジェクト
143              * broadcastパケットに意味付け(送り元、宛先の付与)等を行っています。
144              * また、Pusherへの接続なども隠蔽しています。
145              */
146             var roomClosure = function(apiClosure) {
147                 return function(roomType) {
148                     var api = apiClosure(roomType);
149                     var channelID = -1;
150                     var bind = {};
151 
152                     var toPrev = '';
153 
154                     var retObject = {
155                         // public
156                         api: api,
157                         setToPrev: function(_toPrev) {
158                             toPrev = _toPrev;
159                         },
160                         onreceive: function(data) {
161 
162                         },
163                         onjoin: function() {
164 
165                         },
166                         onresult: function() {
167 
168                         },
169                         onquit: function() {
170 
171                         },
172                         /*
173                          * 一般的なjoin実装
174                          * channelIDが指定されれば送り、指定されなければ送らない
175                          * また、pusherへのbroadcastAPIからのbindingもここで処理しています。
176                          * channelIDが指定されれば送り、指定されなければ送らない
177                          */
178                         join: function(_channelID) {
179                             var joinCallback = function(data) {
180                                 if (data.code === 200) {
181                                     socket.twitterID = data.twitterID;
182                                     channelID = data.channelID;
183                                     bind = api.binder(channelID);
184                                     bind('MSG', function(data) {
185                                         if (data.playerID !== socket.twitterID)
186                                             return;
187                                         retObject.onreceive(data);
188                                     });
189                                     bind('PING', function(data) {
190                                         api.pong(channelID, function() {
191                                         });
192                                     });
193 
194                                     retObject.onjoin(data);
195                                 } else {
196                                     console.log('error: cannot connect');
197                                 }
198                             };
199 
200                             if (_channelID === undefined) {
201                                 api.join(-1, joinCallback);
202                             } else {
203                                 api.join(_channelID, joinCallback);
204                             }
205                         },
206                         // broadcastパケットに送信元、送信先を付与します。
207                         broadcast: function(data, to) {
208                             var broadcastCallback = function(data) {
209                                 retObject.onresult(data);
210                             };
211 
212                             if (to === undefined)
213                                 if (toPrev === '') {
214                                     console.log('error');
215                                     return;
216                                 } else {
217                                     data.playerID = toPrev;
218                                 }
219                             else
220                                 data.playerID = to;
221 
222                             toPrev = data.playerID;
223 
224                             data.myPlayerID = socket.twitterID;
225                             api.broadcast(data, channelID, broadcastCallback);
226                         },
227                         quit: function() {
228                             api.quit(channelID, retObject.onquit);
229                         }
230                     };
231                     return retObject;
232                 };
233             }(apiClosure);
234 
235 
236             /*
237              * lobbyの実体
238              * 1:1 でのランダムマッチの実装を行っている部分
239              * onjoin等イベントリスナーになっている箇所を書き換えるとlobbyから勝手にgameRoomへ入る処理が崩れるのでご注意を
240              */
241             var lobby = roomClosure('lobby');
242 
243             /*
244              * 自分が入室したときに呼ばれる
245              * マッチングできる人がいればランダムマッチング
246              * いなければ、JOINイベント待機
247              */
248             lobby.onjoin = function(data) {
249                 // 対戦可能プレーヤの確認
250                 if (data['playerList'].length !== 0) {
251                     setTimeout(function() {
252                         socket.matching = true;
253                         lobby.broadcast({op: 'apply'}, data['playerList'][Math.floor(Math.random() * data['playerList'].length)]);
254                         socket.timer = setTimeout(function() {
255                             socket.matching = false;
256                         }, socket.timeout);
257                     }, 1000);
258                 }
259             };
260 
261             /*
262              * メッセージを受信したときに呼ばれる
263              * マッチング確認
264              * 自分宛でなければ無視
265              */
266             lobby.onreceive = function(data) {
267                 if (data.playerID !== socket.twitterID)
268                     return;
269 
270                 if (data.op === 'apply' && socket.matching === false) {
271                     socket.matching = true;
272                     setTimeout(function() {
273                         lobby.broadcast({op: 'accept'}, data.myPlayerID);
274                         socket.timer = setTimeout(function() {
275                             socket.matching = false;
276                         }, socket.timeout);
277                     }, 1000);
278                 } else if (data.op === 'accept' && socket.matching === true) {
279                     clearTimeout(socket.timer);
280                     gameRoom.onjoin = function(joinData) {
281                         lobby.broadcast({op: 'goGameRoom', channelID: joinData.channelID});
282                         lobby.quit();
283                         socket.matching = false;
284                         gameRoom.member[0] = [data.myPlayerID];
285                     };
286                     gameRoom.join();
287                 } else if (data.op === 'goGameRoom' && socket.matching === true) {
288                     console.log(data);
289                     clearTimeout(socket.timer);
290                     lobby.quit();
291                     socket.matching = false;
292                     gameRoom.member[0] = [data.myPlayerID];
293                     gameRoom.onjoin = function() {
294                         gameRoom.broadcast({op: 'ready', isLead: false}, data.myPlayerID);
295                         gameReceiveList['ready']({op: 'ready', isLead: true});
296                     };
297                     gameRoom.join(data.channelID);
298                 } else {
299                     console.log('error : ' + data.op);
300                     console.log(socket.matching);
301                 }
302             };
303 
304             /*
305              * デフォルトの動作ではなく、channelIDが指定されなかったらすでにchannelが存在しているかどうか確認して、あれば入るような実装。
306              */
307             lobby.join = function(join) {
308                 return function(channelID) {
309                     if (channelID === undefined) {
310                         // 既存のチャネルの確認、あればつなぐ。なければ新規取得
311                         lobby.api.list(function(data) {
312                             if (data['code'] == 403) {
313                                 window.location.replace('http://9leap.net/api/login?after_login=' + window.location.href);
314                                 return;
315                             }
316                             if (data['channelList'].length === 0)
317                                 join();
318                             else
319                                 join(data['channelList'][0]);
320                         });
321                     } else {
322                         join(channelID);
323                     }
324                 };
325             }(lobby.join);
326             this.lobby = lobby;
327 
328 
329             /*
330              * gameRoomオブジェクト定義
331              * ユーザーによるイベント管理をbroadcast上のパケットに構築
332              * そのイベント管理方法等を付与した
333              */
334             var gameRoom = roomClosure('gameRoom');
335             var gameReceiveList = {};
336             gameReceiveList['ready'] = gameRoom.onready;
337             gameRoom.onready = function() {
338             };
339             gameRoom.member = [];
340             gameRoom.onreceive = function(data) {
341                 var op = data.op;
342                 //delete data.op;
343                 if (op === 'ready') {
344                     gameRoom.setToPrev(data.myPlayerID);
345                 }
346                 gameReceiveList[op](data);
347             };
348             gameRoom.push = function(eventName, data) {
349                 data.op = eventName;
350                 gameRoom.broadcast(data);
351             };
352             gameRoom.addEventListener = function(eventName, func) {
353                 gameReceiveList[eventName] = func;
354             };
355             this.gameRoom = gameRoom;
356         }
357     });
358 
359 })();