Gilded Rose in Lua?
The Gilded Rose ‘kata’ exists in many languages, but I’ve not seen Codea Lua. Maybe I should fix that bug. This is pretty boring. Skim, don’t read. Needed for the record.
I have no real excuse for this idea. But it does offer some interesting sub-problems. So I’ve decided to give it a go.
The conversion itself should be almost mechanical. The Gilded Rose code is horrid but uses no special tricks, so translating to Lua should be little more than direct syntax conversion. However there are issues. We’ll encounter them quickly.
The application comes with at least four components: the written spec, a sample unit test, a text test, and the code. Codea supports a main program, and separate tabs that typically contain classes or functions used by the main. The tabs must contain code: they cannot be pure text. They have to compile, or the application will not run. We’ll have to deal with that somehow. My tentative plan is to put any necessary text in as Lua comments.
Codea’s console print is very rudimentary, along the left side of the screen. This program …
-- GR1
-- Gilded Rose 1
-- Ron Jeffries 20080505
function setup()
print("Hello World!")
print("This is Gilded Rose")
end
function draw()
end
… looks like this on the iPad when we run it:
Codea has the ability to draw text on the screen, although it is not focused on that capability at all. We could do something like this:
-- GR1
-- Gilded Rose 1
-- Ron Jeffries 20080505
function setup()
print("Hello World!")
end
function draw()
fill(255)
textMode(CORNER)
text("This is Gilded Rose\nWelcoming you", 300, 950)
end
The result looks like this:
Unfortunately, Codea doesn’t tell us how far down the screen our text has extended, nor does it remember the left margin. Were we to just say text("Foo")
, the text would appear in the lower left corner of the screen.
If we do decide to put text on the main screen, rather than just the console, we’ll have to manage layout ourselves. That will be relatively easy down to the bottom of the screen, and less easy if we want to support some kind of scrolling up and down. At a rough guess, text scrolling is about as large a problem as Gilded Rose. Hurray for infrastructure!
The good news, such as it is, is that I do have a simple unit testing framework, CodeaUnit, “so that’s nice”.
I think I’ll move fairly directly to pasting in the code and translating it, and I’ll begin by setting up the unit testing framework to help me with that. However, the “released” version of this code has to go without unit tests. Writing them is part of the problem. So forget anything you accidentally learn if you read this.
I added a CodeaUnit tab (I’ll show that in Summing Up) and then added this tab named “TestGR”:
function testGildedRose()
CodeaUnit.detailed = true
_:describe("Gilded Rose Tests", function()
_:before(function()
end)
_:after(function()
end)
_:test("Hookup", function()
_:expect( 2+1).is(3)
end)
end)
end
The result is this:
Now to add, by brute force I guess, the Gilded Rose Item
class and GildedRose
class. Item first, it’s tiny:
class Item
attr_accessor :name, :sell_in, :quality
def initialize(name, sell_in, quality)
@name = name
@sell_in = sell_in
@quality = quality
end
def to_s()
"#{@name}, #{@sell_in}, #{@quality}"
end
end
I’ll paste that into a new tab named Item, then edit it into submission:
Item = class()
function Item:init(x)
-- you can accept and set parameters here
self.x = x
end
function Item:draw()
-- Codea does not automatically call this method
end
function Item:touched(touch)
-- Codea does not automatically call this method
end
--[[
class Item
attr_accessor :name, :sell_in, :quality
def initialize(name, sell_in, quality)
@name = name
@sell_in = sell_in
@quality = quality
end
def to_s()
"#{@name}, #{@sell_in}, #{@quality}"
end
end
]]--
I just stuffed the Ruby code into a block comment at the end of the tab. I left the default draw and touched functions to show you how it starts out, but my current plan is not to include them, as you’ll see. The to_s
in Ruby is called tostring
in Lua, I believe. So …
The “unit test” provided looks like this in Ruby:
def test_foo
items = [Item.new("foo", 0, 0)]
GildedRose.new(items).update_quality()
assert_equal items[0].name, "foo"
end
In Lua, since I don’t even have GildedRose class yet, I’ll do this:
_:test("Item name", function()
local item = Item("bar", 10, 20)
_:expect(item.name).is("foo")
end)
As expected, this fails and I fix the code to instantiate a product named foo, as intended. Miraculously, the tests run correctly!
Now for the harder part, creating the GildedRose class:
GildedRose = class()
function GildedRose:init(x)
end
--[[
class GildedRose
def initialize(items)
@items = items
end
def update_quality()
@items.each do |item|
if item.name != "Aged Brie" and item.name != "Backstage passes to a TAFKAL80ETC concert"
if item.quality > 0
if item.name != "Sulfuras, Hand of Ragnaros"
item.quality = item.quality - 1
end
end
else
if item.quality < 50
item.quality = item.quality + 1
if item.name == "Backstage passes to a TAFKAL80ETC concert"
if item.sell_in < 11
if item.quality < 50
item.quality = item.quality + 1
end
end
if item.sell_in < 6
if item.quality < 50
item.quality = item.quality + 1
end
end
end
end
end
if item.name != "Sulfuras, Hand of Ragnaros"
item.sell_in = item.sell_in - 1
end
if item.sell_in < 0
if item.name != "Aged Brie"
if item.name != "Backstage passes to a TAFKAL80ETC concert"
if item.quality > 0
if item.name != "Sulfuras, Hand of Ragnaros"
item.quality = item.quality - 1
end
end
else # if item.name == "Backstage passes ..."
item.quality = item.quality - item.quality # approx zero
end
else # if item.name == "Aged Brie"
if item.quality < 50
item.quality = item.quality + 1
end
end
end
end
end
end
]]--
I guess I could just paste this whole thing into a method template and edit until it gets quiet. I’ll try that. It might even be easy.
Yeah, right. Anyway here’s a snap of the error on the first line:
I think what I’ll do is put in block-comment brackets near top and bottom, and move them inward as I edit. That might make things a bit more orderly. We’ll see.
Well, maybe not. I am going to quickly get into those nested ifs … unless maybe I worked from the inside out? I’m going to try that.
Don’t like that either, but the conversions have been simple. I’ll just try it line by line. That really only takes a few minutes. All I really had to do was convert !=
to ~=
and add then
to all the if statements. It looks like this now, and should compile. I’ll put in the much more extensive unit test now:
def test_foo
items = [Item.new("foo", 0, 0)]
GildedRose.new(items).update_quality()
assert_equal items[0].name, "foo"
end
becomes:
_:test("Item update", function()
local items = {Item("foo",0,0)}
GildedRose(items):update_quality()
_:expect(items[1].name).is("foo")
end)
And the test runs:
The Text Test
Now we come to the somewhat more challenging text test, expressed in Ruby like this:
puts "OMGHAI!"
items = [
Item.new(name="+5 Dexterity Vest", sell_in=10, quality=20),
Item.new(name="Aged Brie", sell_in=2, quality=0),
Item.new(name="Elixir of the Mongoose", sell_in=5, quality=7),
Item.new(name="Sulfuras, Hand of Ragnaros", sell_in=0, quality=80),
Item.new(name="Sulfuras, Hand of Ragnaros", sell_in=-1, quality=80),
Item.new(name="Backstage passes to a TAFKAL80ETC concert", sell_in=15, quality=20),
Item.new(name="Backstage passes to a TAFKAL80ETC concert", sell_in=10, quality=49),
Item.new(name="Backstage passes to a TAFKAL80ETC concert", sell_in=5, quality=49),
# This Conjured item does not work properly yet
Item.new(name="Conjured Mana Cake", sell_in=3, quality=6), # <-- :O
]
days = 2
if ARGV.size > 0
days = ARGV[0].to_i + 1
end
gilded_rose = GildedRose.new items
(0...days).each do |day|
puts "-------- day #{day} --------"
puts "name, sellIn, quality"
items.each do |item|
puts item
end
puts ""
gilded_rose.update_quality
end
This beauty just prints a report showing the items before update and after update. It’s rigged as it stands to run standalone and print to the console. We don’t have quite that luxury with Codea. I think what I’ll do is add this code, lightly edited, as a class, and control it with another button like the one the testing framework puts up.
I’ll just make a TextTest tab and do it brute force again.
TextTestGildedRose = class()
function TextTestGildedRose:init()
end
function TextTestGildedRose:execute()
print "OMGHAI!"
local items = {
Item{name="+5 Dexterity Vest", sell_in=10, quality=20},
Item{name="Aged Brie", sell_in=2, quality=0},
Item{name="Elixir of the Mongoose", sell_in=5, quality=7},
Item{name="Sulfuras, Hand of Ragnaros", sell_in=0, quality=80},
Item{name="Sulfuras, Hand of Ragnaros", sell_in=-1, quality=80},
Item{name="Backstage passes to a TAFKAL80ETC concert", sell_in=15, quality=20},
Item{name="Backstage passes to a TAFKAL80ETC concert", sell_in=10, quality=49},
Item{name="Backstage passes to a TAFKAL80ETC concert", sell_in=5, quality=49},
-- This Conjured item does not work properly yet
Item{name="Conjured Mana Cake", sell_in=3, quality=6}
}
local days = 2
gilded_rose = GildedRose(items)
for day = 1,days do
print( "-------- day #{day} --------")
print("name, sellIn, quality")
for i, item in pairs(items) do
print(item)
end
print ""
gilded_rose:update_quality()
end
end
parameter.action("TextTest", function()
TextTestGildedRose():execute()
end)
This compiles and gives us the button as expected. Pressing the button delivers this result:
We see two issues. First, the items don’t know how to print. This, unfortunately, is because Codea print
doesn’t call tostring
on a table even if it is implemented, and a class instance in Lua is just a table. So we’re going to have to fix that.
Second and more interesting is that message about comparing a number with nil at line 10, which is:
if item.quality > 0 then
So it looks like our items aren’t initialized correctly. I’m not entirely surprised, because that trick of initializing with a table, as shown below, is new to me:
Item{name="+5 Dexterity Vest", sell_in=10, quality=20},
I think I’ll unit test that to see what’s up:
_:test("Item table init", function()
local item = Item{name="foo", sell_in=3, quality=10}
_:expect(item.name).is("foo")
_:expect(item.quality).is(10)
end)
Yes. The Lua reference lied to me, or I didn’t read far enough. The item created in our test has a table for its name
member variable.
Ah. My bad. That feature isn’t built in to Lua: I thought it was. You have to implement that syntax if you want to handle it. For my sins I’ll make it work:
function Item:init(name, sell_in, quality)
if type(name) == "table" then
local tab = name
self.name = tab.name
self.sell_in = tab.sell_in
self.quality = tab.quality
else
self.name = name
self.sell_in = sell_in
self.quality = quality
end
end
That makes my new unit test pass (and the old ones too). Should be time to run text test again. We don’t get the error any more but of course our records still don’t print. We’ll implement print on Item and use that:
function Item:print()
print(self:tostring())
end
function Item:tostring()
return "-- "..self.name..", "..self.sell_in..", "..self.quality
end
Note that I had forgotten the self.
in tostring
. Now the text test runs and dumps truly ugly output to the console. I notice that the print of day doesn’t work as written, so I’ll update that:
gilded_rose = GildedRose(items)
for day = 1,days do
print( "-------- day "..day.." --------")
print("name, sellIn, quality")
...
The result:
It turns out that you can scroll that horrid little console and eye-check the results, and they’re as expected.
The Spec
I think all we lack now is the requirements spec. I can copy that and paste it into a tab named Requirement. Codea will try to compile it, so I’ll have to paste it in as a block comment:
--[[
======================================
Gilded Rose Requirements Specification
======================================
Hi and welcome to team Gilded Rose. As you know, we are a small inn with a prime location in a
prominent city ran by a friendly innkeeper named Allison. We also buy and sell only the finest goods.
Unfortunately, our goods are constantly degrading in quality as they approach their sell by date. We
have a system in place that updates our inventory for us. It was developed by a no-nonsense type named
Leeroy, who has moved on to new adventures. Your task is to add the new feature to our system so that
we can begin selling a new category of items. First an introduction to our system:
- All items have a SellIn value which denotes the number of days we have to sell the item
- All items have a Quality value which denotes how valuable the item is
- At the end of each day our system lowers both values for every item
Pretty simple, right? Well this is where it gets interesting:
- Once the sell by date has passed, Quality degrades twice as fast
- The Quality of an item is never negative
- "Aged Brie" actually increases in Quality the older it gets
- The Quality of an item is never more than 50
- "Sulfuras", being a legendary item, never has to be sold or decreases in Quality
- "Backstage passes", like aged brie, increases in Quality as its SellIn value approaches;
Quality increases by 2 when there are 10 days or less and by 3 when there are 5 days or less but
Quality drops to 0 after the concert
We have recently signed a supplier of conjured items. This requires an update to our system:
- "Conjured" items degrade in Quality twice as fast as normal items
Feel free to make any changes to the UpdateQuality method and add any new code as long as everything
still works correctly. However, do not alter the Item class or Items property as those belong to the
goblin in the corner who will insta-rage and one-shot you as he doesn't believe in shared code
ownership (you can make the UpdateQuality method and Items property static if you like, we'll cover
for you).
Just for clarification, an item can never have its Quality increase above 50, however "Sulfuras" is a
legendary item and as such its Quality is 80 and it never alters.
]]--
So that’s easy enough. Just to give folks a hint about the use of tabs, I’ll change the main program, which has no purpose so far anyway, to display the spec on the main screen:
--- GR1
-- Gilded Rose 1
-- Ron Jeffries 20080505
function setup()
gr_rq = readProjectTab("Requirements")
end
function draw()
fill(255)
textMode(CENTER)
text(gr_rq, WIDTH/2, HEIGHT/2)
end
Summing Up
Well. This was so boring that I hope you read it rapidly if at all. But I did want to provide a Codea Lua version of Gilded Rose, so here it is.
I learned a few things, including a “clever” way to pass named arguments to a function, and that converting Ruby to Lua is nearly trivial. Oh, and I learned that when you display a number of text lines in CORNER mode, the block origin is at the last line of the text, not the first. That seems obvious now but at the time it surprised me for a moment.
Here’s a listing of all the code, for the record. I’ve not put it on GitHub, as that is tricky from the iPad, but I’ll see about doing it and update this if I do. Oh, and here’s a link to a zip file of the project.
-- GR1
-- Gilded Rose 1
-- Ron Jeffries 20080505
function setup()
gr_rq = readProjectTab("Requirements")
end
function draw()
fill(255)
textMode(CENTER)
text(gr_rq, WIDTH/2, HEIGHT/2)
end
-- TestGR
-- CodeaUnit tests for Gilded Rose
-- Requires CodeaUnit as a dependency
-- RJ 20200505
function testGildedRose()
CodeaUnit.detailed = true
_:describe("Gilded Rose Tests", function()
_:before(function()
end)
_:after(function()
end)
_:test("Hookup", function()
_:expect( 2+1).is(3)
end)
_:test("Item name", function()
local item = Item("foo", 10, 20)
_:expect(item.name).is("foo")
end)
_:test("Item update", function()
local items = {Item("foo",0,0)}
GildedRose(items):update_quality()
_:expect(items[1].name).is("foo")
end)
_:test("Item table init", function()
local item = Item{name="foo", sell_in=3, quality=10}
_:expect(item.name).is("foo")
_:expect(item.quality).is(10)
end)
end)
end
function testTab(name,quality)
print("name=", name)
print("quality=", quality)
end
-- Item
-- Gilded Rose Item
-- RJ 20200505
Item = class()
function Item:init(name, sell_in, quality)
if type(name) == "table" then
local tab = name
self.name = tab.name
self.sell_in = tab.sell_in
self.quality = tab.quality
else
self.name = name
self.sell_in = sell_in
self.quality = quality
end
end
function Item:print()
print(self:tostring())
end
function Item:tostring()
return "-- "..self.name..", "..self.sell_in..", "..self.quality
end
-- GildedRose
-- application code for Gilded Rose
-- RJ 20200505
GildedRose = class()
function GildedRose:init(items)
self.items = items
end
function GildedRose:update_quality()
for i, item in pairs(self.items) do
if item.name ~= "Aged Brie" and item.name ~= "Backstage passes to a TAFKAL80ETC concert" then
if item.quality > 0 then
if item.name ~= "Sulfuras, Hand of Ragnaros" then
item.quality = item.quality - 1
end
end
else
if item.quality < 50 then
item.quality = item.quality + 1
if item.name == "Backstage passes to a TAFKAL80ETC concert" then
if item.sell_in < 11 then
if item.quality < 50 then
item.quality = item.quality + 1
end
end
if item.sell_in < 6 then
if item.quality < 50 then
item.quality = item.quality + 1
end
end
end
end
end
if item.name ~= "Sulfuras, Hand of Ragnaros" then
item.sell_in = item.sell_in - 1
end
if item.sell_in < 0 then
if item.name ~= "Aged Brie" then
if item.name ~= "Backstage passes to a TAFKAL80ETC concert" then
if item.quality > 0 then
if item.name ~= "Sulfuras, Hand of Ragnaros" then
item.quality = item.quality - 1
end
end
else
item.quality = item.quality - item.quality
end
else
if item.quality < 50 then
item.quality = item.quality + 1
end
end
end
end
end
--TextTestGildedRose
-- RJ 20200505
TextTestGildedRose = class()
function TextTestGildedRose:init()
end
function TextTestGildedRose:execute()
print "OMGHAI!"
local items = {
Item{name="+5 Dexterity Vest", sell_in=10, quality=20},
Item{name="Aged Brie", sell_in=2, quality=0},
Item{name="Elixir of the Mongoose", sell_in=5, quality=7},
Item{name="Sulfuras, Hand of Ragnaros", sell_in=0, quality=80},
Item{name="Sulfuras, Hand of Ragnaros", sell_in=-1, quality=80},
Item{name="Backstage passes to a TAFKAL80ETC concert", sell_in=15, quality=20},
Item{name="Backstage passes to a TAFKAL80ETC concert", sell_in=10, quality=49},
Item{name="Backstage passes to a TAFKAL80ETC concert", sell_in=5, quality=49},
-- This Conjured item does not work properly yet
Item{name="Conjured Mana Cake", sell_in=3, quality=6}
}
local days = 2
gilded_rose = GildedRose(items)
for day = 1,days do
print( "-------- day "..day.." --------")
print("name, sellIn, quality")
for i, item in pairs(items) do
item:print()
end
print ""
gilded_rose:update_quality()
end
end
parameter.action("TextTest", function()
TextTestGildedRose():execute()
end)
--[[
======================================
Gilded Rose Requirements Specification
======================================
Hi and welcome to team Gilded Rose. As you know, we are a small inn with a prime location in a
prominent city ran by a friendly innkeeper named Allison. We also buy and sell only the finest goods.
Unfortunately, our goods are constantly degrading in quality as they approach their sell by date. We
have a system in place that updates our inventory for us. It was developed by a no-nonsense type named
Leeroy, who has moved on to new adventures. Your task is to add the new feature to our system so that
we can begin selling a new category of items. First an introduction to our system:
- All items have a SellIn value which denotes the number of days we have to sell the item
- All items have a Quality value which denotes how valuable the item is
- At the end of each day our system lowers both values for every item
Pretty simple, right? Well this is where it gets interesting:
- Once the sell by date has passed, Quality degrades twice as fast
- The Quality of an item is never negative
- "Aged Brie" actually increases in Quality the older it gets
- The Quality of an item is never more than 50
- "Sulfuras", being a legendary item, never has to be sold or decreases in Quality
- "Backstage passes", like aged brie, increases in Quality as its SellIn value approaches;
Quality increases by 2 when there are 10 days or less and by 3 when there are 5 days or less but
Quality drops to 0 after the concert
We have recently signed a supplier of conjured items. This requires an update to our system:
- "Conjured" items degrade in Quality twice as fast as normal items
Feel free to make any changes to the UpdateQuality method and add any new code as long as everything
still works correctly. However, do not alter the Item class or Items property as those belong to the
goblin in the corner who will insta-rage and one-shot you as he doesn't believe in shared code
ownership (you can make the UpdateQuality method and Items property static if you like, we'll cover
for you).
Just for clarification, an item can never have its Quality increase above 50, however "Sulfuras" is a
legendary item and as such its Quality is 80 and it never alters.
]]--
Here, also, is the CodeaUnit tab, written by “jakesankey” on the Codea forum.
-- CodeaUnit
-- by "jakesankey" on Codea Forum. Thanks!
-- per RJ 20200505
CodeaUnit = class()
function CodeaUnit:describe(feature, allTests)
self.tests = 0
self.ignored = 0
self.failures = 0
self._before = function()
end
self._after = function()
end
print(string.format("Feature: %s", feature))
allTests()
local passed = self.tests - self.failures - self.ignored
local summary = string.format("%d Passed, %d Ignored, %d Failed", passed, self.ignored, self.failures)
print(summary)
end
function CodeaUnit:before(setup)
self._before = setup
end
function CodeaUnit:after(teardown)
self._after = teardown
end
function CodeaUnit:ignore(description, scenario)
self.description = tostring(description or "")
self.tests = self.tests + 1
self.ignored = self.ignored + 1
if CodeaUnit.detailed then
print(string.format("%d: %s -- Ignored", self.tests, self.description))
end
end
function CodeaUnit:test(description, scenario)
self.description = tostring(description or "")
self.tests = self.tests + 1
self._before()
local status, err = pcall(scenario)
if err then
self.failures = self.failures + 1
print(string.format("%d: %s -- %s", self.tests, self.description, err))
end
self._after()
end
function CodeaUnit:expect(conditional)
local message = string.format("%d: %s", (self.tests or 1), self.description)
local passed = function()
if CodeaUnit.detailed then
print(string.format("%s -- OK", message))
end
end
local failed = function()
self.failures = self.failures + 1
local actual = tostring(conditional)
local expected = tostring(self.expected)
print(string.format("%s -- Actual: %s, Expected: %s", message, actual, expected))
end
local notify = function(result)
if result then
passed()
else
failed()
end
end
local is = function(expected)
self.expected = expected
notify(conditional == expected)
end
local isnt = function(expected)
self.expected = expected
notify(conditional ~= expected)
end
local has = function(expected)
self.expected = expected
local found = false
for i,v in pairs(conditional) do
if v == expected then
found = true
end
end
notify(found)
end
local hasnt = function(expected)
self.expected = expected
local missing = true
for i,v in pairs(conditional) do
if v == expected then
missing = false
end
end
notify(missing)
end
local throws = function(expected)
self.expected = expected
local status, error = pcall(conditional)
if not error then
conditional = "nothing thrown"
notify(false)
else
notify(string.find(error, expected, 1, true))
end
end
return {
is = is,
isnt = isnt,
has = has,
hasnt = hasnt,
throws = throws
}
end
CodeaUnit.execute = function()
for i,v in pairs(listProjectTabs()) do
local source = readProjectTab(v)
for match in string.gmatch(source, "function%s-(test.-%(%))") do
print("loading", match)
loadstring(match)()
end
end
end
CodeaUnit.detailed = true
_ = CodeaUnit()
parameter.action("CodeaUnit Runner", function()
CodeaUnit.execute()
end)