Skip to content

m-nori/daihinmin

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

75 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

大富豪大貧民用サーバ

本プログラムは大富豪大貧民のAIを動作させるためのプラットフォームとなっている。
ルールに基づいて作成されたAIプログラムがゲームを行うための機能を提供する。

  • 参加ユーザの追加、削除
  • ゲームを実行するための場の作成
  • 場とユーザの結びつけ
  • ゲームの進行管理
  • 手札の配布
  • 参加プレイヤーへの状況通知
  • プレイヤー毎の限定情報提供用のAPI
  • プレイヤーからの手の受け取り
  • 場に出したカードが出すことが可能だったかどうかの判定
  • ゲームごとの順位管理
  • 各種管理用画面
  • ゲーム進行画面
  • ゲーム結果表示画面
  • 手動プレイ用画面
名前 意味
User ゲームに参加するユーザ。
Place 参加するユーザとゲームを保持する場のこと。場の中でゲームが行われる。
Player 場に参加するユーザのこと。
Game 場の中で行われるゲーム。大富豪大貧民の基本単位。
Turn ゲームの中でのプレイヤーの一行動。

ルールはシンプルにすることによりAI作成の難易度は下げている。

  • 1Placeにつき50ゲーム行う。
  • ゲームは5ユーザで対戦を行う。
  • 5ユーザの内訳は大富豪、富豪、平民、貧民、大貧民
  • 大富豪・大貧民は2枚、富豪・貧民は1枚のカード交換を行う。
  • 交換は、最弱カードと最強カードを自動で交換する。
  • カードの枚数はジョーカーを2枚含めた54枚。
  • 場に出せないカードを出したらその時点で負け。
  • 手札を一定時間以内に出せなかった場合は負け。
  • 初回はハート3スタート
  • 2回目以降は大貧民スタート(並び順は変更しない)
  • 階段(3枚以上の場合のみ)
  • 8切り
  • 革命あり(ペアで4枚以上であればジョーカーを含んでいてもOK)
  • 革命返し(革命条件を満たしている場でも、偶数回カードが出されている場合は革命返しとみなす。)
  • ジョーカーor最強カード上がり禁止(行った場合はミスとみなす)
  • 都落ち
  • イレブンバック
  • ジョーカーに対するスペード3返し
  • 階段革命
  • 縛り
  • 開発言語
    • Ruby、JavaScript
  • フレームワーク
    • Ruby on Rails
  • DB
    • MySQL
  • 対象ブラウザ
    • GoogleChrome、FireFox4

モデルイメージ

サーバとAI間の通信はWebSocketプレイヤー用APIを使用する。
用途は以下のとおり。

  • WebSocket
    • 全プレイヤーに共通的に提供できる情報を送信する為に使用する。 ゲームの開始通知やターンの開始通知などを全プレイヤーに送信する。
  • プレイヤー用API
    • 手札の取得や、場にカードを出す為に使用するHTTP通信のAPI。 プレイヤー毎に個別に処理する必要が有るため、使用するためにはユーザ認証を行ってから仕様する必要がある。

通信イメージ

ゲームの進行状況に合わせてサーバ側から送信される。
データはJSON形式となり、すべてのデータに以下の情報が含まれる。

  • place
    • 場のID。自分の参加している場以外の情報も送信されてくるため、自分の場かどうかの判断をしてから処理する必要がある。
  • operation
    • 行われたオペレーション。処理の判定に使用する。
  • card
    1. joker:ジョーカーかどうかのフラグ。trueの場合ジョーカー。
    2. mark:カードのマーク。1〜4でどれが何かは決めていない。
    3. number:カードの数字。1〜13。

場の開始時に送信される。

  • place
    • 場の情報。

例:

{"place_info":{
  "created_at":"2011-05-19T10:34:49Z",
  "game_count":3,
  "id":26,
  "title":"Place2",
  "updated_at":"2011-05-19T10:34:49Z"},
"operation":"start_place",
"place":26}

ゲームの開始時に送信される。

  • game
    • ゲームの情報。

例:

{"game":{
  "created_at":"2011-05-21T01:21:50Z",
  "id":269,"no":1,
  "place_id":26,
  "place_info":"Nomal",
  "status":0,
  "updated_at":"2011-05-21T01:21:50Z"},
"operation":"start_game",
"place":26}

ターンの開始時に送信される。

  • player
    • ターンの回ってきたプレイヤーの名前。
  • place_cards
    • 現在場に置かれているカード。配列になっており、カードがない場合は空の配列となる。
  • place_info
  • 場の情報。"Nomal"の場合通常、"Revolution"の場合革命中。

例:

{"player":"User3",
"place_cards":[
  {"card":
    {"created_at":"2011-04-20T13:32:09Z",
    "id":34,
    "joker":false,
    "mark":3,
    "number":8,
    "updated_at":"2011-04-20T13:32:09Z"}},
  {"card":
    {"created_at":"2011-04-20T13:32:09Z",
    "id":47,
    "joker":false,
    "mark":4,
    "number":8,
    "updated_at":"2011-04-20T13:32:09Z"}}],
"place_info":"Nomal",
"operation":"start_turn",
"place":26}

プレイヤーが上がった場合、又はミスした場合に送信される。

  • player
    • 対象のプレイヤーの名前。
  • rank
    • 対象のプレイヤーのランク情報。rank.rank.rankが順位になる。

例:

{"player":"User5",
"rank":{
  "rank":{"created_at":null,
  "game_id":269,
  "player_id":15,
  "rank":1,
  "updated_at":null}},
"operation":"end_player",
"place":26}

ターンが終了したあと送信される。

  • player
    • 対象のプレイヤーの名前。
  • turn_cards
    • プレイヤーが出したカード。配列になっており、パスされた場合は空の配列となる。
  • reset_place
    • 場がリセットされるかどうかのフラグ。リセットされる場合true。

例:

{"player":"User3",
"turn_cards":[
  {"card":
    {"created_at":"2011-04-20T13:32:09Z",
    "id":34,
    "joker":false,
    "mark":3,
    "number":8,
    "updated_at":"2011-04-20T13:32:09Z"}},
  {"card":
    {"created_at":"2011-04-20T13:32:09Z",
    "id":47,
    "joker":false,
    "mark":4,
    "number":8,
    "updated_at":"2011-04-20T13:32:09Z"}}],
"reset_place":true,
"operation":"end_turn",
"place":26}

ゲームが終了したあと送信される。

  • game
    • ゲームの情報。

例:

{"game":{
  "created_at":"2011-05-21T01:22:15Z",
  "id":270,
  "no":2,
  "place_id":26,
  "place_info":"Revolution",
  "status":1,
  "updated_at":"2011-05-21T01:22:39Z"},
"operation":"end_game",
"place":26}

場が終了したあと送信される。

  • place
    • 場の情報。

例:

{"place_info":{
  "created_at":"2011-05-19T10:34:49Z",
  "game_count":3,
  "id":26,
  "title":"Place2",
  "updated_at":"2011-05-19T10:34:49Z"},
"operation":"end_place",
"place":26}

プレイヤーが自分からアクセスすることで使用することが出来るHTTPのAPI。

APIを使用するためにはこのURLにアクセスしてログインを行う必要がある。
現時点ではログイン情報をCookieに保存するため、Cookie保存を行える言語で実装する必要がある。
Cookie無しでログインできるようにするかは検討中。

ユーザの認証を行う。
クエリーパラメータは以下のとおり。

  • name
    • ユーザ名
  • password
    • パスワード
  • place_id
    • 場のID

情報取得はJSONとXMLにて行える。(JSON推奨)
URLに".json"を付与した場合JSON、".xml"を付与した場合XMLとなる。
HTTPメソッドはget

手札の取得を行う。

  • cards
    • カードの配列。JSONの場合は配列のみ。
  • card
    • カードの詳細情報。

例:JSON

[
  {"card":{"id":20,"joker":false,"mark":2,"number":7}},
  {"card":{"id":19,"joker":false,"mark":2,"number":6}},
  {"card":{"id":6,"joker":false,"mark":1,"number":6}},
  {"card":{"id":22,"joker":false,"mark":2,"number":9}}
]

例:XML

<cards type="array">
  <card>
    <id type="integer">20</id>
    <joker type="boolean">false</joker>
    <mark type="integer">2</mark>
    <number type="integer">7</number>
  </card>
  <card>
    <id type="integer">19</id>
    <joker type="boolean">false</joker>
    <mark type="integer">2</mark>
    <number type="integer">6</number>
  </card>
  <card>
    <id type="integer">6</id>
    <joker type="boolean">false</joker>
    <mark type="integer">1</mark>
    <number type="integer">6</number>
    </card>
  <card>
    <id type="integer">22</id>
    <joker type="boolean">false</joker>
    <mark type="integer">2</mark>
    <number type="integer">9</number>
  </card>
</cards>

場の情報を取得を行う。

  • game_count
    • ゲームの実行回数。
  • player_count
    • 参加しているプレイヤーの人数。
  • player_infos
    • プレイヤーの情報の配列。JSONの場合は配列のみ。
  • player_info
    • プレイヤーの情報。ユーザ名と持っているカードの残数。

例:JSON

{"game_count":10,
"player_count":5,
"player_infos":[
  {"name":"User1","has_card":4},
  {"name":"User2","has_card":1},
  {"name":"User3","has_card":6},
  {"name":"User4","has_card":7},
  {"name":"User5","has_card":4}]}

例:XML

<hash>
  <game-count type="integer">10</game-count>
  <player-count type="integer">5</player-count>
  <player-infos type="array">
    <player-info>
      <name>User1</name>
      <has-card type="integer">4</has-card>
    </player-info>
    <player-info>
      <name>User2</name>
      <has-card type="integer">1</has-card>
    </player-info>
    <player-info>
      <name>User3</name>
      <has-card type="integer">6</has-card>
    </player-info>
    <player-info>
      <name>User4</name>
      <has-card type="integer">7</has-card>
    </player-info>
    <player-info>
      <name>User5</name>
      <has-card type="integer">4</has-card>
    </player-info>
  </player-infos>
</hash>

データの送信フォーマットは未定。(現在はクエリーパラメータを使用)
HTTPメソッドはpost

手札を場に出す。
クエリーパラメータは以下のとおり。

  • card_0 〜 5
    • 出すカードを一つずつ指定して格納する。 パスの場合はクエリーパラメータを指定しないで出す。
    • 格納する値の使用は以下のとおり。
      • ジョーカーの場合:joker
      • ジョーカー以外の場合:#{マーク}-#{数字}

例:ハート3、ダイヤ3、ジョーカー

card_0 => "2-3"
card_0 => "3-3"
card_0 => "joker"

ゲームの進行はゲーム進行画面によって行われる。
start_turn以外のタイミングでゲーム進行画面がサーバにnext_turn通知を行うことで次の処理へと遷移する。
start_turnの次のみプレイヤーが場にカードを出すことで次の処理へと遷移する。(ただしタイムアウトした場合は強制的に次の処理へ進む)
これはゲームの進行を画面から確認できるようにするためのものである。

No. サーバ ゲーム進行画面 プレイヤー
1 start通知を行う。
2 start_placeを送信する。
3 next_turn通知を行う。
4 start_gameを送信する。
5 next_turn通知を行う。
6 start_turnを送信する。
7 場にカードを出す。
8 end_playerを送信する。
9 end_turnを送信する。
10 next_turn通知を行う。
11 end_playerを送信する。
12 end_gameを送信する。
13 next_turn通知を行う。
14 end_placeを送信する。
  • No.4〜13までの処理をGame回数分実行する。
  • No.6〜9までの処理をGameが終了するまで実行する。
  • No.8はNo.7の処理にてプレイヤーが終了した場合のみ発生する。
  • No.11はプレイヤーが残り一人になった場合のみ発生する。

AIは通信ルールに従って実装を行ってあれば言語・プラットフォームに依存しない。
また、本ドキュメントにより公開されていないAPIを使用した場合は失格とする。

AIを実装する言語は以下の機能を実装できる必要がある。
WebSocketの受信はJavaScript、その他の処理はJava等の使い分けでも問題ない。

  1. WebSocket受信
  • サーバからの通知を受信するために必要。
  • Socket通信ができる言語であれば実装可能
  1. HTTP通信
  • APIにアクセスするために必要。
  1. Cookieの保存
  • APIへのログインにて必要。
  • JavaなどではApatchのHttpClient等を使用することで実装可能
  1. スレッド処理
  • サーバからの通知は非同期で行われるため、通知に対する処理は別スレッドで処理することが望ましい。

WebSocket自体の仕様はWikipedia等に仕様は記載されている。
GoogleChromeやFireFox4であればブラウザ側で実装されているのでJavaScriptにて使用することができる。
シンプルな仕様なので他の言語でゼロから作るのもそれほど難しく無いが、JavaやPythonにはClient側のライブラリもいくつか存在しているのでそれを使っても問題ない。
SSL(wss://)は使用する予定が無いため、対応させる必要は無い。
サンプルのソースはRubyのEventMachineの上で動くように作った物。
Socketの中身は適当なところが多いが、問題なく動作する。

require "EventMachine"

class WebsocketClient < EventMachine::Connection
  include EventMachine::Deferrable
  
  def initialize
    @callbacks = []
  end
  
  def self.connect(host, port, callback = nil, &block)
    EventMachine::connect(host, port, self) do |conn|
      conn.add_callback(callback) unless callback.nil?
      conn.add_callback(nil, &block) if block_given?
    end
  end  
  
  def add_callback callback = nil, &block
    @callbacks.push(callback || block)
  end

  def post_init
    request = "GET / HTTP/1.1\r\n"
                request << "Upgrade: WebSocket\r\n"
                request << "Connection: Upgrade\r\n"
                request << "Host: 127.0.0.1\r\n"
                request << "Origin: TODO\r\n"
    request << "\r\n"
    send_data request
    @data = ""
    @handshake = false
    @index = 0
  end

  def receive_data data
    @data << data
    if !@handshake
      if @data =~ /[\n][\r]*[\n]/m
        if @data =~ /HTTP\/1.1 101 Web Socket Protocol Handshake/
          @handshake = true
          @data = ""
        else
          close_connection  
        end
      end  
    elsif @data =~ /^\x00(.*)\xff$/m
      @data.scan(/^\x00(.*)\xff$/m) do |message|
        @callbacks.each do |callback|
          callback.call message
        end  
      end  
      @data = ""
      @index += 1;
    end
  end

  def unbind
    puts "A connection has terminated"
  end    
end

プレイヤーAPIを使用するためにはまずログインを行い、そのクッキーを使用してアクセする必要がある。
そのためJavaではApatchのHttpClientライブラリ等を仕様する必要がある。
サンプルはRubyのMechanizeというライブラリを使っており、そこでクッキー管理やHTMLスクレイピングを行っている。
そのためloginやpost_cardsはformに対するsubmitだけで実行している。

require 'Mechanize'

class PlayerAccsesor
  LOGIN_URL = "/login"
  GET_HAND_URL = "/operations/get_hand.json"
  OPERATION_URL = "/operations"

  def initialize(url, user_name, password, place_id)
    @url = url
    @agent = Mechanize.new
    @agent.log = Logger.new($stderr)
    @agent.log.level = Logger::INFO

    # login execute
    page = @agent.get(@url + LOGIN_URL)
    form = page.forms.first
    form["name"] = user_name
    form["password"] = password
    form["place_id"] = place_id
    form.submit
  end

  def get_hand
    json = @agent.get_file(GET_HAND_URL)
    JSON.parse(json)
  end

  def post_cards(cards)
    page = @agent.get(OPERATION_URL)
    form = page.forms.first
    cards.each_with_index do |card,i|
      form["card_#{i}"] = card_to_string(card)
    end
    form.submit
  end

  private
  def card_to_string(card)
    if card[:joker]
      "joker"
    else
      "#{card[:mark]}-#{card[:number]}"
    end
  end
end

AIの根幹部分となるフロー制御部分。
WebSocketにて受信したJSONのoperationに応じて処理を行う必要がある。
start_turnにて自分のターンであれば手札から出せるカードを探し、プレイヤーAPIを使ってカードを出すのが基本的な処理となる。
ゲーム開始時にプレイヤーAPIにて自分の手札を取得してからはできるだけWebSocketの情報を使って処理したほうが効率がいい。
気をつけるべき箇所として、WebSocketは非同期で送信されてくるのでWebSocketの受信をブロックしていると情報が欠ける可能性がある。
そのためWebSocketの受信部分と処理の中身は別スレッドとしたほうがいい。
また、WebSocketからはJSON形式のデータが送られてくるはずであるが、JSONパーサでは空の文字列はパースエラーとなる。
空の文字列は送られてくる可能性があるため、パースエラーとなっても処理を継続できるようにしておく必要がある。

EM.run do
  player_accsesor = PlayerAccsesor.new("http://localhost:3000", user_name, password, place_id)
  WebsocketClient.connect("localhost", 8081) do |data|
    begin
      json = JSON.parse(data[0])
    rescue
      puts "not json"
      next
    end
    next if json["place"] != place_id.to_i
    case json["operation"]
    when "end_place"
      exit(0)
    when "start_turn"
      Thread.new do
        # 出すカードを作る
        player_accsesor.post_cards(put_cards)
      end
    else
      puts json["operation"]
    end
  end
end
  • ゲーム結果表示画面作成
  • 手動プレイ用画面作成
  • サンプルに説明入れる…
  • プレイヤー用APIで各プレイヤーのランクを取得できるようする
  • プレイヤー用APIで初期手札(交換前)を取得できるようにする

About

大富豪大貧民サーバ

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published