Sunday, July 18, 2010

Continuing with the ruby text game

Posting from Ubuntu 10.04 now, so I'm much more ready to use ruby properly.

I've made updates to my files, as shown here:

Rakefile:
require "rake/testtask"

task :default => [:test]

Rake::TestTask.new do |test|
test.libs << "test"
test.test_files = Dir[ "test/test_*.rb" ]
test.verbose = true
end
test/test_game.rb
require 'test/unit'
require 'test/extension.rb'
require 'stringio'
require 'src/game.rb'

class GameTest < Test::Unit::TestCase

command_list = ["left", "right", "forward",
"go", "back", "shoot", "pew",
"pewpew", "map", "look", "check"]

bad_command_list = ["up", "down", "north", "3", "go wildcats",
"wepwep", "a map", "map ", "go map"]

def setup
@input = StringIO.new
@game = Game.new(@input)
end

command_list.each do |command|
must "set the inputted command: \"#{command}\"" do
#puts "set the inputted command correctly when parsing #{command}"
provide_input(command)
@game.get_command
assert_equal @game.command, command
end
end

command_list.each do |command|
bad_command_list.each do |bad_command|
must "retain the command \"#{command}\" and fail to set \"#{bad_command}\"" do
provide_input(command)
@game.get_command
provide_input(bad_command)
@game.get_command
assert_equal @game.command, command
end
end
end

def provide_input(string)
remember = @input.pos
@input << string
@input.pos = remember
end
end
src/game.rb
class Game
attr_reader :command

def initialize(readin=STDIN, output=STDOUT)
@input = readin
@output = output
@command_list = ["left", "right", "go", "back",
"shoot", "check", "look", "pew",
"pewpew", "exit", "map", "forward"]
end

def get_command
temp_command = @input.gets
temp_command.chomp!
if @command_list.index(temp_command) != nil then
@command = temp_command
end
end

def loop
while @command != "exit"
get_command
puts @command
end
end
end
This has all gone rather well. Running 'rake test' gives:
(in /media/sda1/Users/wcarss/code/ruby)
/usr/bin/ruby1.8 -I"lib:test" "/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/test_game.rb"
Loaded suite /usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
..............................................................................................................
Finished in 0.012626 seconds.

110 tests, 110 assertions, 0 failures, 0 errors
Excellent! Now that's out of the way...

What next?

We've got a game class accepting commands - I think it'd be pertinent to have a Robot who can receive them. A robot ought to have a position, ought to be able to take actions, and have a name. I figure that means making the files test/test_robot.rb and src/robot.rb

test_robot.rb:
require 'test/unit'
require 'test/extension.rb'
require 'src/robot.rb'
require 'matrix'

class RobotTest < Test::Unit::TestCase

directions = ["north", "east", "south", "west"]
base_pos = Vector[5,5]

forward_list = ["go", "forward"]
backward_list = ["back", "backward"]
new_pos = [Vector[0,1], Vector[1,0], Vector[0,-1], Vector[-1,0]]

def setup
@bot = Robot.new
end

directions.each_index do |i|
must "change direction to #{directions[i]} from #{directions[i-1]} on right" do
@bot.direction = directions[i-1]
@bot.right
assert_equal directions[i], @bot.direction
end
end

directions.each_index do |i|
must "change direction to #{directions[i-1]} from #{directions[i]} on left" do
@bot.direction = directions[i]
@bot.left
assert_equal directions[i-1], @bot.direction
end
end

forward_list.each do |command|
directions.each_index do |i|
must "move correctly forward when facing #{directions[i]}, using #{command}" do
@bot.pos = base_pos
@bot.direction = directions[i]
@bot.send(:"#{command}")
assert_equal @bot.pos, base_pos + new_pos[i]
end
end
end

backward_list.each do |command|
directions.each_index do |i|
must "move correctly backward when facing #{directions[i]}, using #{command}" do
@bot.pos = base_pos
@bot.direction = directions[i]
@bot.send(:"#{command}")
assert_equal @bot.pos, base_pos - new_pos[i]
end
end
end
end
robot.rb
require 'matrix'

class Robot
attr_reader :pos, :direction, :name, :score
attr_writer :pos, :direction, :name, :score

@@directions = ["north", "east", "south", "west"]

def initialize(name="Killbot 4000", x_pos = 0, y_pos = 0, direction="north")
@pos = Vector[x_pos, y_pos]
@name = name
@direction = direction
end

def left
@direction = @@directions[ direction_as_int() - 1]
end

def right
@direction = @@directions[ (direction_as_int() + 1) % @@directions.size]
end

def direction_as_int
@@directions.index(@direction)
end

def next_pos
angle = 90 * direction_as_int() * Math::PI/180
Vector[Math.sin(angle).round, Math.cos(angle).round]
end

def forward
@pos = @pos + next_pos()
end

def backward
@pos = @pos - next_pos()
end

def look
puts "Your windows are fogged!"
# not implemented
end

def shoot
puts "KABOOM (probably)"
# not implemented
end

alias back backward
alias go forward
alias check look
alias pew shoot
alias pewpew shoot
end
So, what on earth's going on here? Hopefully a lot of it is self-evident! The robot knows 4 directions, and I generated tests to check that left/right properly cycle through those. Then I generated a set of tests to check that, for every command, for every direction, the robot alters its position correctly. For example, if facing north using 'go', the robot should increment its y-position by 1. (I'm using north as positive, south as negative y, while west is negative x and east is positive x).

The bot itself is a little scattered for the moment - I'll collect private/public methods together next iteration, and I'm thinking I might make a "Direction" class, because a surprising amount of direction-logic had to go into the robot class. If I could just say the bot has a facing = Direction.new("north"), then print facing.to_s or .to_i, facing.left, facing.right, all these things - it would simplify the robot code tremendously. And it's all mostly done already!

Next after that I think is the World class, which will have a text-representation and some notion of simple buildings. From there I'm going to have to implement shooting stuff and looking around, which I think will involve line of sight, which I've never even really /thought about/, so it should be pretty cool.

OH! Actually, what I'll do next iteration is the following:

actually make the game loop send commands to the robot (haha, kinda important)
split direction into its own class with its own tests

then the one after that will be the world class / displaying the map

then like a half iteration should be all I need to make buildings blowupable and implement score. Sounds fun. :)

No comments: