Reversion of Control in Ruby Warrior

Ruby Warrior is a clever rogue-like game where, instead of playing it directly, you implement a Ruby class with your player control logic.

class Player
  def play_turn(warrior)
    # add your code here
  end
end

Level 1

At the start of each level, a README describes the horrors you will face.

Level 1

Silence. The room feels large, but empty. Luckily you have a map of this tower 
to help find the stairs.

Tip: Use warrior.direction_of_stairs to determine which direction stairs are 
located. Pass this to warrior.walk! to walk in that direction.

 ------
|      |
|@     |
|      |
|  >   |
 ------

  > = Stairs
  @ = Juez (20 HP)


Available Abilities:

  warrior.feel
    Returns a Space for the given direction (forward by default).

  warrior.direction_of_stairs
    Returns the direction (:left, :right, :forward, :backward) the stairs are 
from your location.

  warrior.walk!
    Move in given direction (forward by default).

Following the README's advice, clearing the first level is easy.

class Player
  def play_turn(warrior)
    warrior.walk! warrior.direction_of_stairs
  end
end

Or if you're like me and find that code to be unacceptably verbose, you are liable to write something like the following.

module Delegate
  def method_missing symbol, *args
    @delegate.send symbol, *args
  end
end

class Player
  include Delegate

  def play_turn warrior
    @delegate = warrior
    step
  end
end

class Player
  def step
    walk! direction_of_stairs
  end
end

Some programmers are lazy in the worst possible way.

When you play a level, you see a turn-by-turn transcript.

Starting Level 1
- turn 1 -
 ------
|      |
|@     |
|      |
|  >   |
 ------
Juez walks right
- turn 2 -
 ------
|      |
|      |
|@     |
|  >   |
 ------
Juez walks forward
- turn 3 -
 ------
|      |
|      |
| @    |
|  >   |
 ------
Juez walks right
- turn 4 -
 ------
|      |
|      |
|      |
| @>   |
 ------
Juez walks forward
Success! You have found the stairs.
Level Score: 0
Time Bonus: 16
Clear Bonus: 3
Total Score: 19

Onto the second level.

Level 2

The README shows that we are up against three sludges. These undesirables do not move and, are, therefore easily dispatched.

Level 2

Another large room, but with several enemies blocking your way to the stairs.

Tip: Just like walking, you can attack! and feel in multiple directions 
(:forward, :left, :right, :backward).

 ----
| @s |
| sS>|
 ----

  > = Stairs
  @ = Juez (20 HP)
  s = Sludge (12 HP)
  S = Thick Sludge (24 HP)


Available Abilities:

  warrior.feel
    Returns a Space for the given direction (forward by default).

  warrior.rest!
    Gain 10% of max health back, but do nothing more.

  warrior.health
    Returns an integer representing your health.

  warrior.attack!
    Attack the unit in given direction (forward by default).

  warrior.direction_of_stairs
    Returns the direction (:left, :right, :forward, :backward) the stairs are 
from your location.

  warrior.walk!
    Move in given direction (forward by default).

How would you approach this challenge?

Agaram's Approach

A pair of solutions are posted. Kartik Agaram proposed the following solution. (I have edited it by removing comments and unused methods.)

class Player
  def initialize()
    @max_health = nil
    @prev_health = nil
    @warrior = nil
    @direction = :forward
    @health_history = []
    @direction_history = []
  end

  def play_turn(warrior)
    @max_health ||= warrior.health

    @warrior = warrior
    updateHistory

    return walk! if feel.stairs?
    return rest! if needRest && !beingShotAt
    return attack! unless feel.empty?

    reverse_direction if walking_into_fire
    walk!
  end

  def needRest
    @warrior.health < @max_health
  end

  def beingShotAt
    @warrior.health < @prev_health
  end

  def updateHistory
    @prev_health = @curr_health
    @curr_health = @warrior.health
    @health_history << @curr_health
    @direction = @warrior.direction_of_stairs
  end

  def walk!
    @warrior.walk! @direction
  end

  def attack!
    @warrior.attack! @direction
  end

  def rest!
    @warrior.rest!
  end

  def feel
    @warrior.feel @direction
  end

  def walking_into_fire
    return false if @direction_history[-1] != @direction_history[-2] ||
        @direction_history[-2] != @direction_history[-3]
    @health_history[-1] < @health_history[-2] &&
      @health_history[-2] < @health_history[-3]
  rescue
    false
  end

  def reverse_direction
    @direction = (@direction == :forward) ? :backward : :forward
  end
end

What is his strategy? On each turn, Agaram makes a prioritized choice:

  • If he is next to the stairs, then he takes them.
  • If he needs to rest and is safe to do so, then he rests.
  • If he feels something in the direction of the stairs, then he attacks it.
  • If he is "walking into fire", then he walks away from the stairs.
  • Otherwise he walks toward the stairs.

How does Agaram decide whether he is "walking into fire"? Though a little tough to read, the definition boils down to:

  • For Agaram, "walking into fire" means that for the last two turns he has both lost health and faced the same direction.
When confronted with the map, how does Agaram's strategy play out?
Starting Level 2
- turn 1 -
 ----
| @s |
| sS>|
 ----
Agaram attacks Sludge
Sludge takes 5 damage, 7 health power left
Sludge attacks Agaram
Agaram takes 3 damage, 17 health power left
Sludge attacks Agaram
Agaram takes 3 damage, 14 health power left

Agaram attacks the Sludge to his east until it is good and dead.

- turn 3 -
 ----
| @s |
| sS>|
 ----
Agaram attacks Sludge
Sludge takes 5 damage, -3 health power left
Sludge dies
Agaram earns 12 points
Sludge attacks Agaram
Agaram takes 3 damage, 5 health power left
- turn 4 -
 ----
| @  |
| sS>|
 ----
Agaram walks backward
Sludge attacks and hits nothing
- turn 5 -
 ----
|@   |
| sS>|
 ----
Agaram receives 2 health from resting, up to 7 health

He retreats and recovers.

- turn 12 -
 ----
|@   |
| sS>|
 ----
Agaram receives 1 health from resting, up to 20 health
- turn 13 -
 ----
|@   |
| sS>|
 ----
Agaram walks forward
- turn 14 -
 ----
| @  |
| sS>|
 ----
Agaram walks forward
Sludge attacks and hits nothing

Another sludge stands in his way.

- turn 15 -
 ----
|  @ |
| sS>|
 ----
Agaram attacks Thick Sludge
Thick Sludge takes 5 damage, 19 health power left
Thick Sludge attacks Agaram
Agaram takes 3 damage, 17 health power left

He fights the sludge.

- turn 19 -
 ----
|  @ |
| sS>|
 ----
Agaram attacks Thick Sludge
Thick Sludge takes 5 damage, -1 health power left
Thick Sludge dies
Agaram earns 24 points
- turn 20 -
 ----
|  @ |
| s >|
 ----
Agaram receives 2 health from resting, up to 10 health

He heals again.

- turn 25 -
 ----
|  @ |
| s >|
 ----
Agaram receives 2 health from resting, up to 20 health
- turn 26 -
 ----
|  @ |
| s >|
 ----
Agaram walks right
- turn 27 -
 ----
|    |
| s@>|
 ----
Agaram walks forward
Sludge attacks and hits nothing
Success! You have found the stairs.
Level Score: 36
Time Bonus: 13
Total Score: 68

After recovering, Agaram finds his way to the stairs.

Will's Approach

Since Ruby Warrior requires that you write a play_turn method, the easiest way to dive into a solution is to come up with a turn-by-turn strategy saving little bits of state (@direction_history and @direction_history) as needed.

What if it were easy to express a step-by-step plan instead? Then you would:

  • Fight the Sludge.
  • Walk forward twice.
  • Walk right up the stairs.

Piece of cake: ignore the Thick Sludge.

class Player
  def fight!
    attack! while feel.enemy?
  end

  def main
    fight!
    2.times{walk!}
    walk! :right
  end
end

There's only one little problem with this code. The Player class expects turn-by-turn inverted control in its play_turn method. We want a player with good old fashioned step-by-step procedural control. We have suffered inverted control frameworks for far too long. Assert your rights. Revert control. Embrace the imperative. Restore the natural sequence. And iterate until your heart's content. By Ruby, it shall be done:

require 'generator'

class Symbol
  def action?
    to_s[-1] == ?!
  end
end

class Player
  def initialize
    @generator = Generator.new do |g|
      g.yield nil
      main
    end
  end

  def play_turn warrior
    @warrior = warrior
    @generator.next
  end

  def method_missing symbol, *args
    @warrior.send symbol, *args
  ensure
    @generator.yield nil if symbol.action?
  end
end

Beauty. Now I present you with a challenge. My magic stems from the generator library. What would you do if generators, coroutines, continuations, actors, threads, setjmp/longjmp, and all other natural concurrency constructs were unavailable?