July 23, 2015

The task that is solved here is not real, but it’s still a good example of (probably?) real work with Opal. I could choose some complex enough JavaScript library and write a simple wrapper using Opal, but there’s no fun. Instead, let’s write a wrapper for existing rich client-side application (it may show you how to wrap your existing application logic). Well, wrapper for something like a client-side scheduler may sound boring, so I’ve chosen a JS-based browser game called BrowserQuest written by Mozilla, and I’ll show you how to write a bot for it using Opal.

There are so many posts about Opal, so I’m just going to say “it’s a Ruby to JavaScript” compiler, that’s enough.

First of all, we need something that runs the game and injects a bot into the page. I, personally, while writing integration tests (this is the place, where we usually face to web drivers), prefer PhantomJS, but it’s headless, so you can’t enjoy watching how your bot works. We have to use something like Capybara + Selenium:

# Gemfile gem 'capybara' gem 'selenium-webdriver' # runner.rb require 'capybara' Capybara.register_driver :selenium do |app| Capybara::Selenium::Driver.new(app, :browser => :firefox) end Capybara.javascript_driver = :selenium Capybara.default_driver = :selenium Capybara.run_server = false

So, the script registers a driver, specifies its browser (Firefox), makes it default and runs Capybara in browser mode (i.e. without own server in the background)

Opening the page #

Dead simple:

Capybara.current_session.visit('http://browserquest.mozilla.org') # or in object-oriented style class Game include Capybara::DSL def initialize(url) # all methods of # Capybara.current_session # are available here visit(url) end def play # logic of the bot end end Game.new('http://browserquest.mozilla.org/').play

Now it runs a Firefox and opens the page with the game.

Compiling Opal #

So, there are two ways to compile Ruby into Javascript:

compiling Ruby code as a string to JS string

compiling specified Ruby file to JS string

To compile a file with Ruby, run:

Opal.append_path('some/path/to/dir/with/your/files') Opal::Builder.build('relative/path/from/that/dir/to/you/file')

To compile a string with ruby:

Opal.compile("plain ruby code")

The first way is what we really need:

create a directory with all opal files add it to Opal’s load path (all require commands work as in MRI) create a file called app.rb that require -s other files embed app.rb to the page

Fetching the data from the game #

This is the place where the main App class is created. But! It’s defined in anonymous function, so this variable is not available outside the context.

This game uses CommonJS for loading files. This library caches(?) all previously required files and instantly returns cached result on the seconds require .

We can use it:

require app file. wrap any of its methods with some logic that stores current app instance globally and then call super

I’ve chosen a method called start :

# opal/bot.rb module Patch def self.apply %x{ var app = require('app'); oldStart = app.prototype.start; app.prototype.start = function(username) { window.currentApplication = this; oldStart.apply(this, arguments); } } end end Patch.apply

Some explanations:

%x{js code} just passes provided JS into compiled version (i.e. runs it without any translation)

just passes provided JS into compiled version (i.e. runs it without any translation) After compiling this file we have access to a variable currentApplication that contains an instance of App class

Starting the game #

As you can see, to start the game you need to:

type your player name wait for ‘Play’ button to activate (become red) press ‘Play’ button wait until all assets will be loaded close instructions that the game opens for any new player

After all of these steps the game will be ready, but the point here is that most of the steps are asynchronous. You can’t just type your name and immediately press ‘Play’ button (and you can’t press ‘Play’ without waiting for loading)

This is the place where promises shine. Opal has its own standard library that

ships with Opal’s code, so it’s already in the page

has a class called Promise that acts pretty much like a jQuery.Deferred()

require 'promise' promise = Promise.new promise.then { puts 'Done' } promise.fail { puts 'Fail' } promise.resolve # => 'Done' (in JS console) # or promise.reject # => 'Fail'

( Promise is like an object that is a combination of callback -s and errback -s, but you don’t invoke callbacks manually, instead you just switch the state of your promise-object and it automatically triggers callbacks/errobacks)

Here is a little helper module that saves our time:

module Utils def wait_for(promise = Promise.new, &waiting) result = waiting.call if !!result promise.resolve else after 0.1 do wait_for(promise, &waiting) end end promise end end # and usage class MyClass include Utils def call some_async_method_without_ability_to_pass_callback wait_for do method_called && result_is_success end end end

wait_for method takes a promise (which is a blank promise object by default) and a block (which will be converted to JS function). It calls the block and resolve -s the promise if it is returned true. If not, it calls itself again after 100ms ( after = setTimeout ) with the same promise object

To type player’s name we should run:

# I'm using opal-jquery here # I think it doesn't require any explanation input = Element.find('#nameinput') input.value = @player_name wait_for do Element.find('.play.button.disabled').length == 0 end.then do # Button is ready, we can click it here end

To click the button, run:

button = Element.find('#createcharacter .play.button div') button.trigger(:click) wait_for do Element.find('#instructions').has_class?('active') end.then do # The game is ready here # And it shows us instructions # We are almost ready to start the game end

To close instructions, run:

Element.find('#instructions').trigger(:click)

And put everything together

To run this command, call

StartGame.new('Bot player').invoke.then do alert("I'm in the game") end

Time to wrap the code of the game #

As an enter point we are going to use global JS variable currentApplication . It has a property game (that, unexpectedly, returns instance of Game class). game has a player property (instance of Player ) and entities property which is an object containing all entities on the map, their types and coordinates. You can easily find their JS implementations in the GitHub repository of the game.

So, our main objects are:

currentApplication

currentApplication.game

currentApplication.game.player

currentApplication.game.entities

First class for wrapping is definitely an App :

class Application include Native def self.current self.new(`currentApplication`) end def initialize(native) @native = native end alias_native :game, :game, as: Game def to_n @native end end

So, we have a class called Application that wraps some native JS object and has a ruby-method game that calls JS-method game and wraps it using Game class (see below). As a bonus, we have a class-method current that returns wrapped currentApplication .

The next class is a Game :

class Game include Native def self.current Application.current.game end def initialize(native) @native = native end alias_native :player, :player, as: Player alias_native :say def to_n @native end def entities res = [] native_entities = `currentApplication.game.entities` Native::Hash.new(native_entities).each do |e_id, e| res << Native(e) end EntityCollection.new(res) end end

And again, this class can wrap any JS game object, has methods player , say and entities ( EntityCollection is our next class to implement).

(we can test method say write now, just put Game.current.say('Hello') to the block where the game is ready and start chatting with other players)

The game provides a global JS object Types with all mobs/items/armors/weapons information, it allows to identify unknown entity, compare armors and weapons by rank. Basically, it provides everything for writing a bot logic.

To convert it to Ruby, use Types = Native(`Types`) and use this object in the Ruby world!

Here is my definition of Entity class:

class Entity include Native def initialize(native) @native = native end def to_n @native end alias_native :kind def player? Types.isPlayer(kind) end # some other methods # like mob? # or heal? def weapon_rank Types.getWeaponRank(kind) end def armor_rank Types.getArmorRank(kind) end end

Well, this class can wrap player/mob/armor/weapon/healing, but this is only a value-object, we still need to implement our collection-object EntityCollection :

class EntityCollection def initialize(native_entities) @entities = native_entities.map do |native_entity| Entity.new(native_entity.to_n) end end def players entities = @entities.select(&:player?) EntityCollection.new(entities) end # similar methods like # mobs/weapons/armors/healings # are omitted and are just like 'players' method end

Player class #

(quickly and without any explanation):

class Player include Utils include Native def self.current Game.current.player end def initialize(native) @native = native end alias_native :distance_to, :getDistanceToEntity alias_native :moving?, :isMoving alias_native :attacking?, :isAttacking alias_native :hp, :hitPoints alias_native :max_hp, :maxHitPoints def full_hp? hp == max_hp end alias_native :weapon_name, :getWeaponName alias_native :armor_name, :getArmorName end

Writing the code of the bot #

It’s not as difficult once we have all these classes prepared. The algorithm of farming is like:

Find a closest mob and kill it Find a closest weapon (and pick up if it’s enough close) Find a closest armor (and pick up if it’s enough close) Find a closest healing (and pick up if it’s enough close) GOTO 1

All of these steps will be our methods, and all of them must be asynchronous.

Just one method is missing here ( closest ):

class EntityCollection def by_distance entities = @entities.sort_by do |entity| Player.current.distance_to(entity) end EntityCollection.new(entities) end def first @entities.first end def last @entities.last end def closest by_distance.first end end

Killing a mob #

def kill_mob closest_mob = Game.current.entities.mobs.closest `#{Game.current.to_n}.makePlayerAttack(#{closest_mob.to_n})` # TODO: move this method to the game class # using alias_native :) end

Picking up an abstract item #

def pick_up `#{Game.current.to_n}.makePlayerGoToItem(#{item.to_n});` end

Picking up a weapon #

def get_armor current_weapon_name = Player.current.weapon_name weapons = Game.current.entities.weapons closest_weapon = weapons.better_than(current_weapon_name).closest if closest_weapon.nil? # No weapon, probably next time return end if Player.current.distance_to(closest_weapon) > 100 # Weapon is too far away, next time return end pick_up(closest_weapon) end

Picking up an armor #

Just like a previous snippet, but with armors instead of weapons

All of these steps should return promises, every single method written below should wait for player to stop moving and attacking. To make this we need some common method like:

def wait_until_inactive promise = Promise.new wait_for do !Player.current.moving? && !Player.current.attacking? end.then do # Wait 1 more second to continue after 1 do promise.resolve end end promise end

And put it to the end of each action method.

Wrapping a wrapper #

We need the main method farm , right?

def farm kill_mob.then do get_weapon.then do get_armor.then do heal.then do farm end end end end end

This, is probably the thing that I’ve personally learned during writing this article. Even if you think in Ruby, you still have to deal with asynchronous components like callbacks/promises. When you need to make an HTTP request in Ruby, you just get you favorite HTTP adapter (mine is RestClient ), send a request and your interpreter waits for response. In JS you have to process response in some callback, because you can’t just stop your interpreter (you know, it blocks UI).

As for me, the main thing Opal gives to you is some ability to think in terms of Ruby classes/modules/inheritance system. But it doesn’t let you completely escape from JS ecosystem (no callbacks? - block is a callback). I would say, most of Opal functionality related to Ruby classes can be replaced with, for example, JsClass library (which is really wonderful). Opal allows you to compile existing Ruby libraries to JavaScript and use them on the client - this is probably the main feature. Some day significant amount of Ruby libraries will be ported to client-side and probably some day we will think in terms of Ruby even on the client.

31 Kudos