|||
  O o  Electro
   -

Example

What follows is a complete example of a small Electro application. It implements the Fifteen Puzzle, the ubiquitous sliding-tile game where an image is cut into 16 squares, one tile is removed, and the remaining tiles are scrambled. The object is to slide the tiles back into place to restore the image.

Each important chunk of code is described in turn, and the complete, commented source code is presented below for copying and pasting if necessary.

image_file = "venus.png"

camera  = nil
numbers = { }
sprites = { }

curr_i = 4
curr_j = 4
time   = 0

random  = false

These are the global variables. The image filename at the top allows the image to be changed easily. The camera and sprites variables hold the Electro objects that make up the scene graph. The graph consists of a single orthogonal camera with 16 sprite objects as children. This hierarchy is constructed at startup, described below.

The numbers variable will be initialized to a 4x4 table of tables that indicate which tile appears at each row and column. Each element numbers[i][j] will give an index into the sprites table. By moving indices around in the numbers table, we move tiles around in the puzzle.

The curr_i and curr_j globals track the current location of the blank spot. The time variable allows the total amount of time passed to be tracked by the do_timer function. The random variable indicates whether the do_timer function is randomizing the puzzle.

function solved()
    local k = 1

    for i = 1, 4 do
        for j = 1, 4 do
            if numbers[i][j] == k then
                k = k + 1
            else
                return false
            end
        end
    end
    return true
end

The solved function lets us know when the puzzle is complete. When solved, the tile numbers will read in order from left to right and top to bottom. This function scans the rows and columns of the numbers table checking for that property and returning a boolean indicating the result.

function move_sprite(sprite, i, j)
    E.set_entity_position(sprite, view_x + view_w * (j - 1) / 4 + view_w / 8,
                                  view_y + view_h * (4 - i) / 4 + view_h / 8, 0)
end

This is an important bit of Electro code. Given a puzzle row i and column j, the move_sprite function positions the tile object sprite at the correct location on screen. This function will be called any time a tile moves. To do its job, it must know the bounds of the display and do some trivial (but annoying) math to map puzzle row and column onto window x and y.

function move_piece(di, dj)
    local next_i = curr_i + di
    local next_j = curr_j + dj

    if 1 <= next_i and next_i <= 4 and
       1 <= next_j and next_j <= 4 then

        temp                    = numbers[curr_i][curr_j]
        numbers[curr_i][curr_j] = numbers[next_i][next_j]
        numbers[next_i][next_j] = temp

        move_sprite(sprites[numbers[curr_i][curr_j]], curr_i, curr_j)
        move_sprite(sprites[numbers[next_i][next_j]], next_i, next_j)

        curr_i = next_i
        curr_j = next_j

        E.set_entity_flags(sprites[16], E.entity_flag_hidden, not solved())

        return true
    else
        return false
    end
end

The move_piece function is the meat of the fifteen puzzle. The arguments di and dj indicate the row and column change of the blank space respectively. The function begins by confirming that the move is valid—that the blank space would not be moving off the grid. If valid, the contents of the blank (in reality, the hidden 16th piece of the puzzle) are swapped with the contents of the destination space. The two sprites in question are repositioned, and the puzzle is checked for completeness. The hidden flag of the 16th piece is set to the opposite of the solved stutus, thus the 16th piece is displayed when the puzzle is solved, but not otherwise.

The return value indicates whether the requested move is valid. This is used when generating random moves...

function move_random()
    while true do
        local d = math.random(-1, 1)

        if math.random(0, 1) == 0 then
            if move_piece(d, 0) then
                return
            end
        else
            if move_piece(0, d) then
                return
            end
        end
    end
end

Random moves are useful for scrambling the puzzle during initialization, and for running the app in a non-interactive screensaver mode. If we just pick a tile motion at random, there is no guarantee that it will be valid, so we loop until move_piece returns true to indicate success.

function do_timer(dt)
    time = time + dt

    if time > 0.25 then
        move_random()
        time = 0
        return true
    end

    return false
end

The do_timer callback is where non-interactive random motions are generated. It tracks the amount of time that has passed, and triggers a move after a quarter of a second. It returns true to indicate that a move has occurred and the screen needs to be redrawn.

function do_keyboard(k, s)
    if s then
        if k == 276 then move_piece( 0,  1) end
        if k == 275 then move_piece( 0, -1) end
        if k == 273 then move_piece( 1,  0) end
        if k == 274 then move_piece(-1,  0) end

        if k == 32 then
            random = not random
            E.enable_timer(random)
        end
    end
    return true
end

The do_keyboard callback is where interactive motions are generated. Arrow-key-presses trigger moves. The space bar toggles the do_timer callback, in effect putting the puzzle into non-interactive mode. For simplicity, all keypresses trigger a screen update. This is unnecessary, but brief.

function do_start()

    view_x, view_y, view_w, view_h = E.get_display_union()

    camera = E.create_camera(E.camera_type_orthogonal)
    image  = E.create_image(image_file)
    brush  = E.create_brush()

    E.set_background(0, 0, 0)
    E.set_brush_image(brush, image)
    E.set_brush_flags(brush, E.brush_flag_unlit, true)

    for i = 1, 16 do
        sprites[i] = E.create_sprite(brush)

        local x0, y0, z0, x1, y1, z1 = E.get_entity_bound(sprites[i])

        E.parent_entity(sprites[i], camera)
        E.set_entity_scale(sprites[i], 0.25 * view_w / (x1 - x0),
                                       0.25 * view_h / (y1 - y0), 1.0)
    end

    numbers[1] = {  1,  2,  3,  4 }
    numbers[2] = {  5,  6,  7,  8 }
    numbers[3] = {  9, 10, 11, 12 }
    numbers[4] = { 13, 14, 15, 16 }

    for i = 1, 4 do
        for j = 1, 4 do
            local x0 = (j - 1) / 4
            local x1 = (j - 0) / 4
            local y0 = (4 - i) / 4
            local y1 = (5 - i) / 4

            move_sprite(sprites[numbers[i][j]], i, j)
            E.set_sprite_range(sprites[numbers[i][j]], x0, x1, y0, y1)
        end
    end

    for i = 1, 500 do
        move_random()
    end

    return true
end

Finally, here is the do_start function. For the fifteen puzzle, we begin by creating an orthogonal camera, which will be the root of our hierarchy. We want to display our image at full brightness, so we load it and assign it to a brush with the unlit flag. We then use this brush to create 16 child sprite objects to represent the puzzle tiles. Together, these 16 sprites should fill the display, so we must determine both the size of the display and the size of the sprite in order to scale each sprite to 1/16th of the area of the display.

Then, we create the numbers table. It is simply a 4x4 table of tables initialized to a solved configuration. We use the row and column numbers of this table to position our 16 sprites on screen. We also use each sprite's initial row and column number to specify the sprite bounds, that is, we determine what subset of the image should be mapped onto each sprite.

Finally, we initialize the puzzle to an unknown state by calling the random move generator several times. That is all there is to it.

The full source of the Fifteen Puzzle example

image_file = "venus.jpg"

view_x = 0
view_y = 0
view_w = 0
view_h = 0

camera  = nil
numbers = { }
sprites = { }

curr_i  = 4
curr_j  = 4
time    = 0

random  = false

function move_sprite(sprite, i, j)
    E.set_entity_position(sprite, view_x + view_w * (j - 1) / 4 + view_w / 8,
                                  view_y + view_h * (4 - i) / 4 + view_h / 8, 0)
end

function solved()
    local k = 1

    for i = 1, 4 do
        for j = 1, 4 do
            if numbers[i][j] == k then
                k = k + 1
            else
                return false
            end
        end
    end
    return true
end

function move_piece(di, dj)
    local next_i = curr_i + di
    local next_j = curr_j + dj

    -- Confirm that the move is valid.

    if 1 <= next_i and next_i <= 4 and
       1 <= next_j and next_j <= 4 then

        -- Swap the moving piece with the blank piece.

        temp                    = numbers[curr_i][curr_j]
        numbers[curr_i][curr_j] = numbers[next_i][next_j]
        numbers[next_i][next_j] = temp

        -- Set the new on-screen locations of the moving sprites.

        move_sprite(sprites[numbers[curr_i][curr_j]], curr_i, curr_j)
        move_sprite(sprites[numbers[next_i][next_j]], next_i, next_j)

        curr_i = next_i
        curr_j = next_j

        -- Set the visibility of the 16th piece to indicate solution.

        E.set_entity_flags(sprites[16], E.entity_flag_hidden, not solved())

        return true
    else
        return false
    end
end

function move_random()
    while true do
        local d = math.random(-1, 1)

        if math.random(0, 1) == 0 then
            if move_piece(d, 0) then
                return
            end
        else
            if move_piece(0, d) then
                return
            end
        end
    end
end

function do_keyboard(k, s)
    if s then
        -- If an arrow key has been pressed, move a puzzle piece.

        if k == 276 then move_piece( 0,  1) end
        if k == 275 then move_piece( 0, -1) end
        if k == 273 then move_piece( 1,  0) end
        if k == 274 then move_piece(-1,  0) end

        -- If autoplay has been switched, toggle the idle function.

        if k == 32 then
            random = not random
            E.enable_timer(random)
        end
    end
    return true
end

function do_timer(dt)
    time = time + dt

    if time > 0.25 then
        move_random()
        time = 0
      return true
    end

    return false
end

function do_start()

    view_x, view_y, view_w, view_h = E.get_display_union()

    camera = E.create_camera(E.camera_type_orthogonal)
    image  = E.create_image(image_file)
    brush  = E.create_brush()

    E.set_background(0, 0, 0)
    E.set_brush_image(brush, image)
    E.set_brush_flags(brush, E.brush_flag_unlit, true)

    -- Add 16 sprites and scale each to 1/16th the size of the display.

    for i = 1, 16 do
        sprites[i] = E.create_sprite(brush)

        local x0, y0, z0, x1, y1, z1 = E.get_entity_bound(sprites[i])
        local sw = x1 - x0
        local sh = y1 - y0

        E.parent_entity(sprites[i], camera)
        E.set_entity_scale(sprites[i], 0.25 * view_w / (x1 - x0),
                                       0.25 * view_h / (y1 - y0), 1.0)
    end

    -- Initialize an array that maps rows and columns onto puzzle pieces.

    numbers[1] = {  1,  2,  3,  4 }
    numbers[2] = {  5,  6,  7,  8 }
    numbers[3] = {  9, 10, 11, 12 }
    numbers[4] = { 13, 14, 15, 16 }

    -- Assign 1/16th of the image to each sprite.

    for i = 1, 4 do
        for j = 1, 4 do
            local x0 = (j - 1) / 4
            local x1 = (j - 0) / 4
            local y0 = (4 - i) / 4
            local y1 = (5 - i) / 4

            move_sprite(sprites[numbers[i][j]], i, j)
            E.set_sprite_range(sprites[numbers[i][j]], x0, x1, y0, y1)
        end
    end

    -- Scramble the tiles.

    for i = 1, 500 do
        move_random()
    end

    return true
end

do_start()

rlk (at) evl.uic.edu