KLab若手エンジニアの これなぁに?

KLab株式会社の若手エンジニアによる技術ブログです。phpやRubyなどの汎用LLやJavaScript/actionScript等のクライアントサイドの内容、MySQL等のデータベース、その他フレームワークまで幅広く面白い情報を発信します。

2012年01月

テトリス作ってみた

はじめまして!開発部のyatakeと申します。
そして明けましておめでとうございます!今年も宜しくお願い致しますm(_ _)m

さてさて、早速本題です。
ある日気がつくと僕は非常にテトリスが作りたい気分になってたのでサクっと作ってみました。

でけたー!(*´ω`)
落ちる速度を変える処理がうまく動かずちょっとてこずりましたが、テトリスの本体自体は4時間くらいでちょちょいと作れてしまいました。

そして、あまりにもあっさり作れすぎてテトリスに対する気持ちを持て余してる僕はオンライン対戦機能を追加してみる事にしました(´ー`*)

環境としては以下の通りです。

クライアントサイド

  • 対応ブラウザ:chrome
  • 言語:javascript

サーバーサイド

  • 言語:python2.5
  • フレームワーク:tornado

通信部分はWebSocketsを使ってリアルタイムに相手の画面を表示するようにします。

さっそく通信部分の実装を作ってみます。javascriptはこんな感じ。

 // 棒
 var BOUS = [ {
  "p" : {"x" : 4, "y" : 0},
  "O" : {"x" : 1, "y" : 0},
  "layout" : [ [ true, true, true, true ] ]
 }, {
  "p" : {"x" : 5, "y" : 0},
  "O" : {"x" : 1, "y" : 0},
  "layout" : [ [ true, true, true ], [ false, true, false ] ]
 }, {
  "p" : {"x" : 5, "y" : 0},
  "O" : {"x" : 1, "y" : 0},
  "layout" : [ [ false, true, true ], [ true, true, false ] ]
 }, {
  "p" : {"x" : 5, "y" : 0},
  "O" : {"x" : 1, "y" : 0},
  "layout" : [ [ true, true, false ], [ false, true, true ] ]
 }, {
  "p" : {"x" : 5, "y" : 0},
  "O" : {"x" : 1, "y" : 0},
  "layout" : [ [ true, true ], [ true, true ] ]
 }, {
  "p" : {"x" : 5, "y" : 0},
  "O" : {"x" : 1, "y" : 0},
  "layout" : [ [ true, true, true ], [ true, false, false ] ]
 }, {
  "p" : {"x" : 5, "y" : 0},
  "O" : {"x" : 1, "y" : 0},
  "layout" : [ [ true, true, true ], [ false, false, true ] ]
 } ];
 
 // 1秒間に落ちるマス
 var DOWN_P_S = 2;
 
 // マス
 var MASU = {"x" : 10, "y" : 20};
 
 // フィールド
 var FIELD = new Array(MASU["y"]);
 for ( var y = 0; y < MASU["y"]; y++) {
  FIELD[y] = new Array(MASU["x"]);
  for ( var x = 0; x < MASU["x"]; x++) {
   FIELD[y][x] = false;
  }
 }
 
 // 動いてる棒
 MAIN_BO = copy(BOUS[Math.floor(Math.random() * BOUS.length)]);
 
 // レコード(i:一気にi+1個消した回数)
 RECORD = new Array();
 RECORD[0] = 0;
 RECORD[1] = 0;
 RECORD[2] = 0;
 RECORD[3] = 0;
 RECORD[4] = 0;
 
 // ウェブソケット
 var ws = null;
 // 自分のID
 var MY_SESSION_ID = null;
 // 他人の画面
 var MAX_MAPS = 3;
 var MAPS = new Array(); // [{field:マップ情報, message:メッセージ情報, session_id:セッションID},,,,,]
 // ソケット通信で貰ったメッセージ(キュー)
 var MESSAGES_QUE = new Array();
 
 /** 開始処理 **/
 function start() {
  // サーバーソケットセッション確立
  ws = new WebSocket('ws://y-ryo.dyndns.org/ttls');
  ws.onerror = function(e) {
   alert("error");
  };
  ws.onclose = function() {
   alert("server connection closed. (サーバーが死にました。もしくは人数制限で入れません。)");
  };
  ws.onmessage = function(message) {
   msg = JSON.parse(message.data)
 
   // 自分のID
   if (msg.my_session_id) {
    MY_SESSION_ID = msg.my_session_id;
   }
   MESSAGES_QUE.push(msg);
  };
 
  for (var i = 0; i < 10000; i++) {
   if (ws.readyStatus == WebSocket.OPEN) {
    break;
   }
  }
 
  view();
  window.setTimeout(calc, 1000 / DOWN_P_S);
 }
 
 /** 画面表示 **/
 function view() {
  // 棒とマップを合成
  var field = copy(FIELD)
  for ( var y = 0; y < MAIN_BO["layout"].length; y++) {
   for ( var x = 0; x < MAIN_BO["layout"][y].length; x++) {
    if (MAIN_BO["layout"][y][x]) {
     field[MAIN_BO["p"]["y"] - MAIN_BO["O"]["y"] + y][MAIN_BO["p"]["x"] - MAIN_BO["O"]["x"] + x] = true
    }
   }
  }
  // マップを描画
  var field_node = document.getElementById("field");
  field_node.innerHTML = "";
  for (var y = 0; y < MASU["y"]; y++) {
   for (var x = 0; x < MASU["x"]; x++) {
    field_node.innerHTML += field[y][x] ? "■": "□";
   }
   field_node.innerHTML += "
"; } // 他人のマップを描画 for (var i = 0; i < MAX_MAPS; i++) { document.getElementById("field" + i).innerHTML = ""; } for (var i = 0; i < MAPS.length; i++) { field_node = document.getElementById("field" + i) field_node.innerHTML = ""; for (var y = 0; y < MAPS[i].field.length; y++) { for (var x = 0; x < MAPS[i].field[y].length; x++) { field_node.innerHTML += MAPS[i].field[y][x] ? "■": "□"; } field_node.innerHTML += "
"; } } } /** 毎回計算するもの **/ function calc() { MAIN_BO["p"]["y"] += 1; if (is_crash()) { MAIN_BO["p"]["y"] -= 1; save(); MAIN_BO = copy(BOUS[Math.floor(Math.random() * BOUS.length)]); check(); } send_field() while (MESSAGES_QUE.length) { msg = MESSAGES_QUE.shift(); if (msg.func == 'field') { receive_field(msg); } else if (msg.func == 'attack') { receive_attack(msg); } else if (msg.func == 'remove') { receive_remove(msg); } } view() window.setTimeout(calc, 1000 / DOWN_P_S); } /** ぶつかったかのチェック **/ function is_crash() { if (MAIN_BO["p"]["y"] + MAIN_BO["layout"].length - 1 - MAIN_BO["O"]["y"] >= MASU["y"]) { return true; } else if (MAIN_BO["p"]["x"] + MAIN_BO["layout"][0].length - 1 - MAIN_BO["O"]["x"] >= MASU["x"]) { return true; } else if (MAIN_BO["p"]["x"] - MAIN_BO["O"]["x"] <= -1) { return true; } for ( var y = 0; y < MAIN_BO["layout"].length; y++) { for ( var x = 0; x < MAIN_BO["layout"][y].length; x++) { if (FIELD[MAIN_BO["p"]["y"] - MAIN_BO["O"]["y"] + y][MAIN_BO["p"]["x"] - MAIN_BO["O"]["x"] + x] && MAIN_BO["layout"][y][x]) { return true; } } } return false; } /** 棒をフィールドに保存する。 **/ function save() { for ( var y = 0; y < MAIN_BO["layout"].length; y++) { for ( var x = 0; x < MAIN_BO["layout"][y].length; x++) { if (MAIN_BO["layout"][y][x]) { FIELD[MAIN_BO["p"]["y"] - MAIN_BO["O"]["y"] + y][MAIN_BO["p"]["x"] - MAIN_BO["O"]["x"] + x] = true } } } } /** 一本ラインを作れたかどうかのチェック **/ function check() { var del = new Array(); for ( var y = 0; y < MASU["y"]; y++) { var f = true; for ( var x = 0; x < MASU["x"]; x++) { if (!FIELD[y][x]) { f = false; break; } } if (F) { del.push(y); } } for (var i = 0; i < del.length; i++) { for (var y = del[i]; 0 < y; y--) { for (var x = 0; x < MASU["x"]; x++) { FIELD[y][x] = FIELD[y - 1][x]; } } for (var x = 0; x < MASU["x"]; x++) { FIELD[0][x] = false; } } RECORD[del.length - 1] += 1; send_attack(del.length); message(); } /** オブジェクトをコピーする **/ function copy(src) { var ret; if(src.constructor === Array) { ret = []; } else if(src.constructor === Object) { ret = {}; } else { return src; } for(var key in src) { ret[key] = copy(src[key]); } return ret; } /** 回転させる **/ function roll(flg) { for ( var x = 0; x < MAIN_BO["layout"][0].length; x++) { new_layout[x] = new Array(); for ( var y = 0; y < MAIN_BO["layout"].length; y++) { new_layout[x][y] = MAIN_BO["layout"][MAIN_BO["layout"].length - 1 - y][x]; } } } /** レコードを描画 **/ function message() { message_node = document.getElementById("message"); message_node.innerHTML = "session_id:" + MY_SESSION_ID + "
"; message_node.innerHTML += "1line clear count:" + RECORD[0] + "
"; message_node.innerHTML += "2line clear count:" + RECORD[1] + "
"; message_node.innerHTML += "3line clear count:" + RECORD[2] + "
"; message_node.innerHTML += "4line clear count:" + RECORD[3] + "
"; } window.onload = start; // ユーザー操作用 window.onkeydown = function(event) { if (event.keyCode == 37) { MAIN_BO["p"]["x"] -= 1; if (is_crash()) { MAIN_BO["p"]["x"] += 1; } view() return; } else if (event.keyCode == 39) { MAIN_BO["p"]["x"] += 1; if (is_crash()) { MAIN_BO["p"]["x"] -= 1; } view(); return; } else if (event.keyCode == 38) { new_layout = new Array(); roll(true) MAIN_BO["layout"] = new_layout; if (is_crash()) {//TODO roll(true) roll(true) roll(true) } view(); return; } else if (event.keyCode == 40) { DOWN_P_S = 10; } } window.onkeyup = function(event) { if (event.keyCode == 40) { DOWN_P_S = 2; } } /** 自分の状態を送信 **/ function send_field() { // 棒とマップを合成 var field = copy(FIELD) for ( var y = 0; y < MAIN_BO["layout"].length; y++) { for ( var x = 0; x < MAIN_BO["layout"][y].length; x++) { if (MAIN_BO["layout"][y][x]) { field[MAIN_BO["p"]["y"] - MAIN_BO["O"]["y"] + y][MAIN_BO["p"]["x"] - MAIN_BO["O"]["x"] + x] = true } } } ws.send(JSON.stringify({ "func": "field", "session_id": MY_SESSION_ID, "field": field, "message": RECORD })); } /** 自分の状態を送信 **/ function send_attack(num) { if (!num) { return ; } ws.send(JSON.stringify({ "session_id": MY_SESSION_ID, "func": "attack", "attack_lv": num })); } /** 自分が攻撃された事を受け取る **/ function receive_attack(msg) { var attack_lv = msg.attack_lv; // 増加するから全部をずらす for (var i = 0; i < MASU["y"] - 1; i++) { FIELD[i] = FIELD[i + 1] } // 増加する行を生成 var record = Array(MASU["x"]); for (var i = 0; i < MASU["x"]; i++) { if (Math.floor(Math.random() * 10) > msg.attack_lv * 2) { record[i] = true; } else { record[i] = false; } } FIELD[MASU["y"] - 1] = record; } /** 誰かが抜けたことを受け取る **/ function receive_remove(msg) { var session_id = msg.session_id // マップのIDを検索 var map_id = null; for (var i = 0; i < MAPS.length; i++) { if (MAPS[i].session_id == session_id) { map_id = i; break; } } // マップがそもそも無ければ if (map_id == null) { return; } // 削除 MAPS.splice(map_id, 1); } /** 他人の状態を受け取る。 **/ function receive_field(msg) { // マップのIDを検索 var map_id = MAPS.length; for (var i = 0; i < MAPS.length; i++) { if (MAPS[i].session_id == msg.session_id) { map_id = i; break; } } // マップ情報更新 MAPS[map_id] = {}; MAPS[map_id].session_id = msg.session_id; MAPS[map_id].field = msg.field; MAPS[map_id].message = msg.message; }

pythonはこんな感じ。

import tornado.httpserver
import tornado.ioloop
import tornado.web
import tornado.websocket
import simplejson as json
from tornado import options
from hashlib import sha1
import datetime
import random
 
class Model(dict):
    def __getattr__(self, key):
        return dict.__getitem__(self, key)
    def __setattr__(self, key, value):
        return dict.__setitem__(self, key, value)
 
socketConnections = {}
class TTLSSocketHandler(tornado.websocket.WebSocketHandler):
    sessionId = ''
    def open(self, *args, **kwargs):
        print("open: " + self.sessionId)
        if len(socketConnections) > 3:
            self.close()
            return
        session_id = TTLSSocketHandler._create_session()
        self.sessionId = session_id
        socketConnections[session_id] = self
        for key in socketConnections.keys():
            if key != session_id:
                try:
                    socketConnections[key].write_message(json.dumps({"session_id": session_id}))
                except IOError:
                    socketConnections.pop(key)
        self.write_message(json.dumps({'my_session_id': session_id}))
 
    def on_message(self, message):
        print('on_message: ' + self.sessionId)
        msg = Model(json.loads(message));
        while 'func' in msg and msg.func == 'attack' and len(socketConnections) > 1:
            i = random.randint(0, len(socketConnections) - 1)
            sendCon = socketConnections[socketConnections.keys()[i]]
            if sendCon.sessionId != self.sessionId:
                try:
                    sendCon.write_message(message)
                except IOError:
                    socketConnections.pop(sendCon.sessionId)
                return;
 
        for con in socketConnections.values():
            if con != self:
                try:
                    con.write_message(message)
                except IOError:
                    socketConnections.pop(con.sessionId)
 
    @staticmethod
    def _create_session():
        session_id = sha1(str(datetime.datetime.now()) + str(random.randint(1, 10000))).hexdigest()
        while session_id in socketConnections:
            session_id = sha1(str(datetime.datetime.now()) + str(random(1, 10000)))
        return session_id
 
    def on_close(self):
        print('on_close: ' + self.sessionId)
        for con in socketConnections.values():
            if con != self:
                try:
                    con.write_message(json.dumps({
                        'func': 'remove',
                        'session_id': self.sessionId
                    }))
                except IOError:
                    socketConnections.pop(con.sessionId)
        if self in socketConnections:
            socketConnections.pop(self.sessionId)
 
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        print('MainHandler.get(self)')
        self.render("index.html")
 
application = tornado.web.Application([
    (r"/ttls", TTLSSocketHandler),
    (r"/index.html", MainHandler)
], debug=True)
 
if __name__ == "__main__":
    options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(10002)
    tornado.ioloop.IOLoop.instance().start()

動かしてみると、、、、すぐにソケットがcloseしてしまう模様。
socket.jsとか使って別の方法でやろうとしたのですが結局できず。
原因が全くわからないのであまりやった事は無いけどライブラリの中身を解析してみる事に。

Pythonのライブラリは./python/site-packages/以下に入ってるので、その中のtornado/websocket.pyを見てみる。
デバッグ用のprintとか色々埋めこんで通ってる箇所を特定してどんな状態になってるのかを見ていった結果、以下のような処理を発見。

    def _execute(self, transforms, *args, **kwargs):
        self.open_args = args
        self.open_kwargs = kwargs
 
        if (self.request.headers.get("Sec-WebSocket-Version") == "8" or
            self.request.headers.get("Sec-WebSocket-Version") == "7"):
            self.ws_connection = WebSocketProtocol8(self)
            self.ws_connection.accept_connection()

どうも僕が使ってるchromeのバージョン(16.0.912.63 m)ではWebSocket13が使われおり、tornadoのWebSocketは使えない模様。
このままじゃただのテトリスになって全然面白くない!どうにかしたい!!!と思ったのでtornadoを改良する事に。
とりあえず僕の目的はテトリスのオンライン対戦なので規格の事とか一切考えず(ぉ無理やり書き換えてみる。

    def _execute(self, transforms, *args, **kwargs):
        self.open_args = args
        self.open_kwargs = kwargs
 
        print('websocket.py_____9~13 add by ryo')
        if (self.request.headers.get("Sec-WebSocket-Version") == "8" or
            self.request.headers.get("Sec-WebSocket-Version") == "7" or
            self.request.headers.get("Sec-WebSocket-Version") == "13"):
            self.ws_connection = WebSocketProtocol8(self)
            self.ws_connection.accept_connection()

するとここは通り始めた模様。けど違うエラーが出始めた。

[E 120105 23:23:52 iostream:304] Uncaught exception, closing connection.
    Traceback (most recent call last):
      File "/usr/lib/python2.5/site-packages/tornado-2.1.1-py2.5-linux-i686.egg/tornado/iostream.py", line 301, in wrapper
        callback(*args)
      File "/usr/lib/python2.5/site-packages/tornado-2.1.1-py2.5-linux-i686.egg/tornado/websocket.py", line 450, in _on_masking_key
        self._frame_mask = bytearray(data)
    NameError: global name 'bytearray' is not defined
[E 120105 23:23:52 ioloop:410] Exception in callback 
    Traceback (most recent call last):
      File "/usr/lib/python2.5/site-packages/tornado-2.1.1-py2.5-linux-i686.egg/tornado/ioloop.py", line 396, in _run_callback
        callback()
      File "/usr/lib/python2.5/site-packages/tornado-2.1.1-py2.5-linux-i686.egg/tornado/iostream.py", line 301, in wrapper
        callback(*args)
      File "/usr/lib/python2.5/site-packages/tornado-2.1.1-py2.5-linux-i686.egg/tornado/websocket.py", line 450, in _on_masking_key
        self._frame_mask = bytearray(data)
    NameError: global name 'bytearray' is not defined

見た事無いエラー・・・ネットで検索してみると、どうやらpython2.6で使えるようになる関数を使ってる模様。
bytearrayという関数らしい。どういう処理をしているのかというと、文字列をバイト配列にして結合を高速にしているっぽい?
僕の目的はテトリスを動かす事!!!!なので高速とかとりあえずいいやって事でbytearray関数を使わずに普通に文字列結合にしてみる。

    def _on_masking_key(self, data):
        self._frame_mask = [ord(c) for c in data]           # ________ modify by yatake _____ self._frame_mask = bytearray(data)
        self.stream.read_bytes(self._frame_length, self._on_frame_data)
 
    def _on_frame_data(self, data):
        unmasked = [ord(c) for c in data]                   # ________ modify by yatake _____ unmasked = bytesarray(data)
        for i in xrange(len(data)):
            unmasked[i] = unmasked[i] ^ self._frame_mask[i % 4]
        tmp_unmasked = ''                                   # _____ add by yatake
        for c in unmasked:                                  # _____ add by yatake
            tmp_unmasked += unichr(c)                       # _____ add by yatake
        unmasked = tmp_unmasked                             # _____ add by yatake

そして実行!

動いたー!!!(*゚∀゚*)ヒャッホーウィ

完成した直後は二つの画面を操作して一人で(*´Д`)ハァハァ楽しんでましたw

今回テトリス作って思った事。

今までオープンソースで提供されてるソースをいぢって改良したりする事は、「難しそう」「僕のお馬鹿な脳みそじゃ無理!」と勝手に決め付けていたのですが、実際に読んで改良して動かしてみると時間は掛かりましたが想像してたよりもずっと簡単でした!

それに何より動いた時の感動が半端無い!!(*゚∀゚*)ムッハー

テトリス作るのも思っていたよりもだいぶ簡単でしたし、やっぱり何事も実際に触って、体験してみるのが一番だ!という事を実感できました。
皆さんも食わず嫌いせずに、軽い気持ちでもいいのでどんどん新しい事に挑戦していって欲しいなと感じた今日この頃でした。

PS.
全く全然関係ない趣味のお話。
タイバニってアニメお勧めですよ!ヒーロー物!!おじさん萌え!(*´Д`)ハァハァ

エンジニア採用

KLabでは若手エンジニアと一緒に開発してくれる人を募集しています!
当ブログを読んでくれている人、また僕たちの活動に共感してくれる人と是非一緒にコードを書きたいと考えています!!
KLabのビジョンは以下のとおりです。
IT業界で一番ワクワクできて、一番成長できて、一番利益を出す
これは開発部でも同じで、特にワクワクと成長にはこだわっています。
是非、KLab若手エンジニアと一緒にワクワクしながら成長しましょう!

興味を持っていただけた方は以下のリンクからご応募お願いします!
新卒採用
中途採用

このブログについて

このブログはKLab(株)の若手エンジニアによるもので下記内容のものを楽しく書いていくものです。

  • 何気に使っているこの機能って内部的にどうなってるの?
  • 基本的なことだけど深く理解したいから調べたよ!
  • こんなのつくったよ!
 KLab若手エンジニアブログのフッター