Part 1 showed a rather trivial example of executing a Lua script. This time we're going to do something more interesting, the standard guess a number game, but instead of asking the user for input, a function is called to find the random number.

Basic implementation

Sources can be viewed here, the important functions follow.

check_match

This function is called by strategies to guess whether they should try higher or lower. It counts up how many times it is called for statistics.

int check_match(int guess){
    call_count += 1;
    if (guess > target) {
        return 1;
    } else if (guess < target) {
        return -1;
    } else {
        return 0;
    }
}

strategy_iterate

This is a strategy function, it gets given the check_match function and the range of values the random number is in.

int strategy_iterate(int (*check)(int guess), int lower, int upper){
    for (int i = lower; i <= upper; ++i){
        if (check(i) == 0){
            return i;
        }
    }
    return 0;
}

process_strategy

This takes arguments that have been processed on the command line, generates a random number, then calls the strategy, and prints stats on how often check_match was called.

int process_strategy(int min, int max){
    int guess;
    target = rand_range(min, max);

    guess = strategy_iterate(check_match, min, max);
    if (guess != target){
        fprintf(stderr, "Incorrect guess, got %d, wanted %d\n",
                guess, target);
        return 1;
    }

    printf("Guessed %d in %d attempts\n", target, call_count);

    return 0;
}

Strategy via Lua script

As is the point of this article, we're going to embed lua in this program.

The main function now handles -f strategy.lua parameter, which is a filename of a lua script to run instead of the strategy_iterate function.

I won't show the diff, because it's boring, but you can see it here

process_strategy changes

It now gets the path to the lua script that should be used as the strategy, and creates a lua state.

-int process_strategy(int min, int max){
+int process_strategy(char const *strategy, int min, int max){
    int guess;
+   int exit = 0;
+   lua_State *L = luaL_newstate();
    target = rand_range(min, max);

There's also a new exit code variable, since this happens at the end to clean up the created state. Yes gotos are involved, if they're good enough for Linux they're good enough for you.

    printf("Guessed %d in %d attempts\n", target, call_count);
-   return 0;
+cleanup:
+   lua_close(L);
+   return exit;
 }

Calling the strategy has become significantly more complicated.

-   guess = strategy_iterate(check_match, min, max);
+   if (luaL_loadfile(L, strategy)){
+       char const *errmsg = lua_tostring(L, 1);
+       fprintf(stderr, "Failed to load strategy file %s: %s\n",
+               strategy, errmsg);
+       exit = 1;
+       goto cleanup;
+   }

Instead of using luaL_dofile, I'm using luaL_loadfile. dofile loads and executes a file in one function. For my use-case I need to pass parameters in like it were a function. luaL_loadfile will turn a file into a function to be called later.

Arguably I could put the values I want into the Lua global environment, but I don't like global state, and it's cool that you can treat a file like a function in lua.

+   lua_pushcfunction(L, check_match);
+   lua_pushinteger(L, min);
+   lua_pushinteger(L, max);

This adds the parameters to the stack, it gets the function for checking its guess, and the range of values the target is in.

lua_pushcfunction makes our slightly modified check_match function into something that can be called in Lua.

Anyway, we call our function with lua_pcall, the 3 is the number of parameters on the stack it's called with, 1 is the number of expected parameters to be returned.

It returns nonzero if the function call fails, in which case it leaves an error message on the stack.

+   if (lua_pcall(L, 3, 1, 0)){
+       char const *errmsg = lua_tostring(L, 1);
+       fprintf(stderr, "Failed to execute strategy file %s: %s\n",
+               strategy, errmsg);
+       exit = 1;
+       goto cleanup;
+   }

If calling the function works then it returns the guess on the stack, here it's checked, at which point it's back to where it used to be.

+   guess = lua_tointeger(L, 1);
    if (guess != target){
        fprintf(stderr, "Incorrect guess, got %d, wanted %d\n",
                guess, target);
-       return 1;
+       exit = 1;
+       goto cleanup;
    }

check_match changes

check_match is functionally the same, except it has changed calling convention to that of lua functions, where the parameters are part of the lua_State object and the return value is the number of values returned in the lua_State.

-int check_match(int guess){
+int check_match(lua_State *L){
+   int guess = lua_tointeger(L, 1);
    call_count += 1;
    if (guess > target) {
-       return 1;
+   lua_pushinteger(L, 1);
    } else if (guess < target) {
-       return -1;
+       lua_pushinteger(L, -1);
    } else {
-       return 0;
+       lua_pushinteger(L, 0);
    }
+   return 1;
 }

strategy_iterate changes

The strategy_iterate function is gone, instead it's replaced by a lua script.

local check, lower, upper = ...

for i = lower, upper do
    if check(i) == 0 then
        return i
    end
end

return 0

local a, b, c = ... is the syntax for unpacking a tuple. When a file is used like a function, you can't name the parameters, but it's functionally similar to having the parameters declared like function(...).

Now running ./higher-lower -f higher-lower.lua is functionally equivalent to before, but how the numbers are guessed isn't hard coded, so we can start experimenting.

Good bye globals and good riddance!

Before we actually do that, I want to get rid of the global variables used to avoid passing around the target value and the call count.

The full changes can be viewed here, but the important parts are how the lua function is created, and how it's called.

Here, instead of creating it just as a function, it's created as a "closure". If you're not familiar with function closiures, they're a useful way to create stateful functions.

In this case we're adding the target and a reference to the call count to the state of the function in what lua calls upvalues.

-   lua_pushcfunction(L, check_match);
+   lua_pushinteger(L, target);
+   lua_pushlightuserdata(L, &call_count);
+   lua_pushcclosure(L, check_match, 2);

As you can see, the header of the check_match function has changed, the target is no longer a global variable, it's extracted from the lua_State and put on the C function's stack, likewise the call count is extracted, and later incremented.

 int check_match(lua_State *L){
+   int target = lua_tointeger(L, lua_upvalueindex(1));
+   int *call_count = lua_touserdata(L, lua_upvalueindex(2));
    int guess = lua_tointeger(L, 1);
-   call_count += 1;
+   (*call_count) += 1;

More interesting strategies

There's only one more change needed before interesting strategies can be tried; previously the environment the strategy is called in was completely bare. To fix this call luaL_openlibs, you can see where in the changelog.

interactive.lua

This script asks the user for the value to guess, bringing it more into line with the standard guessing game.

local check, lower, upper = ...

while true do
    print(("Guess a number between %d and %d"):format(lower, upper))
    local guess = io.read("*n")
    local result = check(guess)
    if result == 0 then
        return guess
    end
    if result < 0 then
        print("Too low")
        lower = guess
    elseif result > 0 then
        print("Too high")
        upper = guess
    end
end

This can be easily made into a binary search, allowing the correct number to be guessed in ceil(log2(upper - lower)) attempts.

It's also possible to guess the correct value without ever calling check, the answers are in the previously linked commit.

Links

Source code for this example can be found at git://git.gitano.org.uk/personal/richardmaw/lua/higher-lower.git