Sunday, July 25, 2010

A whole new world...

Well, not really. I've just added a lot to the tests. Codedump:
class World
def initialize(x_size=20, y_size=20, building_control=10)
remake(x_size, y_size, building_control)
end

def remake(x_size=20, y_size=20, building_control=10)
@x_size = x_size
@y_size = y_size
@building_control = building_control

@map = createMap
placeBuildings
placeRobot
end

def createMap
map = Array.new(@x_size)
map.each_index do |col|
map[col] = Array.new(y_size)
map[col].each_index do |i|
map[col][i] = " "
end
end
map
end

def display
@map.each do |col|
col.each do |i|
printf(i)
end
puts ""
end
end

def placeBuildings
1.upto(@building_control) do
placeObject("B")
end
# fills the world with buildings
end

def placeRobot
placeObject("P")
# puts Robot in a place where buildings aren't
end

def placeObject(string)
placed = false
while placed == false
x = rand(@x_size)
y = rand(@y_size)
if @map[x][y] == " " then
@map[x][y] = string
placed = true
end
end
end

def line_of_sight
# returns position of next visible object
end

attr_reader :map, :x_size, :y_size, :building_control
end
require 'test/unit'
require 'src/world.rb'

class WorldTest < Test::Unit::TestCase

@@x_size_list = Array.new(10) {|i| i+10}
@@y_size_list = Array.new(10) {|i| i+10}
@@building_control_list = Array.new(3) {|i| i+50}

def setup
@space_count = 0
@building_count = 0
@player_count = 0
@other_count = 0

@w = World.new(@@x_size_list[0], @@y_size_list[0], @@building_control_list[0])
end

must "properly set the x_size parameter" do
assert_equal @w.x_size, @@x_size_list[0]
end

must "properly set the y_size parameter" do
assert_equal @w.y_size, @@y_size_list[0]
end

must "properly set the building control parameter" do
assert_equal @w.building_control, @@building_control_list[0]
end

must "generate a 2D map" do
assert_equal @w.map[0][0].class, String
end

must "generate a 2D map to at least the specified dimensions" do
assert_equal @w.map[@w.x_size-1][@w.y_size-1].class, String
end

must "generate a 2D map not exceeding the specified dimensions" do
assert_raises(NoMethodError) do
assert_equal @w.map[@w.x_size][@w.y_size].class, String
end
end

@@x_size_list.each do |x|
@@y_size_list.each do |y|
@@building_control_list.each do |b|
# puts "#{x} #{y} #{b}: layout"


must "place #{b} buildings on size #{x} * #{y}" do
# puts "#{x}, #{y}, #{b}: buildings"
@w.remake(x, y, b)
count_map_contents
assert_equal b, @w.building_control
assert_equal b, @building_count
end

must "place #{(x * y) - (b+1)} spaces on size #{x} * #{y}" do
# puts "#{x}, #{y}, #{b}: spaces"
@w.remake(x, y, b)
count_map_contents
assert_equal ((x * y) - (b+1)), @space_count
end

must "place 1 player on size #{x} * #{y}, building control #{b}" do
# puts "#{x}, #{y}, #{b}: 1 player"
@w.remake(x, y, b)
count_map_contents
assert_equal 1, @player_count
end

must "not put any ambiguous spaces on size #{x} * #{y}, building control #{b}" do
# puts "#{x}, #{y}, #{b}: no ambiguous"
@w.remake(x, y, b)
count_map_contents
assert_equal 0, @other_count
end
end
end
end

def count_map_contents
@w.map.each_index do |col|
@w.map[col].each do |i|
if i == "B" then
@building_count += 1
elsif i == " " then
@space_count += 1
elsif i == "P" then
@player_count += 1
else
@other_count += 1
end
end
end
end
end


Sooo yeah. 1340 tests, 1640 assertions, 0 failures and 0 errors. I'm not sure whether I should do the line of sight/check code next or the linking code next, to pass the messages around. I'm betting the line of sight will be a lot easier, but it may have to change when I glue tings together - so I'm wary of pushing into it. Glad to get world into a good, test-passing state before I head to Ottawa for a week. I think I'd like to put something into the Hunters story over in Fiction, after I'm back. And keep learning physics -- and of course, finish off this robot game. It's good to design something small, to wean myself onto the process.

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. :)

Thursday, July 15, 2010

Let's make a small text game in Ruby

Want to do some stuff with Ruby? So do I! I'm going to make a small text game, where you control a robot and blow up buildings.

So let's start by thinking about how the game will work. Pretty much, I want it to be command-line interactive and turn based. So you enter a command, and then something happens, and then you can enter another thing, and so on. You should be able to turn left or right and move forward and backward, and check what's in front of you. You should also be able to shoot.

I'm thinking sensible commands would be:

left
right
forward (maybe also go)
back
shoot (also pew or pewpew)
look (or check)

everything should be lower case for now. There'll be buildings randomly placed about, and when you are facing them and shoot, they should blow up. Maybe an extra command,

map

would be useful so you can see there's a building (demarcated by a B) somewhere, and a blown up building (demarcated by an X) somewhere else, and you the player, demarcated by a P. Later, I could make you be one of ^ v > < depending on your facing or something. A score for how many buildings you blown up would be neat too.

COOL!

So where do we begin? Well, if I want it to be interactive I'm going to have to have some kind of game-loop. I *could* just make this a game you play by issuing Class.command commands in irb or something, but that feels cheap. So it makes sense to me that I'd have some sort of controller class I'll call Game. I'm also going to need a Player.. or a Robot who moves around. Yeah, I like the thought of Robot being the thing that shoots. Then I'm gonna need Buildings, and a Map.

So, the Game will have a Robot, Buildings and a Map.
each Building will have a location and a status: either it exists or it doesn't!
the Robot will have a location, a direction, and maybe a name.
the Map will have to show the Robot and the Buildings, and eventually (I hope) the facing.

So let's get started already!!!

First, Iteration 1: Let's make the Game class and the main loop inside it, which will receive commands. What do we do now? That's right, we write some tests!

I'm going to lift some code pretty closely out of Ruby Best Practices here, because it does just about exactly what we want:
class GameTest < Test::Unit::TestCase
def setup
@commandlist = ["left", "right", "forward",
"go", "back", "shoot", "pew",
"pewpew", "map", "look", "check"]
@input = StringIO.new
@game = Game.new(@input)
end

commandlist.each do |command|
must "set the inputted command correctly when parsing #{command}" do
provide_input(command)
Game.get_command
assert_equal Game.command, command
end
end

def provide_input(string)
@input << string
@input.rewind
end
end
not having run this yet or ever done testing in ruby before, I have /no idea/ if this will actually work. Having written it though, I'm going to throw my Game class together (which now essentially writes itself)
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
I don't have rake or rubygems on this computer, so I can't test this yet! I'll have to do so a little later - after a quick manual test of the Game class, it looks like it is in the right place. I realized while making it though, that I forgot to test that I'm not taking inappropriate commands! Fortunately I built it into the game class, and I'll modify the test soon to check for it.

Also of note, my directory structure at the moment:
/
/Rakefile
/src/
/src/game.rb
/test/
/test/test_game.rb
ah, and what exactly is in the rakefile? I've never used rake before! But again lifting from Ruby Best Practices (a fantastic book!), I've got
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
And that's where I'll leave it for now. More later when I can actually run tests and find out all the things I've done incorrectly. :)