2012/02/08

【その後】 やったー Titanium Mobile でも Socket.io が動いたよー \(^o^)/

ポスト @ 23:43:17 | , , ,     

久々に書いてます。

前回 やったー Titanium Mobile でも Socket.io が動いたよー \(^o^)/ って喜んでたら、socket.io のバージョンアップでgdgdしてしまっている間に
masuidrive さんのところで、ti-websocket-client が作られてたので、僕のトコで作ってた socket.io-titanium にそのまま放り込んでまま放置プレイにしてたので、ちょっと書いてみようかなと。

チャットルームみたいなものを作ってみようと思います。
結論から書くと、下の動画みたいになってます。
また、コードは https://github.com/nowelium/socket.io-titaniumにそのまま置いてます。

チャットルームみたいなチャンネル付きチャット

サーバの実装(node.js)

node.js + socket.io でこんなコードを用意します。

var io = require('socket.io').listen(8080);

var archiveMessages = {};
var channels = ['foo channel', 'bar channel'];

var chat = io.of('/chat');
chat.on('connection', function(socket){
  console.log('connected: %s', socket.id);

  // push available channel list
  socket.emit('available_channel', channels);

  socket.on('join', function(value){
    console.log('%s joined channel: %s', socket.id, value.channelId);

    socket.join(value.channelId);
    socket.set('channel_id', value.channelId, function(){
      var messages = archiveMessages[value.channelId] || [];
      socket.emit('joined', messages);
      socket.broadcast.to(value.channelId).emit('user:join', {
        id: socket.id
      });
    });
  });

  socket.on('post', function(message){
    socket.get('channel_id', function(err, channelId){
      console.log('%s says<%s channel>: %s', socket.id, channelId, message);

      if(!(channelId in archiveMessages)){
        archiveMessages[channelId] = [];
      }
      archiveMessages[channelId].push(message);

      socket.emit('posted', {
        message: message
      });
      socket.broadcast.to(channelId).emit('user:message', {
        id: socket.id,
        message: message
      });
    });
  });

  socket.on('disconnect', function(){
    console.log('%s disconnected', socket.id);
    socket.get('channel_id', function(channelId){
      socket.leave(channelId);
      socket.broadcast.to(channelId).emit('user:leave', {
        id: socket.id
      });
    });
  });
});

ここで重要なのは、socket.join(channel) なところと、 socket.broadcast.to(channel)、そして、socket.leave(channel) なところでしょうか。
これを使うことで、同じ channel に join してる人に broadcast できたりするので、今回はここからスタート

ルームを取得する

こっから titanium になります。

server 側で available_channel を socket.connection イベント時(接続したとき) に返してもらっているので、それを素直に読み取ってTableViewに詰めます。

var win = Titanium.UI.currentWindow;

var io = require('socket.io-titanium');
var socket = io.connect('169.254.10.100:8080');

var channelTable = Titanium.UI.createTableView();

var chat = socket.of('/chat');
chat.on('available_channel', function(channels){
  //
  // channel view
  //
  var rows = channels.map(function(channel){
    var row = Titanium.UI.createTableViewRow({
      title: channel
    });
    row.addEventListener('click', function (){
      channelTable.fireEvent('click:channel', {
        channelId: channel
      });
    });
    return row;
  });
  channelTable.setData(rows);
});
win.add(channelTable);

socket.ioの通信はJSONをそのまま扱ってくれるので、JSON.parseとか気にしなくていいので楽ですね。

ルームチャットを実装する

サーバ側では join イベントを待っているので、win.open 時に呼び出してあげてます。
また、サーバ側から送られてくるイベントもの(ここでは user:join や user:message など)は、一般的な EventEmitter 的な待ち受けを行い、イベントを受け取ったら処理をする。という一般的なイベントドリブンな感じで実装します。

channelTable.addEventListener('click:channel', function(evt){
  //
  // chat view
  //
  var chatWindow = Titanium.UI.createWindow({
    title: 'room: ' + evt.channelId + ' chat'
  });

  var lastRowIndex = 0;
  var chatTable;
  if(/android/i.test(Titanium.Platform.osname)){
    var input = Titanium.UI.createSearchBar({
      showCancel: false
    });
    chatTable = Titanium.UI.createTableView({
      search: input
    });
    chatWindow.add(chatTable);
  } else {
    var header = Titanium.UI.createView({
      top: 0,
      height: 60,
      width: win.width,
      borderWidth: 2,
      borderColor: '#333'
    });
    var input = Titanium.UI.createTextField({
      top: 10,
      height: 40,
      width: 200,
      color: '#333',
      hintText: 'message here'
    });
    chatTable = Titanium.UI.createTableView({
      top: 60,
      height: win.height - 60,
      width: win.width
    });

    chatWindow.add(header);
    chatWindow.add(input);
    chatWindow.add(chatTable);
  }

  chat.on('joined', function (messages){
    var rows = messages.map(function(message){
      return Titanium.UI.createTableViewRow({
        title: message,
        color: '#999'
      });
    });
    var section = Titanium.UI.createTableViewSection({
      headerTitle: 'archived message(s)'
    });
    rows.forEach(function(row){
      section.add(row);
    });
    chatTable.setData([section]);
    // delay
    setTimeout(function (){
      chatTable.scrollToIndex(rows.length - 1, { animated: false });
      lastRowIndex = rows.length;
    }, 100);
  });
  var addMessage = function(message){
    var row = Titanium.UI.createTableViewRow({
      title: message
    });
    chatTable.appendRow(row, { animated: true });
    // delay
    setTimeout(function (){
      chatTable.scrollToIndex(lastRowIndex, { animated: true });
      lastRowIndex = lastRowIndex + 1;
    }, 100);
  };
  chat.on('posted', function(value){
    return addMessage('you posted: ' + value.message);
  });
  chat.on('user:join', function(value){
    return addMessage(value.id + ' joined this channel');
  });
  chat.on('user:leave', function(value){
    return addMessage(value.id + ' leaved this channel');
  });
  chat.on('user:message', function(value){
    return addMessage(value.id + ' says ' + value.message);
  });
  input.addEventListener('return', function (){
    var messageValue = input.value;
    if(/^\s+$/.test(messageValue)){
      return;
    }

    chat.emit('post', messageValue);
    input.value = '';
  });
  chatWindow.addEventListener('open', function (){
    chat.emit('join', {
      channelId: evt.channelId
    });
  });
  chatWindow.addEventListener('close', function (){
    chat.disconnect();
  });

  chatWindow.add(chatTable);
  return Titanium.UI.currentTab.open(chatWindow);
});

うーん。短い。(色々冗長だけど)
基本的な部分はこんな感じで実装しました。

ついでにwebViewでも実装を書いてみる

これも github の方にはそのまま入ってますが、一応記述するとするとこんな感じ

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>chat sample</title>
    <style>
    #rooms button {
      display: block;
    }
    </style>
    <script type="text/javascript" src="../socket.io/dist/socket.io.js"></script>
    <script type="text/javascript">
    var $ = function(id){
      return document.getElementById(id);
    };
    window.addEventListener('load', function (){
      var socket = io.connect('169.254.10.100:8080');

      var chat = socket.of('/chat');
      chat.on('available_channel', function(channels){
        channels.forEach(function (channelId){
          var button = document.createElement('button');
          button.appendChild(document.createTextNode(channelId));

          $('rooms').appendChild(button);
          button.addEventListener('click', function(){
            $('chat').style.display = '';
            chat.emit('join', {
              channelId: channelId
            });
          });
        });
      });

      var addMessage = function(message){
        var li = document.createElement('li');
        li.appendChild(document.createTextNode(message));
        $('messages').appendChild(li);
      };
      chat.on('joined', function(messages){
        messages.forEach(function(message){
          var li = document.createElement('li');
          li.appendChild(document.createTextNode(message));
          $('archives').appendChild(li);
        });
      });
      chat.on('posted', function(value){
        return addMessage('you posted: ' + value.message);
      });
      chat.on('user:join', function(value){
        return addMessage(value.id + ' joined this channel');
      });
      chat.on('user:leave', function(value){
        return addMessage(value.id + ' leaved this channel');
      });
      chat.on('user:message', function(value){
        return addMessage(value.id + ' says ' + value.message);
      });

      $('submit').addEventListener('click', function (){
        var messageValue = $('message').value;
        if(/^\s+$/.test(messageValue)){
          return;
        }

        chat.emit('post', messageValue);
        $('message').value = '';
      });
    });
    </script>
  </head>
  <body>
    <div id="rooms">
    </div>
    <div id="chat" style="display:none">
      <input type="text" id="message" value="" placeholder="message here" />
      <input type="submit" id="submit" value="send" />
      <p>archives</p>
      <ul id="archives" style="color: #999"></ul>
      <p>messages</p>
      <ul id="messages"></ul>
    </div>
  </body>
</html>

手抜きながら短いですね。

FAQ. 169.254.10.100 ってIPアドレスは何?

これは、iphonesimulator とか android simulator で 127.0.0.1 とか 使えないので、 localhost(127.0.0.1) の alias を切ってます(OSX)

こんな感じのコマンドで割り当てます

shell > sudo ifconfig lo0 alias 169.254.10.100 netmask 0xffffff

その他。

これはまったく、未確認情報で、hybiの実装を見てないんだけど、socket.io が parse error を吐く!ってことがあったら、https://gist.github.com/1768174 のpatch を使ってみると治ることがあるらしい。

ということで、ひさびさにブログを更新した今日この頃。

2011/08/22

javascripterになろう。の巻 3章

ポスト @ 1:02:14 , 修正 @ 2011/08/22 1:04:15 | , ,     

前回「紛失した。」って書いてあったハズなのに、とある人(@hika69)からの熱い熱意によって、描き起こしました。

例によって、独自解釈部分を含む + 校正はしてないので、間違っている点があるかも。

2011/07/31

javascripterになろう。の社内勉強会

ポスト @ 23:48:24 | , ,     

社内で使っているモノにTitaniumとかSenchaとか増えてきたので、javascriptをより知ってもらおうと思って、社内勉強会で使った資料とか、今さら

地震とか色々あって3章が紛失してしまったけど、今のところ3章まで話したハズ。
内容の校正とかしてないから、間違ってる点があるかも。

2011/06/13

みんな iolangage をやれば解決だ

ポスト @ 3:07:47 , 修正 @ 2011/06/13 3:17:48 | ,     

気がつくと今年も6月の中盤にさしかかろうとしてますね。
そんな中、Scalaの(いろいろと)話題があって、僕も数ヶ月前まではScalaで開発してたので、そういうのみてると何とも言えない気持ちになる。
というか、自分も使ってる言語が炎上(?)してるのはなかなか見るに見兼ねるのである(昔はPHPの時もそういうのが嫌だった)

(中略)

ということで、ここでは Io language の素晴らしさに浸ってみようと思う(※ちなみに、ここでは Io 20080120ベースです)

Io Language もマルチパラダイム言語だよ!

Scalaの説明で、マルチパラダイム言語 って説明されてたりするけど、それなら Io Language にも当てはまる

Actorが使える

Scala(もとい Act1, Erlang)で有名になった Actor は Io Language でも使える

hoge := Object clone
hoge foo := method(a, b,
  (a + b) println
)

async1 := hoge @@foo(1, 2)
async2 := hoge @@foo(3, 4)
yield
yield

Prototypeが使える

Javascript(Self) で有名な Prototype は Io Language でも使える

Hoge := Object clone do (
  foo := method("hello world")
)

bar := Hoge clone
bar foo println

// appendProto
baz := Object clone
baz appendProto(Hoge)
baz foo println

メッセージ指向でもある

Small Talkのようなメッセージ指向のやりとりもできる

a := if(1 < 2, "hello world", "foo bar")
a split foreach(v, v uppercase println)
// ==> HELLO
//         WORLD

LISP風である

関数プログラミングとかみたいに "("と")" で十分プログラミングできる。 ちなみに記号といえば @ と @@ くらいだったりする

Object do (
  Lobby setSlot("Hoge", clone do (
    setSlot("foo", method("hello world"))
    setSlot("bar", method("foo bar"))
  ))
  writeln((block(
    "#{foo}" interpolate
  ) setScope(Hoge) call))
)

//
// もう少し崩すと
//
Object do (
  writeln(block(
    "#{foo}" interpolate
  ) setScope(Lobby setSlot("Hoge", clone do (
    setSlot("foo", method("hello world"))
    setSlot("bar", method("foo bar"))
  ))) call)
)

すべてがオブジェクトである

インスタンスベースであるので、こまけぇことが気になりません

Range
Hoge := Object clone do (
  foo := 1 to(10)
)
hoge := Hoge clone do (
  bar := foo last to("20" asNumber)
)
hoge bar foreach(v, v println)
// foo のインスタンスが共有されてるので rewind...
Hoge foo rewind foreach(v, v println)

※いい例が出てこない...

C言語風である

Cスタイルの記法ならいくつか用意されてる(return とか)

a := 1;
b := 10;

a = 1 + 2;
b = a * 10;

for(i, a, b,
  write(i);
);

if(a < b) then (
  return write("a");
) else (
  return write("b");
)

※ C言語のようなスタイルが思いつかなかったので、手抜きです...

VMだってある

VMが必要なら、小さいけどVMは付いている。Collectorも用意されてる(※むかしやったやつ)

//Collector setDebug(true)
aaa := method(
    Hoge := Object clone do(
        foo ::= nil
    )
    Foo := Object clone do(
        hoge ::= nil
    )

    h := Hoge clone setFoo(Foo clone setHoge(Hoge clone))
    f := Foo clone setHoge(Hoge clone setFoo(Foo clone))
    h clone setFoo(f clone setHoge(h clone))
    f clone setHoge(h clone setFoo(f clone))

    l := List clone
    for(i, 0, 100, l append(h, f, i * 0.987654321))
)
test := method(100 repeat(aaa))

writeln("time: ", Date clone cpuSecondsToRun(test), " seconds")
writeln("mpa \t as \t mb \t time \t mbt \t gc")

lastTimeUsed := 0.0

list(1.01, 1.05, 1.1, 1.2, 1.5, 1.7, 2, 4) foreach(as,
    list(0.01, 0.1, 1, 2, 4, 16) foreach(mpa,
        Collector setMarksPerAlloc(mpa)
        Collector setAllocatedStep(as)
        
        time := Date clone cpuSecondsToRun(test)
        mb := (Collector maxAllocatedBytes / 1000000) asString(0, 2)
        
        writeln(
            Collector marksPerAlloc asString(0, 2), "\t",
            Collector allocatedStep asString(0, 2), "\t",
            mb, "\t", 
            time asString(0, 2) , "\t", 
            (mb asNumber * time) asString(0, 2), "\t", 
            (100 * (Collector timeUsed - lastTimeUsed) / time) asString(2, 1), "%"
        )
        
        lastTimeUsed = Collector timeUsed

        // Collector showStats
        writeln("collected items: ", Collector collect)
        Collector resetMaxAllocatedBytes
    )
    "" println
)

new?こちとらcloneで2文字も多い

clone地獄。ときどき with も使える。それでも4文字...。

a := List clone
a append(1)
a append(2)
a println

List with(3, 4) println
list(5, 6, 7) println

Object squareBrackets := Object getSlot("list");
[1, 2, 3] println

b := Map clone
b atPut("foo", 1)
b atPut("bar", 2)
b asObject println

Object curlyBrackets := method(
  map := Map clone
  call message arguments foreach(arg, arg setName("atPut"); map doMessage(arg))
  map
)
{foo := 1, bar := 2} asObject println

with が使えたり、listのようにシンタックスシュガーがあるけど、存在しない場合は自分で作るしかない。それが io style

まとめ

ということで、いろいろできる Io Language
なんだ Io Language すばらしいじゃん。というお話しでした。

その他

ブラウザで動かせない?

いつか誰かが動かせるようにしてくれる。と期待中

※ 完全に放置してるけど、Rhino で io.js をつくってみたよ。ただ途中で秋田

JVMで動かせない?

Iokeなんてものがあるらしい。

※ むかーし JavaCCの勉強で作ろうとした残骸 JoParser.jjt。これも途中で放置

2011/05/07

やったー Titanium Mobile でも Socket.io が動いたよー \(^o^)/

ポスト @ 22:10:29 , 修正 @ 2011/05/07 22:21:03 | ,     

socket.ioでブラウザ依存の部分を使わないようにwrapper書いただけです。。
ref - https://github.com/nowelium/socket.io-titanium

ちなみに、module版が既にありました
ref - https://github.com/saiten/TiSocketIO

使い方

socket.io(socket.io-nodeじゃない方)を含むので、--recursiveオプションでgit cloneする

git clone git@github.com:nowelium/socket.io-titanium.git --recursive

上のリポジトリを落としてきたら、こんな構成になってるので、example-nodejs-server/server.js を起動する(socket.ioはnpmとかでインストールしとくこと)

prj/
 - README
 - LICENSE
 - tiapp.xml
 - example-nodejs-server/
   - server.js
 - Resources/
  - app.js
  - socket.io-titanium.js
  - socket.io/
    - package.json
    - socket.io.js
    - lib/
      - io.js
      - socket.js
      - util.js
      - transport.js
      - transports/
        - xhr.js
        - xhr-polling.js

serverはこんなコードで

var http = require('http');

var server = http.createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('hello world');
});
server.listen(8080);

var io = require('socket.io');
var socket = io.listen(server);
socket.on('connection', function(client){
  console.log('connect : ' + client.sessionId);
  client.on('message', function (message){
    console.log('client message: ' + message);
    client.send('rep:' + message);
  });
  client.on('disconnect', function(){
    console.log('disconnect : ' + client.sessionId);
  });
});

こんな感じで起動しておく

shell > node example-nodejs-server/server.js

Resources/app.jsはこんな感じにしてます。IPアドレスの部分は適宜必要に応じて変更してください

Titanium.UI.setBackgroundColor('#FFF');

require('socket.io-titanium');

var socket = new io.Socket('169.254.10.100', { port: 8080 });
socket.connect();
socket.send('hello world!!');
socket.on('message', function (message){
  Titanium.API.debug('got message: ' + message);
});

var win = Titanium.UI.createWindow({
  barColor: '#369',
  tabBarHidden: true,
  title: 'demo'
});
win.add(Titanium.UI.createLabel({
  text: 'check console output'
}));

var tabGroup = Titanium.UI.createTabGroup();
tabGroup.addTab(Titanium.UI.createTab({
  window: win
}));  
tabGroup.open();

後は Titanium Developer に Import Project して起動

こんなログがみえるハズ

起動してほっておくと、server側はこんな感じのログがでます

7 May 22:02:53 - socket.io ready - accepting connections
7 May 22:03:03 - Initializing client with transport "xhr-polling"
7 May 22:03:03 - Client 07882023160345852 connected
connect : 07882023160345852
client message: hello world!!
7 May 22:03:14 - Initializing client with transport "xhr-polling"
7 May 22:03:14 - Client 9657788660842925 connected
connect : 9657788660842925
disconnect : 07882023160345852
7 May 22:03:21 - Client 07882023160345852 disconnected
7 May 22:03:25 - Initializing client with transport "xhr-polling"
7 May 22:03:25 - Client 7632819225545973 connected
connect : 7632819225545973

クライアント(iphonesimulator)はこんなログを出してます

[INFO] Application started
[DEBUG] reading stylesheet from: /path/to/Library/Application Support/iPhone Simulator/4.2/Applications/36A39314-2736-4755-95B2-A459111F09F1/socketio.titanium.app/stylesheet.plist
[INFO] socketio.titanium/1.0 (1.6.2.878906d)
[DEBUG] Analytics is enabled = YES
2011-05-07 22:03:03.737 socketio.titanium[43279:207] [DEBUG] Reachability Flag Status Change: -R -----l- networkStatusForFlags
[DEBUG] loading: /path/to/Resources/app.js, resource: path/to/Resources/app_js
[DEBUG] loading: /path/to/Resources/socket.io-titanium.js, resource: path/to/Resources/socket_io-titanium_js
[DEBUG] loading: /path/to/Resources/socket.io/lib/io.js, resource: path/to/Resources/socket_io/lib/io_js
[DEBUG] loading: /path/to/Resources/socket.io/lib/socket.js, resource: path/to/Resources/socket_io/lib/socket_js
[DEBUG] loading: /path/to/Resources/socket.io/lib/transport.js, resource: path/to/Resources/socket_io/lib/transport_js
[DEBUG] loading: /path/to/Resources/socket.io/lib/transports/xhr.js, resource: path/to/Resources/socket_io/lib/transports/xhr_js
[DEBUG] loading: /path/to/Resources/socket.io/lib/transports/xhr-polling.js, resource: path/to/Resources/socket_io/lib/transports/xhr-polling_js
[DEBUG] application booted in 81.870973 ms
[DEBUG] got message: rep:hello world!!
2011-05-07 22:03:08.761 socketio.titanium[43279:9c03] [DEBUG] Reachability Flag Status Change: -R -----l- networkStatusForFlags

ということで、動いてくれたのでよしとする。

2011/05/06

JavaScriptでRPCゲーム的な。その3

ポスト @ 0:57:58 | ,     

その2からの続き
レベルアップするようにしてみる。難易度とかゲームバランスとかって難しくなってきた

Function.prototype.bind = function(obj){
  var __method__ = this;
  var __args__ = Array.prototype.slice.call(arguments);
  __args__.shift(); // obj
  return function (){
    // concat args
    var args = Array.prototype.slice.call(arguments);
    return __method__.apply(obj, __args__.concat(args));
  };
};

var AbstractBattleStrategy = function (character){
  this.character = character;
};
AbstractBattleStrategy.prototype.next = function (){};
AbstractBattleStrategy.prototype.attack = function(){};
AbstractBattleStrategy.prototype.guard = function (){};

var SimpleBattleStrategy = function (){
  AbstractBattleStrategy.apply(this, arguments);
};
SimpleBattleStrategy.prototype = AbstractBattleStrategy.prototype;
SimpleBattleStrategy.prototype.attack = function(){
  return this.character.offence;
};
SimpleBattleStrategy.prototype.guard = function (){
  return this.character.defence;
};

var WoundedRecoveryBattleStrategy = function (){
  AbstractBattleStrategy.apply(this, arguments);
  this.recovery = false;
};
WoundedRecoveryBattleStrategy.prototype = AbstractBattleStrategy.prototype;
WoundedRecoveryBattleStrategy.prototype.attack = function (){
  if(this.recovery){
    this.character.hp += 30;
    return 0;
  }
  return this.character.offence;
};
WoundedRecoveryBattleStrategy.prototype.guard = function (){
  if(this.recovery){
    return 0;
  }
  return this.character.defence;
};
WoundedRecoveryBattleStrategy.prototype.next = function (){
  this.recovery = false;

  if(this.character.hp < 10){
    var rand = Math.floor(Math.random() * 3);
    if(1 == rand){
      this.recovery = true;
    }
  }
};

var AbstractLevelStrategy = function (character, proto){
  this.character = character;
  this.proto = proto;
};
AbstractLevelStrategy.prototype.execute = function (){};

var SimpleLevelStrategy = function(){
  AbstractLevelStrategy.apply(this, arguments);
};
SimpleLevelStrategy.prototype.execute = function (){
  var level = this.character.level;
  var proto = this.proto;

  this.character.strength = level * proto.strength;
  this.character.agility = level * proto.agility;
  this.character.vitality = level * proto.vitality;
  this.character.intelligence = level * proto.intelligence;
  this.character.dexterity = level * proto.dexterity;
  this.character.lucky = level * proto.lucky;

  this.character.offence = this.character.strength + this.character.agility;
  this.character.defence = this.character.vitality + this.character.dexterity;
  this.character.hp = level * proto.hp;
};

var AbstractCharacter = function (){};
AbstractCharacter.prototype.strength = 0;
AbstractCharacter.prototype.agility = 0;
AbstractCharacter.prototype.vitality = 0;
AbstractCharacter.prototype.intelligence = 0;
AbstractCharacter.prototype.dexterity = 0;
AbstractCharacter.prototype.lucky = 0;
AbstractCharacter.prototype.name = '';
AbstractCharacter.prototype.hp = 0;
AbstractCharacter.prototype.level = 1;
AbstractCharacter.prototype.experience = 0;
AbstractCharacter.prototype.offence = 0;
AbstractCharacter.prototype.defence = 0;
AbstractCharacter.prototype.battleStrategy = null;
AbstractCharacter.prototype.levelStrategy = null;
AbstractCharacter.prototype.battle = function (){
  return new Battle(this, this.battleStrategy);
};
AbstractCharacter.prototype.setLevel = function(level){
  this.level = level;
  this.levelStrategy.execute();
};

var Slime = function() {
  this.battleStrategy = new SimpleBattleStrategy(this);
  this.levelStrategy = new SimpleLevelStrategy(this, Slime.prototype);
};
Slime.prototype = new AbstractCharacter();
Slime.prototype.name = 'スライム';
Slime.prototype.hp = 20;
Slime.prototype.experience = 10;
Slime.prototype.strength = 3;
Slime.prototype.agility = 3;
Slime.prototype.vitality = 3;
Slime.prototype.intelligence = 0;
Slime.prototype.dexterity = 3;
Slime.prototype.lucky = 0;

var HoimiSlime = function() {
  this.battleStrategy = new WoundedRecoveryBattleStrategy(this);
  this.levelStrategy = new SimpleLevelStrategy(this, HoimiSlime.prototype);
};
HoimiSlime.prototype = new AbstractCharacter();
HoimiSlime.prototype.name = 'ホイミスライム';
HoimiSlime.prototype.hp = 25;
HoimiSlime.prototype.experience = 50;
HoimiSlime.prototype.strength = 5;
HoimiSlime.prototype.agility = 5;
HoimiSlime.prototype.vitality = 2;
HoimiSlime.prototype.intelligence = 0;
HoimiSlime.prototype.dexterity = 2;
HoimiSlime.prototype.lucky = 0;

var KingSlime = function() {
  this.battleStrategy = new SimpleBattleStrategy(this);
  this.levelStrategy = new SimpleLevelStrategy(this, KingSlime.prototype);
};
KingSlime.prototype = new AbstractCharacter();
KingSlime.prototype.name = 'キングスライム';
KingSlime.prototype.hp = 50;
KingSlime.prototype.experience = 80;
KingSlime.prototype.strength = 8;
KingSlime.prototype.agility = 12;
KingSlime.prototype.vitality = 8;
KingSlime.prototype.intelligence = 0;
KingSlime.prototype.dexterity = 8;
KingSlime.prototype.lucky = 1;

var Hero = function (){
  this.battleStrategy = new SimpleBattleStrategy(this);
  this.levelStrategy = new SimpleLevelStrategy(this, Hero.prototype);
};
Hero.prototype = new AbstractCharacter();
Hero.prototype.name = '勇者';
Hero.prototype.hp = 100;
Hero.prototype.experience = 50;
Hero.prototype.strength = 10;
Hero.prototype.agility = 10;
Hero.prototype.vitality = 10;
Hero.prototype.intelligence = 10;
Hero.prototype.dexterity = 10;
Hero.prototype.lucky = 10;

var Battle = function(character, strategy){
  this.character = character;
  this.strategy = strategy;
};
Battle.prototype.logger = null;
Battle.prototype.start = function (){
  this.logger.log(this.getName() + 'の戦闘開始: Lv.' + this.getLevel() + ' HP: ' + this.getHp());
};
Battle.prototype.attack = function (){
  return this.strategy.attack();
};
Battle.prototype.guard = function (){
  return this.strategy.guard();
};
Battle.prototype.next = function (){
  return this.strategy.next();
};
Battle.prototype.end = function (){
  this.logger.log(this.getName() + 'は戦闘を離脱した');
};
Battle.prototype.isDie = function (){
  return this.character.hp < 1;
};
Battle.prototype.isAgilityQuick = function(target){
  return this.character.agility < target.character.agility;
};
Battle.prototype.addDamage = function (damage){
  this.character.hp -= damage;
};
Battle.prototype.addExperience = function(experience){
  this.character.experience += experience;
};
Battle.prototype.getExperience = function (){
  return this.character.experience;
};
Battle.prototype.getLevel = function (){
  return this.character.level;
};
Battle.prototype.getHp = function (){
  return this.character.hp;
};
Battle.prototype.getName = function (){
  return this.character.name;
};
Battle.prototype.getLucky = function (){
  return this.character.lucky;
};
Battle.prototype.offence = function(defencer){
  var attackValue = this.attack();
  var guardValue = defencer.guard();

  if(0 < this.getLucky()){
    var rand = Math.floor(Math.random() * 100);
    if(this.getLucky() == rand){
      this.logger.log(this.getName() + 'のクリティカル!');
      attackValue = attackValue * 2;
    }
  }

  var damage = 0;
  if(guardValue < attackValue){
    damage = attackValue - guardValue;
  }
  defencer.addDamage(damage);

  this.logger.log(this.getName() + 'の攻撃: ' + defencer.getName() + 'に' + damage + 'のダメージ');

  if(defencer.isDie()){
    this.addExperience(defencer.getExperience());
    return this.logger.log(defencer.getName() + 'は倒れた');
  }
};

var TurnBattle = function(hero, monsterList){
  this.hero = hero;
  this.monsterList = monsterList;
};
TurnBattle.prototype.logger = null;
TurnBattle.prototype.execute = function(){
  var heroBattle = this.hero.battle();
  try {
    heroBattle.start();

    var monsterBattles = this.monsterList.map(function(monster){
      var battle = monster.battle();
      battle.start();
      return battle;
    });

    var turn = 1;
    while(true){
      this.logger.log('ターン: ' + turn);

      // 攻撃対象をランダムで設定
      var rand = Math.floor(Math.random() * monsterBattles.length);
      var monsterBattle = monsterBattles[rand];

      this.battle(heroBattle, monsterBattle);

      monsterBattles = monsterBattles.filter((function(monsterBattle){
        if(monsterBattle.isDie()){
          monsterBattle.end();
          return false;
        }
        return true;
      }).bind(this));

      if(monsterBattles.length < 1){
        return this.logger.log(heroBattle.getName() + 'は勝利した');
      }
      if(heroBattle.isDie()){
        return this.logger.log(heroBattle.getName() + 'は死んでしまった');
      }

      turn = turn + 1;
    }
  } finally {
    heroBattle.end();
  }
}
TurnBattle.prototype.battle = function (hero, monster){
  try {
    if(hero.isAgilityQuick(monster)){
      monster.offence(hero);
      hero.offence(monster);
    } else {
      hero.offence(monster);
      monster.offence(hero);
    }
  } finally {
    hero.next();
    monster.next();
  }
};

var AbstractQuestState = function(){};
AbstractQuestState.prototype.next = true;
AbstractQuestState.prototype.execute = function (){};

var QuestStartState = function (){};
QuestStartState.prototype = new AbstractQuestState();
QuestStartState.prototype.execute = function (quest){
  quest.logger.log('クエスト開始');
  quest.count = 0;
  quest.hero.setLevel(1);
  quest.state = new NextQuestState();
};

var QuestEndState = function (){};
QuestEndState.prototype = new AbstractQuestState();
QuestEndState.prototype.execute = function(quest){
  quest.logger.log(quest.count + '回のクエストを終えた');
  quest.logger.log('クエスト終了');
  quest.state.next = false;
};

var NextQuestState = function (){};
NextQuestState.prototype = new AbstractQuestState();
NextQuestState.prototype.execute = function(quest){
  quest.logger.log('移動中...');

  quest.state = this.createState();
  quest.count = quest.count + 1;
};
NextQuestState.prototype.createState = function (){
  if(10 < quest.count){
    return new QuestEndState();
  }
  if(quest.hero.hp < 1){
    return new QuestEndState();
  }

  var rand = Math.floor(Math.random() * 10);
  if(rand < 2){
    return new NextQuestState();
  }
  if(rand < 9){
    return new BattleState();
  }
  return new RecoveryLakeState();
};

var BattleState = function (){};
BattleState.prototype = new AbstractQuestState();
BattleState.prototype.execute = function(quest){
  quest.logger.log('モンスターに遭遇した');

  var monsters = [];
  var monsterCount = Math.floor(Math.random() * 10) + 1;
  for(var i = 0; i < monsterCount; ++i){
    monsters.push(this.createMonster(quest.hero.level));
  }
  monsters.forEach(function (monster){
    var monsterLevel = Math.floor(Math.random() * quest.hero.level) + 1;
    monster.setLevel(monsterLevel);
  });

  var battle = new TurnBattle(quest.hero, monsters);
  battle.execute();

  if(quest.hero.hp < 1){
    quest.state = new QuestEndState();
  } else {
    quest.state = new CheckExperienceState();
  }
};
BattleState.prototype.createMonster = function(heroLevel){
  var battleDifficulty = Math.floor(Math.random() * heroLevel);
  if(battleDifficulty < 3){
    return new Slime();
  }
  if(battleDifficulty < 6){
    return new HoimiSlime();
  }
  return new KingSlime();
};
var CheckExperienceState = function (){};
CheckExperienceState.prototype = new AbstractQuestState();
CheckExperienceState.prototype.execute = function(quest){
  var levelUpExperience = Hero.prototype.experience * quest.hero.level;
  if(levelUpExperience <= quest.hero.experience){
    var nextLevel = quest.hero.level + 1;
    quest.hero.setLevel(nextLevel);
    quest.logger.log(quest.hero.name + 'はレベルが上がった: Lv.' + nextLevel);
  }

  quest.state = new NextQuestState();
};
var RecoveryLakeState = function (){};
RecoveryLakeState.prototype = new AbstractQuestState();
RecoveryLakeState.prototype.execute = function(quest){
  quest.logger.log('回復の泉を発見');

  var value = Math.floor(Math.random() * 49) + 1;
  quest.hero.hp += value;

  quest.logger.log(value + '回復した');
  
  quest.state = new NextQuestState();
};

var Quest = function(hero){
  this.hero = hero;
  this.count = 0;
  this.state = new QuestStartState();
};
Quest.prototype.logger = new function (){
  if(typeof console != 'undefined'){
    this.log = function(message){
      console.log(message);
    };
  } else {
    this.log = function(message){
      print(message);
    };
  }
};
Quest.prototype.start = function (){
  Battle.prototype.logger = this.logger;
  TurnBattle.prototype.logger = this.logger;

  while(this.state.next){
    this.state.execute(this);
  }
};

var hero = new Hero();
var quest = new Quest(hero);
quest.start();

レベルアップは簡単にこんな感じでいいのだろうか

var AbstractLevelStrategy = function (character, proto){
  this.character = character;
  this.proto = proto;
};
AbstractLevelStrategy.prototype.execute = function (){};

var SimpleLevelStrategy = function(){
  AbstractLevelStrategy.apply(this, arguments);
};
SimpleLevelStrategy.prototype.execute = function (){
  var level = this.character.level;
  var proto = this.proto;

  this.character.strength = level * proto.strength;
  this.character.agility = level * proto.agility;
  this.character.vitality = level * proto.vitality;
  this.character.intelligence = level * proto.intelligence;
  this.character.dexterity = level * proto.dexterity;
  this.character.lucky = level * proto.lucky;

  this.character.offence = this.character.strength + this.character.agility;
  this.character.defence = this.character.vitality + this.character.dexterity;
  this.character.hp = level * proto.hp;
};

Character毎のprototype(根元の設定値)の参照とかコンストラクタでやってしまう

var Slime = function() {
  this.battleStrategy = new SimpleBattleStrategy(this);
  this.levelStrategy = new SimpleLevelStrategy(this, Slime.prototype);
};
Slime.prototype = new AbstractCharacter();
Slime.prototype.name = 'スライム';
Slime.prototype.hp = 20;
Slime.prototype.experience = 10;
Slime.prototype.strength = 3;
Slime.prototype.agility = 3;
Slime.prototype.vitality = 3;
Slime.prototype.intelligence = 0;
Slime.prototype.dexterity = 3;
Slime.prototype.lucky = 0;

んで、こんな感じに。

クエスト開始
移動中...
モンスターに遭遇した
勇者の戦闘開始: Lv.1 HP: 100
スライムの戦闘開始: Lv.1 HP: 20
スライムの戦闘開始: Lv.1 HP: 20
スライムの戦闘開始: Lv.1 HP: 20
スライムの戦闘開始: Lv.1 HP: 20
スライムの戦闘開始: Lv.1 HP: 20
スライムの戦闘開始: Lv.1 HP: 20
ターン: 1
勇者の攻撃: スライムに14のダメージ
スライムの攻撃: 勇者に0のダメージ
ターン: 2
勇者の攻撃: スライムに14のダメージ
スライムの攻撃: 勇者に0のダメージ
ターン: 3
勇者の攻撃: スライムに14のダメージ
スライムの攻撃: 勇者に0のダメージ
...
勇者はレベルが上がった: Lv.3
移動中...
モンスターに遭遇した
勇者の戦闘開始: Lv.3 HP: 300
スライムの戦闘開始: Lv.2 HP: 40
スライムの戦闘開始: Lv.1 HP: 20
スライムの戦闘開始: Lv.3 HP: 60
スライムの戦闘開始: Lv.1 HP: 20
ターン: 1
勇者の攻撃: スライムに42のダメージ
スライムの攻撃: 勇者に0のダメージ
ターン: 2
勇者の攻撃: スライムに54のダメージ
スライムは倒れた
スライムの攻撃: 勇者に0のダメージ
スライムは戦闘を離脱した
ターン: 3
勇者の攻撃: スライムに54のダメージ
スライムは倒れた
スライムの攻撃: 勇者に0のダメージ
スライムは戦闘を離脱した
...
勇者はレベルが上がった: Lv.7
移動中...
回復の泉を発見
1回復した
移動中...
モンスターに遭遇した
勇者の戦闘開始: Lv.7 HP: 701
スライムの戦闘開始: Lv.2 HP: 40
スライムの戦闘開始: Lv.4 HP: 80
ホイミスライムの戦闘開始: Lv.1 HP: 25
ホイミスライムの戦闘開始: Lv.6 HP: 150
スライムの戦闘開始: Lv.5 HP: 100
スライムの戦闘開始: Lv.1 HP: 20
キングスライムの戦闘開始: Lv.7 HP: 350
ターン: 1
勇者の攻撃: スライムに134のダメージ
スライムは倒れた
スライムの攻撃: 勇者に0のダメージ
スライムは戦闘を離脱した
ターン: 2
キングスライムの攻撃: 勇者に0のダメージ
勇者の攻撃: キングスライムに28のダメージ
ターン: 3
キングスライムの攻撃: 勇者に0のダメージ
勇者の攻撃: キングスライムに28のダメージ
ターン: 4
勇者の攻撃: スライムに128のダメージ
スライムは倒れた
スライムの攻撃: 勇者に0のダメージ
スライムは戦闘を離脱した
ターン: 5
勇者の攻撃: ホイミスライムに136のダメージ
ホイミスライムは倒れた
ホイミスライムの攻撃: 勇者に0のダメージ
ホイミスライムは戦闘を離脱した
...
キングスライムは倒れた
キングスライムは戦闘を離脱した
勇者は勝利した
勇者は戦闘を離脱した
勇者はレベルが上がった: Lv.8
移動中...
回復の泉を発見
20回復した
移動中...
12回のクエストを終えた
クエスト終了

大きなロジックは終わったかな
フロア移動とかボスとか装備品とかそんなのがあるとよさそうかも

2011/05/05

JavaScriptでRPCゲーム的な。その2

ポスト @ 21:28:58 | ,     

前回のあれだと、戦闘しかなかったので、もう少し色々歩きまわるようにしてみた

Function.prototype.bind = function(obj){
  var __method__ = this;
  var __args__ = Array.prototype.slice.call(arguments);
  __args__.shift(); // obj
  return function (){
    // concat args
    var args = Array.prototype.slice.call(arguments);
    return __method__.apply(obj, __args__.concat(args));
  };
};

var AbstractBattleStrategy = function (character){
  this.character = character;
};
AbstractBattleStrategy.prototype.next = function (){};
AbstractBattleStrategy.prototype.attack = function(){};
AbstractBattleStrategy.prototype.guard = function (){};

var SimpleBattleStrategy = function (){
  AbstractBattleStrategy.apply(this, arguments);
};
SimpleBattleStrategy.prototype = AbstractBattleStrategy.prototype;
SimpleBattleStrategy.prototype.attack = function(){
  return this.character.offence;
};
SimpleBattleStrategy.prototype.guard = function (){
  return this.character.defence;
};

var WoundedRecoveryBattleStrategy = function (){
  AbstractBattleStrategy.apply(this, arguments);
  this.recovery = false;
};
WoundedRecoveryBattleStrategy.prototype = AbstractBattleStrategy.prototype;
WoundedRecoveryBattleStrategy.prototype.attack = function (){
  if(this.recovery){
    this.character.hp += 30;
    return 0;
  }
  return this.character.offence;
};
WoundedRecoveryBattleStrategy.prototype.guard = function (){
  if(this.recovery){
    return 0;
  }
  return this.character.defence;
};
WoundedRecoveryBattleStrategy.prototype.next = function (){
  this.recovery = false;

  if(this.character.hp < 10){
    var rand = Math.floor(Math.random() * 3);
    if(1 == rand){
      this.recovery = true;
    }
  }
};

var AbstractCharacter = function (){};
AbstractCharacter.prototype.strength = 0;
AbstractCharacter.prototype.agility = 0;
AbstractCharacter.prototype.vitality = 0;
AbstractCharacter.prototype.intelligence = 0;
AbstractCharacter.prototype.dexterity = 0;
AbstractCharacter.prototype.lucky = 0;
AbstractCharacter.prototype.name = '';
AbstractCharacter.prototype.hp = 0;
AbstractCharacter.prototype.offence = 0;
AbstractCharacter.prototype.defence = 0;
AbstractCharacter.prototype.strategy = null;
AbstractCharacter.prototype.battle = function (){
  return new Battle(this);
};

var Slime = function() {
  this.strategy = new SimpleBattleStrategy(this);
};
Slime.prototype = new AbstractCharacter();
Slime.prototype.name = 'スライム';
Slime.prototype.hp = 20;
Slime.prototype.lucky = 0;
Slime.prototype.offence = 3;
Slime.prototype.defence = 3;

var HoimiSlime = function() {
  this.strategy = new WoundedRecoveryBattleStrategy(this);
};
HoimiSlime.prototype = new AbstractCharacter();
HoimiSlime.prototype.name = 'ホイミスライム';
HoimiSlime.prototype.hp = 25;
HoimiSlime.prototype.offence = 2;
HoimiSlime.prototype.defence = 2;

var KingSlime = function() {
  this.strategy = new SimpleBattleStrategy(this);
};
KingSlime.prototype = new AbstractCharacter();
KingSlime.prototype.name = 'キングスライム';
KingSlime.prototype.hp = 50;
KingSlime.prototype.agility = 12;
KingSlime.prototype.lucky = 1;
KingSlime.prototype.offence = 8;
KingSlime.prototype.defence = 8;

var Hero = function (){
  this.strategy = new SimpleBattleStrategy(this);
};
Hero.prototype = new AbstractCharacter();
Hero.prototype.name = '勇者';
Hero.prototype.hp = 300;
Hero.prototype.agility = 10;
Hero.prototype.lucky = 10;
Hero.prototype.offence = 12;
Hero.prototype.defence = 12;

var Battle = function(character){
  this.character = character;
};
Battle.prototype.logger = null;
Battle.prototype.start = function (){
  this.logger.log(this.getName() + 'の戦闘開始');
};
Battle.prototype.attack = function (){
  return this.character.strategy.attack();
};
Battle.prototype.guard = function (){
  return this.character.strategy.guard();
};
Battle.prototype.next = function (){
  return this.character.strategy.next();
};
Battle.prototype.end = function (){
  this.logger.log(this.getName() + 'は戦闘を離脱した');
};
Battle.prototype.isDie = function (){
  return this.character.hp < 1;
};
Battle.prototype.isAgilityQuick = function(target){
  return this.character.agility < target.character.agility;
};
Battle.prototype.setDamage = function (damage){
  this.character.hp -= damage;
};
Battle.prototype.getHp = function (){
  return this.character.hp;
};
Battle.prototype.getName = function (){
  return this.character.name;
};
Battle.prototype.getLucky = function (){
  return this.character.lucky;
};
Battle.prototype.offence = function(defencer){
  var attackValue = this.attack();
  var guardValue = defencer.guard();

  if(0 < this.getLucky()){
    var rand = Math.floor(Math.random() * 100);
    if(this.getLucky() == rand){
      this.logger.log(this.getName() + 'のクリティカル!');
      attackValue = attackValue * 2;
    }
  }

  var damage = 0;
  if(guardValue < attackValue){
    damage = attackValue - guardValue;
  }
  defencer.setDamage(damage);

  this.logger.log(this.getName() + 'の攻撃: ' + defencer.getName() + 'に' + damage + 'のダメージ');

  if(defencer.isDie()){
    return this.logger.log(defencer.getName() + 'は倒れた');
  }
};

var TurnBattle = function(hero, monsterList){
  this.hero = hero;
  this.monsterList = monsterList;
};
TurnBattle.prototype.logger = null;
TurnBattle.prototype.execute = function(){
  var heroBattle = this.hero.battle();
  try {
    heroBattle.start();

    var monsterBattles = this.monsterList.map(function(monster){
      var battle = monster.battle();
      battle.start();
      return battle;
    });

    var turn = 1;
    while(true){
      this.logger.log('ターン: ' + turn);

      // 攻撃対象をランダムで設定
      var rand = Math.floor(Math.random() * monsterBattles.length);
      var monsterBattle = monsterBattles[rand];

      this.battle(heroBattle, monsterBattle);

      monsterBattles = monsterBattles.filter((function(monsterBattle){
        if(monsterBattle.isDie()){
          monsterBattle.end();
          return false;
        }
        return true;
      }).bind(this));

      if(monsterBattles.length < 1){
        return this.logger.log(heroBattle.getName() + 'は勝利した');
      }
      if(heroBattle.isDie()){
        return this.logger.log(heroBattle.getName() + 'は死んでしまった');
      }

      turn = turn + 1;
    }
  } finally {
    heroBattle.end();
  }
}
TurnBattle.prototype.battle = function (hero, monster){
  try {
    if(hero.isAgilityQuick(monster)){
      monster.offence(hero);
      hero.offence(monster);
    } else {
      hero.offence(monster);
      monster.offence(hero);
    }
  } finally {
    hero.next();
    monster.next();
  }

  this.logger.log('-------------------');
  this.logger.log(hero.getName() + 'のHP: ' + hero.getHp());
  this.logger.log(monster.getName() + 'のHP: ' + monster.getHp());
  this.logger.log('-------------------');
};

var AbstractQuestState = function(){};
AbstractQuestState.prototype.next = true;
AbstractQuestState.prototype.execute = function (){};

var QuestStartState = function (){};
QuestStartState.prototype = new AbstractQuestState();
QuestStartState.prototype.execute = function (quest){
  quest.logger.log('クエスト開始');
  quest.count = 0;
  quest.state = new NextQuestState();
};

var QuestEndState = function (){};
QuestEndState.prototype = new AbstractQuestState();
QuestEndState.prototype.execute = function(quest){
  quest.logger.log(quest.count + '回のクエストを終えた');
  quest.logger.log('クエスト終了');
  quest.state.next = false;
};

var NextQuestState = function (){};
NextQuestState.prototype = new AbstractQuestState();
NextQuestState.prototype.execute = function(quest){
  if(10 < quest.count){
    quest.state = new QuestEndState();
  } else {
    if(quest.hero.hp < 1){
      quest.state = new QuestEndState();
    } else {
      quest.state = new BattleState();
    }
  }
  quest.count = quest.count + 1;
};

var BattleState = function (){};
BattleState.prototype = new AbstractQuestState();
BattleState.prototype.execute = function(quest){
  var monsters = [new Slime(), new HoimiSlime(), new KingSlime()];
  var battle = new TurnBattle(quest.hero, monsters);
  battle.execute();

  if(quest.hero.hp < 1){
    quest.state = new QuestEndState();
  } else {
    var rand = Math.floor(Math.random() * 3);
    switch(rand){
    case 0:
      quest.state = new NextQuestState();
      break;
    case 1:
      quest.state = new BattleState();
      break;
    case 2:
      quest.state = new RecoveryLakeState();
      break;
    }
  }
};
var RecoveryLakeState = function (){};
RecoveryLakeState.prototype = new AbstractQuestState();
RecoveryLakeState.prototype.execute = function(quest){
  quest.logger.log('回復の泉を発見');

  var value = Math.floor(Math.random() * 49) + 1;
  quest.hero.hp += value;

  quest.logger.log(value + '回復した');
  
  quest.state = new NextQuestState();
};

var Quest = function(hero){
  this.hero = hero;
  this.count = 0;
  this.state = new QuestStartState();
};
Quest.prototype.logger = new function (){
  if(typeof console != 'undefined'){
    this.log = function(message){
      console.log(message);
    };
  } else {
    this.log = function(message){
      print(message);
    };
  }
};
Quest.prototype.start = function (){
  Battle.prototype.logger = this.logger;
  TurnBattle.prototype.logger = this.logger;

  while(this.state.next){
    this.state.execute(this);
  }
};

var hero = new Hero();
var quest = new Quest(hero);
quest.start();

Stateっぽく、してQuestをこなすようにしてみた

var AbstractQuestState = function(){};
AbstractQuestState.prototype.next = true;
AbstractQuestState.prototype.execute = function (){};

var QuestStartState = function (){};
QuestStartState.prototype = new AbstractQuestState();
QuestStartState.prototype.execute = function (quest){
  quest.logger.log('クエスト開始');
  quest.count = 0;
  quest.state = new NextQuestState();
};

var QuestEndState = function (){};
QuestEndState.prototype = new AbstractQuestState();
QuestEndState.prototype.execute = function(quest){
  quest.logger.log(quest.count + '回のクエストを終えた');
  quest.logger.log('クエスト終了');
  quest.state.next = false;
};

var NextQuestState = function (){};
NextQuestState.prototype = new AbstractQuestState();
NextQuestState.prototype.execute = function(quest){
  if(10 < quest.count){
    quest.state = new QuestEndState();
  } else {
    if(quest.hero.hp < 1){
      quest.state = new QuestEndState();
    } else {
      quest.state = new BattleState();
    }
  }
  quest.count = quest.count + 1;
};

var BattleState = function (){};
BattleState.prototype = new AbstractQuestState();
BattleState.prototype.execute = function(quest){
  var monsters = [new Slime(), new HoimiSlime(), new KingSlime()];
  var battle = new TurnBattle(quest.hero, monsters);
  battle.execute();

  if(quest.hero.hp < 1){
    quest.state = new QuestEndState();
  } else {
    var rand = Math.floor(Math.random() * 3);
    switch(rand){
    case 0:
      quest.state = new NextQuestState();
      break;
    case 1:
      quest.state = new BattleState();
      break;
    case 2:
      quest.state = new RecoveryLakeState();
      break;
    }
  }
};
var RecoveryLakeState = function (){};
RecoveryLakeState.prototype = new AbstractQuestState();
RecoveryLakeState.prototype.execute = function(quest){
  quest.logger.log('回復の泉を発見');

  var value = Math.floor(Math.random() * 49) + 1;
  quest.hero.hp += value;

  quest.logger.log(value + '回復した');
  
  quest.state = new NextQuestState();
};

var Quest = function(hero){
  this.hero = hero;
  this.count = 0;
  this.state = new QuestStartState();
};
Quest.prototype.logger = new function (){
  if(typeof console != 'undefined'){
    this.log = function(message){
      console.log(message);
    };
  } else {
    this.log = function(message){
      print(message);
    };
  }
};
Quest.prototype.start = function (){
  Battle.prototype.logger = this.logger;
  TurnBattle.prototype.logger = this.logger;

  while(this.state.next){
    this.state.execute(this);
  }
};

これで、動かす

クエスト開始
勇者の戦闘開始
スライムの戦闘開始
ホイミスライムの戦闘開始
キングスライムの戦闘開始
ターン: 1
キングスライムの攻撃: 勇者に0のダメージ
勇者の攻撃: キングスライムに4のダメージ
-------------------
勇者のHP: 300
キングスライムのHP: 46
-------------------
ターン: 2
勇者の攻撃: ホイミスライムに10のダメージ
ホイミスライムの攻撃: 勇者に0のダメージ
-------------------
勇者のHP: 300
ホイミスライムのHP: 15
-------------------
ターン: 3

...

キングスライムの攻撃: 勇者に0のダメージ
勇者の攻撃: キングスライムに4のダメージ
-------------------
勇者のHP: 388
キングスライムのHP: 10
-------------------
ターン: 17
キングスライムの攻撃: 勇者に0のダメージ
勇者のクリティカル!
勇者の攻撃: キングスライムに16のダメージ
キングスライムは倒れた
-------------------
勇者のHP: 388
キングスライムのHP: -6
-------------------
キングスライムは戦闘を離脱した
勇者は勝利した
勇者は戦闘を離脱した
12回のクエストを終えた
クエスト終了

レベルアップとか欲しくなるね

JavaScriptでStrategyパターン。RPG風

ポスト @ 4:42:07 | , ,     

結構前から遊んでたんだけど、ゆけ勇者が面白いなぁ。と思って、こんな感じなのを作るとするとどうなるんだろうかと思って、とりあえずStrategyパターンを使いつつ

(中略)

今回の元ネタは
ref- 指向性メモ::2005-02-24::JavaScriptでデザインパターンその3
になります。かなりパクった感じです。
# もう、6年も前のネタだったのか。ずっとブックマークにあった。重宝してます。

さっそく、実装コードはこんな感じ

Function.prototype.bind = function(obj){
  var __method__ = this;
  var __args__ = Array.prototype.slice.call(arguments);
  __args__.shift(); // obj
  return function (){
    // concat args
    var args = Array.prototype.slice.call(arguments);
    return __method__.apply(obj, __args__.concat(args));
  };
};

var AbstractStrategy = function (){};
AbstractStrategy.prototype.execute = function(character){};

var StrategyEveryAttack = function (){};
StrategyEveryAttack.prototype.execute = function(character){
  if(0 < character.lucky){
    var rand = Math.floor(Math.random() * 100);
    if(character.lucky == rand){
      return character.offence * 2;
    }
  }
  return character.offence;
};

var StrategyWoundedRecoverHp = function (){};
StrategyWoundedRecoverHp.prototype.execute = function(character){
  var rand = Math.floor(Math.random() * 3);
  if(character.hp < 10 && rand == 1){
    character.hp += 30;
    return 0;
  }
  return character.offence;
};

var AbstractCharacter = function (){};
AbstractCharacter.prototype.strength = 0;
AbstractCharacter.prototype.agility = 0;
AbstractCharacter.prototype.vitality = 0;
AbstractCharacter.prototype.intelligence = 0;
AbstractCharacter.prototype.dexterity = 0;
AbstractCharacter.prototype.lucky = 0;
AbstractCharacter.prototype.name = '';
AbstractCharacter.prototype.hp = 0;
AbstractCharacter.prototype.offence = 0;
AbstractCharacter.prototype.defence = 0;
AbstractCharacter.prototype.action = function (){};

var AbstractMonster = function() {};
AbstractMonster.prototype = new AbstractCharacter();
AbstractMonster.prototype.action = function (){
  return this.attack();
};
AbstractMonster.prototype.attack = function (){
  return this.strategy.execute(this);
};

var Slime = function() {};
Slime.prototype = new AbstractMonster();
Slime.prototype.name = 'スライム';
Slime.prototype.hp = 20;
Slime.prototype.lucky = 0;
Slime.prototype.offence = 3;
Slime.prototype.defence = 3;
Slime.prototype.strategy = new StrategyEveryAttack();

var HoimiSlime = function() {};
HoimiSlime.prototype = new AbstractMonster();
HoimiSlime.prototype.name = 'ホイミスライム';
HoimiSlime.prototype.hp = 25;
HoimiSlime.prototype.offence = 2;
HoimiSlime.prototype.defence = 2;
HoimiSlime.prototype.strategy = new StrategyWoundedRecoverHp();

var KingSlime = function() {};
KingSlime.prototype = new AbstractMonster();
KingSlime.prototype.name = 'キングスライム';
KingSlime.prototype.hp = 50;
KingSlime.prototype.agility = 12;
KingSlime.prototype.lucky = 1;
KingSlime.prototype.offence = 4;
KingSlime.prototype.defence = 5;
KingSlime.prototype.strategy = new StrategyEveryAttack();

var Hero = function (){};
Hero.prototype = new AbstractCharacter();
Hero.prototype.name = '勇者';
Hero.prototype.hp = 500;
Hero.prototype.agility = 10;
Hero.prototype.lucky = 10;
Hero.prototype.offence = 12;
Hero.prototype.defence = 10;
Hero.prototype.action = function (){
  return this.offence;
};

var Battle = function(hero, monsterList){
  this.turn = 1;
  this.hero = hero;
  this.monsterList = monsterList;
};
Battle.prototype.logger = new function (){
  if(typeof console != 'undefined'){
    this.log = function(message){
      console.log(message);
    };
  } else {
    this.log = function(message){
      print(message);
    };
  }
};
Battle.prototype.start = function (){
  while(true){
    if(this.monsterList.length < 1){
      return this.logger.log(this.hero.name + ' は勝利した');
    }
    if(this.hero.hp < 1){
      return this.logger.log(this.hero.name + ' は死んでしまった');
    }
    this.next();
  }
};
Battle.prototype.next = function (){
  try {
    this.logger.log('ターン: ' + this.turn);

    // 攻撃対象をランダムで設定
    var targetIndex = Math.floor(Math.random() * this.monsterList.length);
    var monster = this.monsterList[targetIndex];

    // 素早さの早い方からattack
    if(this.hero.agility < monster.agility){
      this.attack(monster, this.hero);
    } else {
      this.attack(this.hero, monster);
    }

    // モンスターリストの再構築: hp が 0 以下のモノは取り除く
    this.monsterList = this.monsterList.filter((function(monster){
      if(monster.hp <= 0){
        this.logger.log(monster.name + 'は死んだ');
        return false;
      }
      return true;
    }).bind(this));
  } finally {
    this.turn = this.turn + 1;
  }
};
Battle.prototype.attack = function (attacker, defencer, once){
  var attackValue = attacker.action();
  var damage = defencer.defence - attackValue;

  var diff = defencer.defence - attackValue;
  var damage = Math.abs(diff);
  defencer.hp -= damage;

  this.logger.log(attacker.name + 'の攻撃: ' + defencer.name + 'に' + damage + 'のダメージ');

  this.logger.log('-------------------');
  this.logger.log(attacker.name + 'のHP: ' + attacker.hp);
  this.logger.log(defencer.name + 'のHP: ' + defencer.hp);
  this.logger.log('-------------------');

  if(defencer.hp <= 0){
    return this.logger.log(defencer.name + 'は倒れた');
  }

  // 再起しない場合は、抜ける
  if(once){
    return;
  }

  // attackerとdefencerを入れ替えて attack 再開
  return this.attack(defencer, attacker, true);
};

// デモコード
// スライム * 2、ホイミスライム、キングスライムの組み合わせ
var monsters = [new Slime(), new Slime(), new HoimiSlime(), new KingSlime()];
var hero = new Hero();
var btl = new Battle(hero, monsters);
btl.start();

もう、元ネタからは、モンスター名とロジックの一部くらいしか残ってないですが、ほぼ同じようなコードになってます。
strategy pattern っぽく、各モンスターたちは何らかの戦略を持つようにしてます。

var AbstractStrategy = function (){};
AbstractStrategy.prototype.execute = function(character){};

var StrategyEveryAttack = function (){};
StrategyEveryAttack.prototype.execute = function(character){
  if(0 < character.lucky){
    var rand = Math.floor(Math.random() * 100);
    if(character.lucky == rand){
      return character.offence * 2;
    }
  }
  return character.offence;
};

var StrategyWoundedRecoverHp = function (){};
StrategyWoundedRecoverHp.prototype.execute = function(character){
  var rand = Math.floor(Math.random() * 3);
  if(character.hp < 10 && rand == 1){
    character.hp += 30;
    return 0;
  }
  return character.offence;
};

var AbstractCharacter = function (){};
AbstractCharacter.prototype.agility = 0;
AbstractCharacter.prototype.lucky = 0;
AbstractCharacter.prototype.name = '';
AbstractCharacter.prototype.hp = 0;
AbstractCharacter.prototype.offence = 0;
AbstractCharacter.prototype.defence = 0;
AbstractCharacter.prototype.action = function (){};

var AbstractMonster = function() {};
AbstractMonster.prototype = new AbstractCharacter();
AbstractMonster.prototype.action = function (){
  return this.attack();
};
AbstractMonster.prototype.attack = function (){
  return this.strategy.execute(this);
};

var Slime = function() {};
Slime.prototype = new AbstractMonster();
Slime.prototype.name = 'スライム';
Slime.prototype.hp = 20;
Slime.prototype.lucky = 0;
Slime.prototype.offence = 3;
Slime.prototype.defence = 3;
Slime.prototype.strategy = new StrategyEveryAttack();

var HoimiSlime = function() {};
HoimiSlime.prototype = new AbstractMonster();
HoimiSlime.prototype.name = 'ホイミスライム';
HoimiSlime.prototype.hp = 25;
HoimiSlime.prototype.offence = 2;
HoimiSlime.prototype.defence = 2;
HoimiSlime.prototype.strategy = new StrategyWoundedRecoverHp();

var KingSlime = function() {};
KingSlime.prototype = new AbstractMonster();
KingSlime.prototype.name = 'キングスライム';
KingSlime.prototype.hp = 50;
KingSlime.prototype.agility = 12;
KingSlime.prototype.lucky = 1;
KingSlime.prototype.offence = 4;
KingSlime.prototype.defence = 5;
KingSlime.prototype.strategy = new StrategyEveryAttack();

といっても、RPGの基本となる戦略はほとんどがランダムなので、あまり使い物になってないという。。

実行するとこんな感じで動きます(rhinoくらいでしか動かしてない)

ターン: 1
勇者の攻撃: スライムに9のダメージ
-------------------
勇者のHP: 500
スライムのHP: 11
-------------------
スライムの攻撃: 勇者に7のダメージ
-------------------
スライムのHP: 11
勇者のHP: 493
-------------------
ターン: 2
キングスライムの攻撃: 勇者に6のダメージ
-------------------
キングスライムのHP: 50
勇者のHP: 487
-------------------
勇者の攻撃: キングスライムに7のダメージ
-------------------
勇者のHP: 487
キングスライムのHP: 43
-------------------
ターン: 3
勇者の攻撃: スライムに9のダメージ
-------------------
勇者のHP: 487
スライムのHP: 2
-------------------
スライムの攻撃: 勇者に7のダメージ
-------------------
スライムのHP: 2
勇者のHP: 480
-------------------
ターン: 4
勇者の攻撃: スライムに9のダメージ
-------------------
勇者のHP: 480
スライムのHP: -7
-------------------
スライムは倒れた
スライムは死んだ
(snip)
ターン: 18
キングスライムの攻撃: 勇者に6のダメージ
-------------------
キングスライムのHP: 1
勇者のHP: 394
-------------------
勇者の攻撃: キングスライムに7のダメージ
-------------------
勇者のHP: 394
キングスライムのHP: -6
-------------------
キングスライムは倒れた
キングスライムは死んだ
ターン: 19
勇者の攻撃: ホイミスライムに10のダメージ
-------------------
勇者のHP: 394
ホイミスライムのHP: 5
-------------------
ホイミスライムの攻撃: 勇者に8のダメージ
-------------------
ホイミスライムのHP: 5
勇者のHP: 386
-------------------
ターン: 20
勇者の攻撃: ホイミスライムに10のダメージ
-------------------
勇者のHP: 386
ホイミスライムのHP: -5
----------------
ホイミスライムは倒れた
ホイミスライムは死んだ
勇者は勝利した

色々なstateとかキャラクターのstatusとか永続化できれば、ネットワーク越しでも遊べるようになりそう。
あと、ステータス(agilityとかdexterityとか)の値をレベルアップできるようになったり、装備品とかでoffence/diffence補正ができれば、RPGっぽくなる。かも
うむむ。もう少し

2011/04/04

やったーjavascriptでScala風のActorできたよー\(^o^)/

ポスト @ 0:28:36 | , ,     

まだ、loopとかreactとか、その辺りが動くかなーって感じで中途ハンパだけど。

元scalaのコード(http://www.scala-lang.org/node/54から転載)

abstract class PingMessage
case object Start extends PingMessage
case object SendPing extends PingMessage
case object Pong extends PingMessage

abstract class PongMessage
case object Ping extends PongMessage
case object Stop extends PongMessage

object pingpong extends Application {
  val pong = new Pong
  val ping = new Ping(100000, pong)
  ping.start
  pong.start
  ping ! Start
}

class Ping(count: Int, pong: Actor) extends Actor {
  def act() {
    println("Ping: Initializing with count "+count+": "+pong)
    var pingsLeft = count
    loop {
      react {
        case Start =>
          println("Ping: starting.")
          pong ! Ping
          pingsLeft = pingsLeft - 1
        case SendPing =>
          pong ! Ping
          pingsLeft = pingsLeft - 1
        case Pong =>
          if (pingsLeft % 1000 == 0)
            println("Ping: pong from: "+sender)
          if (pingsLeft > 0)
            self ! SendPing
          else {
            println("Ping: Stop.")
            pong ! Stop
            exit('stop)
          }
      }
    }
  }
}

class Pong extends Actor {
  def act() {
    var pongCount = 0
    loop {
      react {
        case Ping =>
          if (pongCount % 1000 == 0)
            println("Pong: ping "+pongCount+" from "+sender)
          sender ! Pong
          pongCount = pongCount + 1
        case Stop =>
          println("Pong: Stop.")
          exit('stop)
      }
    }
  }
}

これが、こんな感じになる

//
// PingPong:
// http://www.scala-lang.org/node/54
//

var MSG_Start = 1;
var MSG_SendPing = 2;
var MSG_Pong = 3;
var MSG_Ping = 4;
var MSG_Stop = 5;

var Ping = function(count, pong) {
  this.count = count;
  this.pong = pong;
};
Ping.prototype = new Actor();
Ping.prototype.toString = function (){
  return 'Ping[count: ' + this.count + ', pong: ' + this.pong + ']';
};
Ping.prototype.act = function(actor){
  console.log('Ping initializing with count:' + this.count + ': ' + this.pong);
  var pingsLeft = this.count;
  actor.loop(actor.react(function(message, sender){
    switch(message){
    case MSG_Start:
      console.log('Ping:start');
      this.pong['!'](MSG_Ping, this);
      pingsLeft = pingsLeft - 1;
      break;
    case MSG_SendPing:
      this.pong['!'](MSG_Ping, this);
      pingsLeft = pingsLeft - 1;
      break;
    case MSG_Pong:
      if(0 === (pingsLeft % 1000)){
        console.log('Ping pong from: ' + sender);
      }
      if(0 < pingsLeft){
        this['!'](MSG_SendPing, this);
      } else {
        console.log('Ping Stop');
        this.pong['!'](MSG_Stop, this);
        this.stop();
      }
      break;
    }
  }));
};
var Pong = function (){};
Pong.prototype = new Actor();
Pong.prototype.toString = function (){
  return '[Pong]';
};
Pong.prototype.act = function(actor){
  var pongCount = 0;
  actor.loop(actor.react(function(message, sender){
    switch(message){
    case MSG_Ping:
      if(0 === (pongCount % 1000)){
        console.log('Pong ping:' + pongCount + ' from ' + sender);
      }
      sender['!'](MSG_Pong, this);
      pongCount = pongCount + 1;
      break;
    case MSG_Stop:
      console.log('Pong Stop');
      this.stop();
      break;
    }
  }));
};

var pong = new Pong();
var ping = new Ping(100000, pong);

ping.start();
pong.start();

ping['!'](MSG_Start);

actor['!']とかsenderが引数で与えられているトコ以外は似てきたかな?

んで、Actorの実装は

Function.prototype.bind = function(obj){
  var __method__ = this;
  var __args__ = Array.prototype.slice.call(arguments);
  __args__.shift(); // obj
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return __method__.apply(obj, __args__.concat(args));
  };
};

Function.prototype.curry = function (){
  var __method__ = this;
  var __args__ = Array.prototype.slice.call(arguments);
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return __method__.apply(this, __args__.concat(args));
  };
};

var GlobalActor = new function (){
  var call = function(callback){
    setTimeout(callback, 0);
  };

  var Queue = function (){};
  Queue.prototype.enqueue = function (callback){
    call(callback);
  };

  var AbstractActor = function(){
    this.queue = new Queue();
  };
  AbstractActor.prototype.act = function (){
    throw new Error('override actor');
  };
  AbstractActor.prototype.start = function (){
    this.queue.enqueue((function (){
      this.act.call(this, this);
    }).bind(this));
  };
  AbstractActor.prototype.stop = function (){
    this.queue.enqueue = function (){
      // nop
    };
  };
  this.AbstractActor = AbstractActor;
};

var Actor = function (){
  this.messages = [];
};
Actor.MESSAGE_BANG = 1;
Actor.MESSAGE_BANGBANG = 2;
Actor.MESSAGE_BANGQMARK = 3;
Actor.prototype = new GlobalActor.AbstractActor();
Actor.prototype.loop = function(callback){
  var self = this;
  var loop = function (){
    try {
      callback.apply(self);
      self.queue.enqueue(loop);
    } catch(e) {
      console.error(e);
    }
  };
  self.queue.enqueue(loop);
};
Actor.prototype.performMessage = function(callback){
  // clear reply
  this.reply = function (){};
  // dequeue message
  var message = this.messages.shift();

  switch(message.type){
  case Actor.MESSAGE_BANG:
    // no reply
    return callback.call(this, message.parameter, message.sender);
  case Actor.MESSAGE_BANGBANG:
    // reply async
    this.reply = function(value){
      var partialResult = message.partialFunction(value);
      var future = message.future;
      this.queue.enqueue(future.callback.curry(partialResult));
    };
    return callback.call(this, message.parameter, message.sender);
  case Actor.MESSAGE_BANGQMARK:
    throw new Error('not yet supported');
  }
};
Actor.prototype.react = function(callback){
  var self = this;
  return function (){
    if(0 < self.messages.length){
      self.performMessage.apply(self, [callback]);
    }
  };
};
Actor.prototype.receive = function(callback){
  throw new Error('not yet supported');
};
Actor.prototype['!'] = function (_parameter, _sender){
  this.messages.push({
    type: Actor.MESSAGE_BANG,
    parameter: _parameter,
    sender: _sender,
    partialFunction: null,
    future: null
  });
};
Actor.prototype['!!'] = function(_parameter, _partialFunction, _sender){
  var __future__ = function(callback){
    arguments.callee.callback = callback;
  };
  __future__.callback = null;

  this.messages.push({
    type: Actor.MESSAGE_BANGBANG,
    parameter: _parameter,
    sender: _sender,
    partialFunction: _partialFunction,
    future: __future__
  });
  return __future__;
};
Actor.prototype['!?'] = function(parameter){
  throw new Error('not yet supported');
};

といった感じ。
本来のScalaのActorならThreadを使ってたりするけど、ここでは setTimeout(callback, 0)になってるんで、少し挙動が違う(どちらかというと、io-languageのActorに似てるかも)
actor.loopからactor.reactもなんか違うかも。。

まだまだ修正中
https://gist.github.com/889276

2011/03/07

Re: Titanium Mobileの暗黒面

ポスト @ 2:06:51 | ,     

面白いものをみてしまったので、僕も少しだけ
ref - Titanium Mobileの暗黒ノウハウを公開します。 - このブログは証明できない。

Object の wrap って出来ないね。ってやつ

Titanium.Database まわりを実装していたときのことなんですが

DB の処理って大半は CRUD しかないから、それらを楽に扱えるように、wrapper を書こうと思って下記のようなコードを用意してみました。

Titanium.UI.setBackgroundColor('#000');

var copyArray = function(obj){
  var result = [];
  for(var i = 0; i < obj.length; ++i){
    result.push(obj[i]);
  }
  return result;
};

var DatabaseWrapper = function(name){
  this.name = name;
  this.db = Titanium.Database.open(name);
};
DatabaseWrapper.prototype.select = function (){
  var args = copyArray(arguments);
  var sql = args.shift();
  var parameters = args;
  var rs = this.db.execute.apply(this.db, args);
  try {
    var returnValues = [];
    while(rs.isValidRow()){
      returnValues.push(rs.fieldByName('hoge'));
    }
    return returnValues;
  } finally {
    rs.close();
  }
};
DatabaseWrapper.prototype.insert = function (){
  //
  // doSomething
  //
};

実装はとても簡単で、 Titanium.Database.execute を直接触るのではなく DatabaseWrapper.select を触るようにしたもの。

実行はこんな感じで行ないました。

//
// hoge.sqlite
// sqlite> .dump
// BEGIN TRANSACTION;
// CREATE TABLE tbl (id int not null, primary key(id));
// INSERT INTO "tbl" VALUES(100);
// INSERT INTO "tbl" VALUES(200);
// INSERT INTO "tbl" VALUES(300);
// COMMIT;
//
Titanium.Database.install('hoge.sqlite', 'hoge');

var db = new DatabaseWrapper('hoge');
Titanium.API.debug(db.select('SELECT id FROM tbl WHERE id > ?', 10)); // ERROR

/*
[ERROR] Script Error = Result of expression 'this.db.execute.apply' [undefined] is not a function. at app.js (line 21).
*/

エラーと言われて動きません。。下記のようなコードに変えることで、動作するので、どうも [object TiDabase] や Kroll Proxy あたりの実装に問題がありそうです。

//var db = new DatabaseWrapper('hoge');
//Titanium.API.debug(db.select('SELECT id FROM tbl WHERE id > ?', 10));
var hoge = new DatabaseWrapper('hoge');
Titanium.API.debug(hoge.db.execute('SELECT id FROM tbl WHERE id > ?', 10)); // [object TiDatabaseResultSet]

Objectのwrapどころか、上書きしても動いてくれないコードになるよね。ってやつ

すでに javascripter としては、上記の問題で愕然としているわけですが、懲りずに(というか上記みたいにエラーがでないので)やっちまいがちなのが、 View まわりです

まず、下記のコードは、真っ黒な画面に 四角い(200x200)の view を 1000ms 後に表示するってコード

Titanium.UI.setBackgroundColor('#000');

var view = Titanium.UI.createView({width: 200, height: 200, backgroundColor: '#fff'});
view.hide();

var win = Titanium.UI.createWindow();
win.add(view);

setTimeout(function (){
  view.show();
}, 1000);

win.open({fullscreen: true});

この 1000ms 後によばれる show に何かしらの処理を埋め込みたくて、下記のように alert を埋め込んでみた

var view = Titanium.UI.createView({width: 200, height: 200, backgroundColor: '#fff'});
view.hide();

var win = Titanium.UI.createWindow();
win.add(view);

setTimeout(function (){
  view.show();
}, 1000);

var originalShowFunc = view.show;
view.show = function (){
  alert('hello world!'); // => 呼ばれない
  return originalShowFunc.apply(view, []);
};

win.open({fullscreen: true});

同じインスタンスのメソッド(func)を上書きしてんだから、ふつーに考えればフツーにうごきそうですが、動きません。

ちなみに、これを上手く使うことで、 Titanium.UI.WebView の repaint メソッドが android で呼びだすとエラーになってしまう(undefinedだったかな)の問題は対処できます。

var utils = {
  createWebView: function(options){
    var webView = Titanium.UI.createWebView(options);
    // android only
    // webView.repaint was undefined: then android webView
    webView.repaint = function (){};
    return webView;
  }
};

ちなみに、ちなみに、object.methodName.apply な呼出は委譲を使うときに使ったりするときに使ったり使わなかったり

var Foo = function (name){
  this.name = name;
};
Foo.prototype.getName = function (){
  return this.name;
};

var foo1 = new Foo('name_1');
var foo2 = new Foo('name_2');

foo1.getName.apply(foo2, []); // => name_2

NavigationGroup がないと、Titleやcontrolがでないのってどうなの

少し設計まわりネタなのですが、Titanium Mobile でプロジェクトを生成するとこんなコードが生成されます。(大幅に変更してるので、「こんな感じ」のコードと思ってください)

Titanium.UI.setBackgroundColor('#000');

var win1 = Titanium.UI.createWindow({  
  backgroundColor: '#fff'
});
win1.title = 'win 1';

var win2 = Titanium.UI.createWindow({  
  backgroundColor: '#fff'
});
win2.title = 'win 2';

var tab1 = Titanium.UI.createTab({  
  icon: 'KS_nav_views.png',
  title: 'Tab 1',
  window: win1
});

var tab2 = Titanium.UI.createTab({  
  icon: 'KS_nav_ui.png',
  title: 'Tab 2',
  window: win2
});

var tabGroup = Titanium.UI.createTabGroup();
tabGroup.addTab(tab1);  
tabGroup.addTab(tab2);  

tabGroup.open();

このコードで実行すると、こんな画面が表示されます。

下記のTabGroupいらないような画面だったので、使わないコードにすると、title が残るようになるのかなーと思うと、そうはいかなかったりする。

Titanium.UI.setBackgroundColor('#000');

var win1 = Titanium.UI.createWindow({  
  backgroundColor: '#fff'
});
win1.title = 'win 1';

var win2 = Titanium.UI.createWindow({  
  backgroundColor: '#fff'
});
win2.title = 'win 2';

var tab1 = Titanium.UI.createTab({  
  icon: 'KS_nav_views.png',
  title: 'Tab 1',
  window: win1
});

var tab2 = Titanium.UI.createTab({  
  icon: 'KS_nav_ui.png',
  title: 'Tab 2',
  window: win2
});

/*
var tabGroup = Titanium.UI.createTabGroup();
tabGroup.addTab(tab1);  
tabGroup.addTab(tab2);  

tabGroup.open();
*/

win1.open();

このコードを実行すると、下記のように真っ白になってしまうのである。

確かに iPhone などの実装をみると NavigationGroup に入ってる必要がある(?)ので、上記のことをやろうとするならば、Titanium.UI.iPhone.NavigationGroupで書きなおせば動くけど、Titanium.UI.currentTab 的な便利なのもなくなってしまうので、Titanium.UI.currentTab.open(window, props)もできなくなってしまうのである。
困ったなーと思って API を見ていると Titanium.UI.Window の property に tabBarHidden なんてものがある

確かに下記のようなコードにすることで、やりたいことが実現出来ているのだが...

:
:
:
win1.tabBarHidden = true;

var tabGroup = Titanium.UI.createTabGroup();
tabGroup.addTab(tab1);  
tabGroup.addTab(tab2);  

tabGroup.open();

これらは window の property ではなく Tab もしくは TagGroup にある方がいいんではないだろうか。
Ti.currentTab.windowTitle = 'hoge'; とかね。

グローバルがないっぽいから、Titanium.App に入れてしまおうね。ってやつ

@masuidrive に教えてもらったのをそのまま、なんですが、Titanium.App をGlobal代わりに使ってしまうやつです。
例(下記サンプルコード)が悪いですが、何度も初期化したくないようなコードは一旦 Titanium.App につめて、必要に応じてそこの値を参照するようにしてみました。

// utils.js
(function (){
  if(typeof Titanium.App.StringTemplate == 'undefined'){
    var StringTemplate = function (template, pattern){
      this.template = template;
      this.pattern = pattern || StringTemplate.Pattern;
    };
    StringTemplate.Pattern = /#\{((\d|\w)+)\}/g;
    StringTemplate.prototype.evaluate = function(parameter){
      return this.template.replace(this.pattern, function (target, match){
        return parameter[match];
      });
    };
  }

  String.prototype.template = function (){
    var proto = Titanium.App.StringTemplate;
    return new proto(this);
  };
  String.prototype.interpolate = function(obj){
    return this.template().evaluate(obj);
  };
})();

// foo.js
require('utils');
var tpl = '#{hoge} #{foo}'.template();
var value = tpl.interpolate({hoge: 'hello', foo: 'world'});

// bar.js
require('utils');
var value = 'is #{bar} value'.interpolate({bar: 'bar'});

Titanium.App.appURLToPath('app://path/to') っていう相対パス参照の方法がある。らしい

Titanium.include だったり、 Titanium.UI.Window.url だったり Titanium.UI.ImageView:image だったりパス指定の相対パスでハマりがち(特にandroidで)な、相対パス問題なんですが、
今までこんなコードを書いてどうにか対処してました。

Titanium.Platform.isAndroid = /android/i.test(Titanium.Platform.osname);
var utils = {
  resourceFile: function(resourceDirRelativeDirPath, fileName){
    var dirPath = Titanium.Filesystem.resourcesDirectory + resourceDirRelativeDirPath;
    if(Titanium.Platform.isAndroid){
      dirPath = dirPath.replace('//', '/');
    }
    return Titanium.Filesystem.getFile(dirPath, fileName);
  },
  resourcePath: function(resourceDirRelativeDirPath, fileName){
    var file = utils.resourceNativeFile(resourceDirRelativeDirPath, fileName);
    var nativePath = file.nativePath;
    if(Titanium.Platform.isAndroid){
      return nativePath.replace(/^(file\:\/\/\/android_asset\/Resources)/, '');
    }
    return nativePath;
  }
};

どうやら、Titanium.App.appURLToPath('app://path/to') で参照する方法があるらしい。

調べてみると、Titanium Desktop のメソッドらしい
Titanium Mobile の Titanium.App の API docsには無いみたいだけど、使えるならばこっちを使うほうが良さそう

setTimeout とかは気をつけましょうね。ってやつ

すごく単純で。window 単位とかで setTimeout するとコードが実行前に close しちゃってハマるかも。ってやつです。

// app.js
Titanium.UI.setBackgroundColor('#fff');

var win = Titanium.UI.createWindow({
  url: 'win1.js'
});

var root = Titanium.UI.createWindow();
var label = Titanium.UI.createLabel({
  top: 0,
  text: 'win1 value = '
});
root.add(label);
root.add(win);
root.open();

setInterval(function (){
  label.text = win.extern.value;
}, 100);

win.open();

このコードは、win1.js っていう別の window を開いて、win.extern (画面間で引き継ぐ値をいれとくハコ) の値を watch してるやつ
win1.jsの実装はこんな感じ。いつでも閉じれるように close ボタンを用意している

// win1.js
var win = Titanium.UI.currentWindow;
win.extern = {
  value: 'before timeout'
};

var label = Titanium.UI.createLabel({
  text: 'hello world'
});
var button = Titanium.UI.createButton({
  title: 'close'
});
button.addEventListener('click', function (){
  win.close();
});

win.add(label);
win.add(button);

setTimeout(function (){
  label.text = 'timeouted!';
  win.extern = {
    value: 'timeout!'
  };
}, 5000);

実行してもらえるとわかるけど、setTimeout が発生しているタイミングによっては、値が入ってなかったりする(サンプルコードが分かりにくいけど)

なので、こういうコードは close を押させないようにするとか、実行するデータは database に書いておくとか、復帰可能なコードを残しておく必要があるようです。
少なくとも window が閉じると、そのコンテキストで動作していた値の参照はできなくなるようです。

その他

否定的なことばかりな事ばかり書いてるけど、こんなに簡単に(しかも javascript で!) iPhone とか android のコードが書ける Titanium Mobile は好きです。(マゾではないですよ。一応)

思い出したら、新しく書き起こす。きっと。。

以前のログ