About a month ago I embarked on the project of writing a poker bot. It has all the things I enjoy about programming: it’s open-ended and can always be improved, it can be played against, and AI features heavily. There’s also a wealth of research on the topic: poker-ai.org and The University of Alberta Computer Poker Research Group are the two sites I use the most.

I started by plugging in to Poker Academy, a game featuring strong AI opponents and a decent user interface. It allows you to hook in your own bots using their own Java-based API, called Meerkat. This was and still is a great tool to test with, but it has its drawbacks. You can only run one instance at once, it’s quite slow, and it has no networking capability. I wanted something I could use to demo the bot online.

So, inevitably, I ended up writing a poker server. I’d already written most of the game engine out of necessity for the poker bot; it had to model and understand the game before it could play it. The challenge was to extract this engine out of the code, and build it into a multi-client game server.

For the server to be useful, it had to meet a few basic requirements:

  • It must handle multiple games at once
  • It must support bot and human players
  • It must be accessible via a browser; no downloads
  • It must not be limited to one particular protocol
  • The poker server should not be coupled to a webserver

The key features here are concurrency, scalability and loose coupling. I first needed something to handle the networking, and EventMachine was the obvious choice (I’m using Ruby for this, not Perl.) EventMachine supports many protocols, so I’d support plain TCP for remote bot/telnet clients. But because the main interface will be web-based, I decided to build in WebSocket support right away.

The good news is that EventMachine already has a WebSocket server. Unfortunately, it works a little differently to the TCP server, so I had to create a generic connection handler that could wrap TCP, WebSocket and other connections.

The main EventMachine loop simply starts the various servers. For now I just have TCP, keyboard and WebSocket servers:

    def start
      # Start listening on various protocols: keyboard, telnet and websocket.
      # Players can use any of these.
      EventMachine.run do
        create_games_from_config
        EventMachine.start_server("0.0.0.0", 10000, Merlion::Lobby::TelnetServer, self)
        EM.open_keyboard(Merlion::Lobby::KeyboardHandler, self)
        Merlion::Lobby::WebSocketServer.instance.init(self)
        Merlion::Lobby::WebSocketServer.instance.start_server
      end
    end

Each type of connection needs to implement two basic things:

  • handle: Handling a command from the client
  • write: Sending data to the client

To handle these in a generic way, I’ve written a module called ConnHelper, designed to be mixed into Connection classes. ConnHelper implements handle, and the Connection class should implement write.

When a message comes in, it’s handled by a Connection object. This could be a subclass of EventMachine::Connection or in the case of WebSocket, a separate WebSocketConnection which I’ve implemented. These classes all include the ConnHelper module. The job of the Connection class is to:

  • Process incoming messages and pass them to handle()
  • Implement write so that messages can be sent back to the client.

For TCP clients, this is straightforward:

    class TelnetServer < Merlion::Lobby::TextClient
      include Merlion::Lobby::ConnHelper

      def receive_data(data)
        handle(data)
      end

      def write(msg, channel)
        send_data(msg)
      end
    end

But for WebSocket, it’s a little bit trickier. We need to create a WebSocketConnection when the client connects, and marshal messages to it like so:

def start_server
  EM::WebSocket.start(:host => '0.0.0.0', :port => 11111) do |ws|
    ws.onopen do |handshake|
      @ws_conns[ws.object_id] = Merlion::Lobby::WebSocketConnection.new(ws, self.lobby)
    end
    ws.onmessage do |msg|
      = @ws_conns[ws.object_id]
      obj.handle(msg)
    end
  end
end

Finally, WebSocketConnection implements write, so we can write data to WebSocket clients. Because the WebSocket client is likely to be a JavaScript and not a human, we can take advantage of JSON to provide extra information about the state of the game. So write does not write raw strings; instead, it converts the message to JSON and sends that. In addition, it includes the notion of a ‘channel’ to describe the type of message. This makes it easier to handle on the client side, which I’ll explain in a later post.

class WebSocketConnection < JSONClient
  def initialize(ws, lobby)
    @ws = ws
    @lobby = lobby
  end

  def write(msg, channel)
    payload = {
      merlion: [channel, msg]
    }.to_json
    @ws.send(payload)
  end
end

And that’s my basic framework for message handling in the poker server. It can receive data from any EventMachine servers and handle them in a generic way. When we want to write to the client, we just need to call write on the connection object and it’ll do the right thing. And it’s all asynchronous.

You might have noticed this doesn’t have anything to do with poker. You would be correct; it doesn’t have anything to do with poker. For that, we’d have to implement handle and connect players to the games, calling write at significant points in the game so the client knows what’s going on. But I’ll save that for another blog post :)

-Mike

Writing a poker server in Ruby: part 1

Post navigation


Leave a Reply

Your email address will not be published. Required fields are marked *