2014年6月26日木曜日

人狼知能エージェントの作成・その2 Talk編

Talkの仕様変更に伴い,本記事の内容は利用できなくなりました.人狼知能エージェントの作成・その5 Talk編をご覧ください.






さて,人狼知能作成その2である. 今回は会話にチャレンジする.
とりあえず,人狼知能のサーバ,クライアントなどをGitHubにUpしたので, 中身を見たい人は https://github.com/aiwolf/ を参照して欲しい.
この記事は,書いている時点で最新のライブラリを使っているので,古いバージョンだと一部動かないこともあるかもしれないし, 新しいバージョンになっても変更があるかもしれないので,その点はご了承ください.
まだ,開発中のライブラリということで,ご勘弁を・・・
一応動かない機能が発生する場合はマイナーバージョンをアップするつもりではいます.
ちなみに,現在これを書いている段階で,バージョンは0.1.6.

発話の取得

ところで,前回までは村人エージェントについて,適当に自分以外の人に投票するようにした.
今回は,ほかの人が何を言っているのか聞いてみたいと思う.
他の人が話したことは,GameInfo#gameInfo.getTalkList()で獲得可能である.
まずは,自分の話す番が来るたびにそれまでの会話を確認するようにしてみよう.
自分が話す番が来たら,talkメソッドが呼ばれるので,そこに以下のように記述する.
 @Override
 public String talk() {
  GameInfo gameInfo = getLatestDayGameInfo();
  List<Talk> talkList = gameInfo.getTalkList();
  
  System.out.println("Today's talk");
  for(Talk talk:talkList){
   System.out.println(talk);
  }
  return null;
 }
これを実行すると,
Today's talk
Day00[000] Agent[02] 5 werewolf
中略
Today's talk
Day01[000] Agent[02] 6 werewolf
Day01[001] Agent[04] ( 11 werewolf ) and ( 4 comingout seer ) and ( 3 inspected HUMAN ) 
Day01[002] Agent[09] 4 werewolf
Day01[003] Agent[10] 2 werewolf
Day01[004] Agent[01] 12 werewolf
Day01[005] Agent[03] 12 werewolf
Day01[006] Agent[12] 2 werewolf
中略
Today's talk
Day01[000] Agent[02] 6 werewolf
Day01[001] Agent[04] ( 11 werewolf ) and ( 4 comingout seer ) and ( 3 inspected HUMAN ) 
Day01[002] Agent[09] 4 werewolf
Day01[003] Agent[10] 2 werewolf
Day01[004] Agent[01] 12 werewolf
Day01[005] Agent[03] 12 werewolf
Day01[006] Agent[12] 2 werewolf
Day01[007] Agent[06] 1 werewolf
Day01[008] Agent[11] 10 werewolf
Day01[009] Agent[11] Over
Day01[010] Agent[10] Over
Day01[011] Agent[03] Over
Day01[012] Agent[04] Over
中略
Today's talk
Day01[000] Agent[02] 6 werewolf
Day01[001] Agent[04] ( 11 werewolf ) and ( 4 comingout seer ) and ( 3 inspected HUMAN ) 
Day01[002] Agent[09] 4 werewolf
Day01[003] Agent[10] 2 werewolf
Day01[004] Agent[01] 12 werewolf
Day01[005] Agent[03] 12 werewolf
Day01[006] Agent[12] 2 werewolf
Day01[007] Agent[06] 1 werewolf
Day01[008] Agent[11] 10 werewolf
Day01[009] Agent[11] Over
Day01[010] Agent[10] Over
Day01[011] Agent[03] Over
Day01[012] Agent[04] Over
Day01[013] Agent[06] Over
Day01[014] Agent[02] 9 werewolf
Day01[015] Agent[09] Over
Day01[016] Agent[12] Over
Day01[017] Agent[01] Over
Day01[018] Agent[09] Over
Day01[019] Agent[11] Over
Day01[020] Agent[01] Over
Day01[021] Agent[06] Over
Day01[022] Agent[12] Over
Day01[023] Agent[04] Over
というように表示されるはず.
これを見れば分かる通り,getTalkListは,gameInfoが指す日のすべてのTalkを返してくる. したがって,前回からの差分は取ることができない.
差分がとりたい場合は,前回どこまでのTalkを取得したか記憶しておく必要がある.
そこで,アップデートされたTalkだけを取得するメソッドgetUpdatedTalkを作成してみる.
 @Override
 public String talk() {
  List<Talk> talkList = getUpdatedTalk();
  
  System.out.println("Updated talk");
  for(Talk talk:talkList){
   System.out.println(talk);
  }
  return null;
 }

 /**
  * 前回最後に獲得したTalkのインデックス
  */
 int lastTalkIdx = -1;
 
 /**
  * 前回最後に獲得したTalkの日付
  */
 int lastTalkDay = -1;
 
 /**
  * 前回Talkを取得した後投稿されたTalkのみを獲得する
  * @return 前回獲得したTalkとの差分が入ったList
  */
 protected List<Talk> getUpdatedTalk(){
  GameInfo gameInfo = getLatestDayGameInfo();
  List<Talk> talkList = gameInfo.getTalkList();
  
  if(lastTalkDay != gameInfo.getDay()){
   lastTalkDay = gameInfo.getDay();
   if(!talkList.isEmpty()){
    lastTalkIdx = talkList.get(talkList.size()-1).getIdx();
   }
   return talkList;
  }
  for(int i = 0; i < talkList.size(); i++){
   if(talkList.get(i).getIdx() > lastTalkIdx){
    lastTalkIdx = talkList.get(talkList.size()-1).getIdx();
    return talkList.subList(i, talkList.size());
   }
  }
  return new ArrayList<>();
 }
Updated talk
Day00[000] Agent[04] 7 werewolf
Day00[001] Agent[08] 4 werewolf
Day00[002] Agent[05] 11 werewolf
Day00[003] Agent[10] 4 werewolf
Day00[004] Agent[11] 4 werewolf
Day00[005] Agent[01] 5 werewolf
Day00[006] Agent[12] 8 werewolf
Day00[007] Agent[07] 10 werewolf
Day00[008] Agent[02] 10 werewolf
Day00[009] Agent[06] 10 werewolf
中略
Updated talk
Day00[010] Agent[09] 6 werewolf
Day00[011] Agent[08] Over
中略
Updated talk
Day02[000] Agent[01] 10 werewolf
Day02[001] Agent[10] ( 2 werewolf ) and ( 10 comingout seer ) and ( 9 inspected HUMAN ) and ( 6 inspected werewolf ) 
Day02[002] Agent[06] ( 1 werewolf ) and ( 6 comingout medium ) and ( 3 medium_telled HUMAN ) and ( 4 medium_telled HUMAN ) 
Day02[003] Agent[02] 6 werewolf
Day02[004] Agent[09] 6 werewolf
Day02[005] Agent[11] 6 werewolf
というわけで,無事前回との差分を獲得できた.

発話の内容

さて,次に他のエージェントが何を言っているのかを理解しよう.
ログに表示される
Day00[000] Agent[04] 7 werewolf
は,0日目のTalkIndex000は,Agent04が,「7 werewolf」と発言していることを示している.
Day00[011] Agent[08] Over
は,エージェント08はもうこれ以上話すことはない,という発言をしている.
さらに,
Day02[001] Agent[10] ( 2 werewolf ) and ( 10 comingout seer ) and ( 9 inspected HUMAN ) and ( 6 inspected werewolf ) 
Day02[002] Agent[06] ( 1 werewolf ) and ( 6 comingout medium ) and ( 3 medium_telled HUMAN ) and ( 4 medium_telled HUMAN ) 
では,エージェント10が,「( 2 werewolf ) and ( 10 comingout seer ) and ( 9 inspected HUMAN ) and ( 6 inspected werewolf ) 」と発言し, 同様にAgent06も長い発言をしている.
このうち,Overは良いとして,それ以外の発話はどういう意味だろうか.
まず,
Day00[000] Agent[04] 7 werewolf
は,「Agent7が人狼ではないかと疑っている」宣言である.事実上,今日投票するのはAgent7であると同等のもの・・・らしい.
(これは,SamplePlayerの実装依存だが,ソースコードを見ると,そうなっている)
次に,長い発話についてみてみよう.
Day02[001] Agent[10] ( 2 werewolf ) and ( 10 comingout seer ) and ( 9 inspected HUMAN ) and ( 6 inspected werewolf ) 
については,
  • Agent2が人狼だと思う
  • Agent10は占い師だとカミングアウトする
  • Agent9は占いの結果人間だった
  • Agent6は占いの結果人狼だった
という4つの内容が含まれた発話である.
一方,
Day02[001] Agent[10] ( 2 werewolf ) and ( 10 comingout seer ) and ( 9 inspected HUMAN ) and ( 6 inspected werewolf ) 
Day02[002] Agent[06] ( 1 werewolf ) and ( 6 comingout medium ) and ( 3 medium_telled HUMAN ) and ( 4 medium_telled HUMAN ) 
は,
  • Agent1が人狼だと思う
  • Agent6は霊媒師だとカミングアウトする
  • Agent3は霊媒の結果人間だった
  • Agent4は霊媒の結果人間だった
という内容を含んだ発話である.

発話の理解

発話は上記のようにテキストとして送られてくるため,それをエージェントが理解する必要がある. このとき,直接発話のテキストを見て,何を言っているのか理解をしてもよいが, そのためのParser(構文解析機)を作るのは手間がかかるため,aiwolf-clientライブラリでは,発話をマシンリーダブルに変換するためのParserライブラリが用意されている.
そこで,実際にTalkの内容をパースしてみよう.
Talkのパースには,Protocolクラスを利用する.
Protocolクラスは,人狼プロトコルを理解するためのクラスで,複数のUtteranceから構成され, UtteranceはSentenceTypeとPassageからなり・・・という多層構造を持っている.
大まかなProtocolクラスの概略は以下の通り.

プロトコルクラスのインスタンスをパースした発話から作成すると,この構造に従って発話をパースしてくれる.
実際に以下のコードで実行してみる.
なお,ここではその日の会話を総合して投票先を決める,ということを考え,talkメソッドではなく,voteメソッドでtalkのパースを行う.
 @Override
 public Agent vote() {
  GameInfo gameInfo = getLatestDayGameInfo();
  Agent myself = gameInfo.getAgent();

  
  List<Talk> talkList = gameInfo.getTalkList();
  for(Talk talk:talkList){
   System.out.println(talk);
   Protocol protocol = new Protocol(talk.getContent());
   List<Utterance> utteranceList = protocol.getUtterances();
   System.out.println("Num of Utterance="+utteranceList.size());
   for(Utterance utterance:utteranceList){
    SentenceType sentenceType = utterance.getSentenceType();
    Passage passage = utterance.getPassage();
    
    if(sentenceType != null){
     System.out.println("sentenceType");
     System.out.println(sentenceType.getUv());
     System.out.println(sentenceType.getRate());
    }
    
    if(passage != null){
     System.out.println("passage");
     System.out.println("Action="+passage.getAction());
     System.out.println("Attribution="+passage.getAttribution());
     System.out.println("Category="+passage.getCategory());
     System.out.println("Object="+passage.getObject());
     System.out.println("State="+passage.getState());
     System.out.println("Subject="+passage.getSubject());
     System.out.println("Verb="+passage.getVerb());
    }
    
   }
  }
  
  List<Agent> agentList = gameInfo.getAliveAgentList();

  for(Agent agent:agentList){
   if(agent != myself){
    System.out.println(myself+" vote to "+agent);
    return agent;
   }
  }
  throw new AIWolfRuntimeException("Something wrong");
 }

すると,「7 werewolf」に関してだと,以下のような出力が得られる.
Day00[003] Agent[07] 7 werewolf
Num of Utterance=1
passage
Action=null
Attribution=null
Category=ESTIMATE
Object=null
State=werewolf
Subject=Agent[07]
Verb=is
特に内容がないところにはnullが入ってくるので注意が必要.
さて,この中身について具体的にみる. まず「7 werewolf」という発話は,「Agent7は人狼だと思う」という意味を持つ.
文としては単文のため,Utteranceは1つ.
そのUtteranceを分解すると,
  • カテゴリは推定(ESTIMATE)
  • 対象(Subject)はAgent07
  • 動詞はis
  • 状態は人狼(werewolf)
となっていることが分かる.

一方,長い発話の場合はどうなるか, 「( 2 werewolf ) and ( 4 comingout seer ) and ( 12 inspected HUMAN ) and ( 10 inspected 」を例に見てみよう.
Day03[005] Agent[04] ( 2 werewolf ) and ( 4 comingout seer ) and ( 12 inspected HUMAN ) and ( 10 inspected werewolf ) 
Num of Utterance=4
passage2 werewolf部分のUtterance
Action=null
Attribution=null
Category=ESTIMATE
Object=null
State=werewolf
Subject=Agent[02]
Verb=is
passage//4 comingout seer部分のUtterance
Action=null
Attribution=null
Category=COMINGOUT
Object=seer
State=null
Subject=Agent[04]
Verb=comingout
passage//12 inspected HUMAN部分のUtterance
Action=inspected
Attribution=Human
Category=RESULT
Object=null
State=null
Subject=Agent[12]
Verb=inspected
passage//10 inspected部分のUtterance
Action=inspected
Attribution=Werewolf
Category=RESULT
Object=null
State=null
Subject=Agent[10]
Verb=inspected
となる.二番目のPassageにおいて,このエージェントがseerであるとCOMINGOUTしていることが分かる.

他エージェントの発話を考慮した投票先決定

では,最後に他のエージェントの発話に基づいて投票先を決めるようにしてみよう.
ここでは,簡単のために
「seerだとカミングアウトしたエージェントの投票に合わせる」.
という戦略を実装する.

 /**
  * 人狼候補
  */
 Set werewolfCandidateSet = new HashSet<>();
 
 @Override
 public Agent vote() {
  GameInfo gameInfo = getLatestDayGameInfo();
  Agent myself = gameInfo.getAgent();

  List<Talk> talkList = gameInfo.getTalkList();
  for(Talk talk:talkList){
   Protocol protocol = new Protocol(talk.getContent());
   List<Utterance> utteranceList = protocol.getUtterances();
   for(Utterance utterance:utteranceList){
    SentenceType sentenceType = utterance.getSentenceType();
    Passage passage = utterance.getPassage();
    
    
    if(passage != null){
     if(passage.getCategory() == Category.COMINGOUT){
      if(passage.getObject() == Role.seer){
       Agent candidate = talk.getAgent();
       seerCandidateSet.add(candidate);
      }
     }
    }
   }
  }
  
  System.out.println("Find Candidate");
  //占い師の意見だけ採用
  for(Talk talk:talkList){
   if(!seerCandidateSet.contains(talk.getAgent())){
    continue;
   }
   Protocol protocol = new Protocol(talk.getContent());
   List<Utterance> utteranceList = protocol.getUtterances();
   for(Utterance utterance:utteranceList){
    Passage passage = utterance.getPassage();
    
    if(passage.getCategory() == Category.RESULT && passage.getVerb() == Verb.inspected){
     if(passage.getAttribution() == Species.Werewolf){
      System.out.println(talk);
      Agent candidate = passage.getSubject();
      if(candidate != myself){
       werewolfCandidateSet.add(candidate);
      }
      else{
       //自分を人狼だと言ってきたら,その占い師こそが人狼だ!
       seerCandidateSet.remove(talk.getAgent());
       werewolfCandidateSet.add(talk.getAgent());
      }
     }
    }
   }
  }
  
  for(Agent candidate:seerCandidateSet){
   System.out.println("Seer candidate:"+candidate+" "+gameInfo.getStatusMap().get(candidate));
  }
  for(Agent candidate:werewolfCandidateSet){
   System.out.println("Werewolf candidate:"+candidate+" "+gameInfo.getStatusMap().get(candidate));
  }
  
  //見つかった人狼候補に生きているものがいれば,そこに投票
  for(Agent candidate:werewolfCandidateSet){
   if(gameInfo.getStatusMap().get(candidate) == Status.alive){
    System.out.println(myself+" vote to "+candidate+" by seer's result");
    return candidate;
   }
  }
  
  //生きている人狼候補がいなければ,適当に投票
  List<Agent> agentList = gameInfo.getAliveAgentList();

  for(Agent agent:agentList){
   if(agent != myself || seerCandidateSet.contains(agent)){
    System.out.println(myself+" vote to "+agent);
    return agent;
   }
  }
  throw new AIWolfRuntimeException("Something wrong");
 }

この結果以下のようになった.
Find Candidate
Day02[008] Agent[04] ( 12 werewolf ) and ( 3 inspected werewolf ) 
Seer candidate:Agent[04] alive
Seer candidate:Agent[12] alive
Werewolf candidate:Agent[03] alive
Agent[10] vote to Agent[03] by seer's result

中略

===========
Day 03
Agent[03] executed
Agent[12] divine Agent[04]. Result is Human
Agent[07] guarded Agent[09]@2 guarded
null attacked
======
Agent[01] SamplePlayer dead medium
Agent[02] SamplePlayer alive villager
Agent[03] SamplePlayer dead werewolf executed
Agent[04] SamplePlayer alive possessed divined
Agent[05] SamplePlayer alive villager
Agent[06] SamplePlayer dead werewolf
Agent[07] SamplePlayer alive bodyguard
Agent[08] SamplePlayer alive villager
Agent[09] SamplePlayer alive villager guarded
Agent[10] BasePlayer alive villager
Agent[11] SamplePlayer alive villager
Agent[12] SamplePlayer alive seer
というわけで,seer(Agent12)の占い結果に基づいてAgent03に投票し, 結果として人狼を処刑することに成功した.
今回は,他のエージェントのTalkを理解して,それを投票につなげるコードを書いてみた.
次は,自分の意見をTalkで表明することを目指してみよう.

0 件のコメント: