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:

lua console

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:

lua text

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:

lua first test

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:

lua-error

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.

lua-comments

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:

lua-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:

lua-text-working

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

requirements-on-screen

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)