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
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.
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?
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:
How does Agaram decide whether he is "walking into fire"? Though a little tough to read, the definition boils down to:
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.
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:
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?
Commentary