Tutorial #04: Lua Card Game

We demonstrate how the card game from Tutorial #03: Card Game can be created solely with Lua.

For this tutorial it is assumed that the reader already has basic knowledge about Lua programming.

Note
You need to add the optional Lua add-on to your project in order to be able to use Lua scripts. The easiest way to configure add-ons is by using the Murl Dashboard and the command Project / Configure Project.

Resource Package

We use a copy of the resources game.murlres from tutorial #03.

Additionally we add script resources to the package and store them in a separate lua folder.

  • data/packages/game.murlres/lua/card_game_logic.lua
  • data/packages/game.murlres/lua/card_game_card_instance.lua

The script resources have to be added to the package like any other resource file.

<?xml version="1.0" ?>
<!-- Copyright 2014 Spraylight GmbH -->
<Package id="game">
    
    <!-- Animation resources -->
    <Resource id="anim_game_screen"     fileName="anim_game_screen.xml"/>
    
    <!-- Bitmap resources -->
    <Resource id="gfx_clubs"            fileName="gfx_clubs.png"/>
    <Resource id="planes_clubs"         fileName="planes_clubs.xml"/>
    <Resource id="gfx_diamonds"         fileName="gfx_diamonds.png"/>
    <Resource id="planes_diamonds"      fileName="planes_diamonds.xml"/>
    <Resource id="gfx_hearts"           fileName="gfx_hearts.png"/>
    <Resource id="planes_hearts"        fileName="planes_hearts.xml"/>
    <Resource id="gfx_misc"             fileName="gfx_misc.png"/>
    <Resource id="planes_misc"          fileName="planes_misc.xml"/>
    <Resource id="gfx_spades"           fileName="gfx_spades.png"/>
    <Resource id="planes_spades"        fileName="planes_spades.xml"/>
    
    <!-- Font resources -->
    <Resource id="arial_color_24_glyphs" fileName="fonts/arial_color_24_glyphs.murl"/>
    <Resource id="arial_color_24_map"    fileName="fonts/arial_color_24_map.png"/>
    <Resource id="arial_color_48_glyphs" fileName="fonts/arial_color_48_glyphs.murl"/>
    <Resource id="arial_color_48_map"    fileName="fonts/arial_color_48_map.png"/>
    
    <!-- Graph resources -->
    <Resource id="graph_camera"         fileName="graph_camera.xml"/>
    <Resource id="graph_game_card"      fileName="graph_game_card.xml"/>
    <Resource id="graph_game_card_suit" fileName="graph_game_card_suit.xml"/>
    <Resource id="graph_game_screen"    fileName="graph_game_screen.xml"/>
    <Resource id="graph_materials"      fileName="graph_materials.xml"/>
    <Resource id="graph_textures"       fileName="graph_textures.xml"/>
    
    <!-- Lua resources -->
    <Resource id="lua_card_game_logic"          fileName="lua/card_game_logic.lua" enableCompression="yes"/>
    <Resource id="lua_card_game_card_instance"  fileName="lua/card_game_card_instance.lua" enableCompression="yes"/>

    <!-- Graph instances -->
    <Instance graphResourceId="graph_materials"/>
    <Instance graphResourceId="graph_textures"/>
    <Instance graphResourceId="graph_camera"/>

    <!-- Script instances -->
    <Instance scriptResourceId="lua_card_game_logic"/>

</Package>

The resource lua_card_game_logic is instantiated with <Instance scriptResourceId="lua_card_game_logic"/>. This means that the script automatically will be added as a logic processer after the package has been loaded and is processed like any other logic processor.

Scene Graph

The second script resource lua_card_game_card_instance is used as a script controller of a graph node.

<?xml version="1.0" ?>
<!-- Copyright 2014 Spraylight GmbH -->
<Graph>
    
    <Namespace id="{cardName}" activeAndVisible="no"
        controller="ScriptLogicController"
        controller.scriptResourceId="game:lua_card_game_card_instance">

        <Transform id="position">
            
            <Reference targetId="/textures/{cardName}"/>
            
            <SubState>
                <TextureState textureId="/textures/tex_misc"/>
                <Transform axisX="0" axisY="1" axisZ="0" angle="180 deg">
                    <Switch id="back_switch">
                        <Reference targetId="/textures/black_back"/>
                        <Reference targetId="/textures/red_back"/>
                    </Switch>
                </Transform>
            </SubState>
            
            <Button id="button" sizeX="90" sizeY="130"/>
            
        </Transform>
        
    </Namespace>
    
</Graph>

Thus for every card graph instance also a lua_card_game_card_instance script instance is created. After a graph node has been initialized the corresponding controller script is processed on every logic tick.

Application

A pure Lua application uses a small portion of generic C++ code to initially load the first Lua script. This script has to implement the IApp class in Lua and needs to be stored in the packages folder.

  • data/packages/lua_card_game_app.lua
Note
Hint: A pure Lua application with all necessary code parts can easily be created with the Murl Dashboard by using the Create Project command and the Lua template.

The program flow is almost identical to the C++ program from tutorial #03.

-- Copyright 2014 Spraylight GmbH

-- Open Lua standard libraries
local luaAddon = Murl.Addons.Lua.Factory.GetAddon()
luaAddon:OpenLibrary("math")
luaAddon:OpenLibrary("string")
luaAddon:OpenLibrary("table")
luaAddon:OpenLibrary("utf8")

-- Parameter for Murl.IApp
local name = ...
-- print("\nLUA IApp new '" ..  name .. "'")

-- Murl IApp callbacks
local mainIApp =
{
    Configure = function (self, engineConfig, fileInterface)
        -- print("LUA IApp Configure")

        local appConfig = engineConfig:GetAppConfiguration()
        local platformConfig = engineConfig:GetPlatformConfiguration()

        engineConfig:SetProductName("LuaCardGame")
        appConfig:SetWindowTitle("LuaCardGame powered by Murl Engine")

        appConfig:SetSystemDebugInfoItems(Murl.IEnums.STATISTIC_ITEM_FRAMES_PER_SECOND)

        if (platformConfig:IsTargetClassMatching(Murl.IEnums.TARGET_CLASS_COMPUTER)) then

            if (platformConfig:IsOperatingSystemMatching(Murl.IEnums.OPERATING_SYSTEM_WINDOWS)) then
                engineConfig:SetVideoApi(Murl.IEnums.VIDEO_API_DX90)
            end

            appConfig:SetDisplaySurfaceSize(1024, 768)
            appConfig:SetLockWindowAspectEnabled(true)
            appConfig:SetFullScreenEnabled(false)

        elseif (platformConfig:IsTargetClassMatching(Murl.IEnums.TARGET_CLASS_HANDHELD)) then

            -- set landscape orientation
            appConfig:SetScreenOrientation(Murl.IEnums.SCREEN_ORIENTATION_LANDSCAPE_1)
            -- enable landscape orientations
            appConfig:SetAllowedScreenOrientations(Murl.IEnums.SCREEN_ORIENTATIONS_LANDSCAPE)
            -- enable auto rotation
            appConfig:SetAutoRotationActive(true)
            appConfig:SetOrientationActive(true)
            -- enable multi touch
            appConfig:SetMultiTouchActive(true)

        end

        engineConfig:SetDeactivatedAppRunState(Murl.IEnums.APP_RUN_STATE_PAUSED)

        return true
    end,

    IsUserConfigurationMatching = function (self, userConfigId)
        -- print("LUA IApp IsUserConfigurationMatching", userConfigId)

        return false
    end,

    RegisterCustomAddonClasses = function (self, addonRegistry)
        -- print("LUA IApp RegisterCustomAddonClasses")

        return true
    end,

    UnregisterCustomAddonClasses = function (self, addonRegistry)
        -- print("LUA IApp UnregisterCustomAddonClasses")

        return true
    end,

    Init = function (self, appState)
        -- print("LUA IApp Init")

        local loader = appState:GetLoader()
        loader:AddPackage("startup", Murl.ILoader.LOAD_MODE_STARTUP)
        loader:AddPackage("game", Murl.ILoader.LOAD_MODE_BACKGROUND)

        return true
    end,

    DeInit = function (self, appState)
        -- print("\nLUA IApp DeInit")

        return true
    end
}

return Murl.IApp.new(mainIApp)

This script is loaded and executed immediately when the program starts. The script has to return a IApp object.

  • In the first section (line 4) Lua standard libraries are loaded.
  • In the second section (line 11) the parameters are evaluated. The Lua application script gets exactly one parameter containing the name of the script resource.
  • In the third section (line 15) a Lua function table is created. These functions act as callback functions and have the same meaning as the equally named C++ IApp methods.
  • In the last section (line 92) an instance of the IApp class with the callback function table is created and returned.

Afterwards the Lua callback functions in mainIApp are executed like otherwise the equally named C++ methods.

The function Configure() (line 17) contains the same program as the C++ method in card_game_app.cpp from tutorial #03. The same applies for the function Init() (line 75) which loads the resource packages.

Lua programming syntactically slightly differs from C++ programming but almost all Murl classes are mapped to equally named Lua tables. A complete documentation of all Lua tables can be found in the download section in the MurlEngineLuaAPI archive.

Card Instance Script

Our game has 54 card instances where each can be controlled separately. To keep it simple we implement a script which can control exactly one card. This script is used as a graph node controller as shown above in the sub graph graph_game_card.xml.

-- Copyright 2014 Spraylight GmbH

-- Parameters for Logic IAppGraph
local resourceId, nodeId = ...
-- print("\nLUA IAppGraph new", resourceId, nodeId)

-- Logic IAppGraph callbacks
local logicIAppGraph =
{
    mNodeObserver = Murl.Logic.StaticFactory.CreateNodeObserver(),
    mNamespaceNode = Murl.Logic.NamespaceNode.new(),
    mTransformNode = Murl.Logic.TransformNode.new(),
    mButton = Murl.Logic.ButtonNode.new(),
    mBackSwitch = Murl.Logic.SwitchNode.new(),

    mStepableObserver = Murl.Logic.StaticFactory.CreateStepableObserver(),
    mPositionAnim = Murl.Logic.AnimationVector.new(),
    mRotationAnim = Murl.Logic.AnimationVector.new(),

    mIsShowingFront = false,

    OnPostInit = function (self, state)
        -- print("LUA IAppGraph OnInit", nodeId)

        self.mNodeObserver:Add(self.mNamespaceNode:GetReference(state:GetCurrentGraphNode()))
        if (not self.mNamespaceNode:IsValid()) then
            return false
        end

        self.mNodeObserver:Add(self.mTransformNode:GetReference(self.mNamespaceNode:FindNode("position")))

        self.mNodeObserver:Add(self.mButton:GetReference(self.mNamespaceNode:FindNode("button")))
        self.mNodeObserver:Add(self.mBackSwitch:GetReference(self.mNamespaceNode:FindNode("back_switch")))

        if (not self.mNodeObserver:AreValid()) then
            return false
        end

        self.mPositionAnim:AddKey(0.0, Murl.Math.Vector.new(Murl.Math.Vector.ZERO_POSITION), Murl.IEnums.INTERPOLATION_EASE_OUT)
        self.mPositionAnim:AddKey(1.0, Murl.Math.Vector.new(Murl.Math.Vector.ZERO_POSITION))

        self.mRotationAnim:AddKey(0.0, Murl.Math.Vector.new(), Murl.IEnums.INTERPOLATION_LINEAR)
        self.mRotationAnim:AddKey(0.5, Murl.Math.Vector.new(), Murl.IEnums.INTERPOLATION_EASE_OUT)
        self.mRotationAnim:AddKey(1.0, Murl.Math.Vector.new());

        self.mStepableObserver:Add(self.mPositionAnim:GetStepable())
        self.mStepableObserver:Add(self.mRotationAnim:GetStepable())

        self:SetBack(CARDBACKSIDE_BLACK)
        self:EnableButton(false)
        self:SetCardPosition(self.mTransformNode:GetPosition(), false, 0)
        self.mIsShowingFront = true

        return true
    end,

    OnPreDeInit = function (self, state)
        -- print("LUA IAppGraph OnDeInit", nodeId)
        local ret = true

        if (not self.mNodeObserver:RemoveAll()) then
            ret = false
        end

        return ret
    end,

    OnPreProcessTick = function (self, state)
        self.mStepableObserver:ProcessTick(state)

        if (self.mPositionAnim:IsOrWasRunning()) then
            self.mTransformNode:SetPosition(self.mPositionAnim:GetCurrentValue())
        end

        if (self.mRotationAnim:IsOrWasRunning()) then
            local currentRotation = self.mRotationAnim:GetCurrentValue()
            self.mTransformNode:SetRotation(currentRotation.x, currentRotation.y, currentRotation.z)
        end
    end,

    SetObtained = function (self, isObtained)
        self.mNamespaceNode:GetNodeInterface():SetActiveAndVisible(isObtained)
    end,

    SetSortDepth = function (self, sortDepth)
        self.mTransformNode:SetDepthOrder(sortDepth)
    end,

    SetBack = function (self, backSide)
        if (backSide == CARDBACKSIDE_RED) then
            self.mBackSwitch:SetIndex(1)
        else
            self.mBackSwitch:SetIndex(0)
        end
    end,

    SetCardPosition = function (self, position, showFront, duration)
        self.mTransformNode:SetPosition(position)
        if (showFront) then
            if (not self.mIsShowingFront) then
                if (duration > 0) then
                    self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, -Murl.Math.PI, 0, 0))
                    self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0, -Murl.Math.HALF_PI, 0, 0))
                    self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, 0, 0, 0))
                    self.mRotationAnim:ModifyKeyTime(1, duration / 2)
                    self.mRotationAnim:ModifyKeyTime(2, duration)
                    self.mRotationAnim:StartForward()
                else
                    self.mTransformNode:SetRotation(0, 0, 0)
                end
            end
        else
            if (self.mIsShowingFront) then
                if (duration > 0) then
                    self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, 0, 0, 0))
                    self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0, Murl.Math.HALF_PI, 0, 0))
                    self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, Murl.Math.PI, 0, 0))
                    self.mRotationAnim:ModifyKeyTime(1, duration / 2)
                    self.mRotationAnim:ModifyKeyTime(2, duration)
                    self.mRotationAnim:StartForward()
                else
                    self.mTransformNode:SetRotation(0, Murl.Math.PI, 0)
                end
            end
        end
        self.mIsShowingFront = showFront
    end,

    MoveCardToPosition = function (self, position, showFront, duration)
        self.mPositionAnim:SetKey(0, 0.0, self.mTransformNode:GetPosition(), Murl.IEnums.INTERPOLATION_EASE_OUT)
        self.mPositionAnim:SetKey(1, duration, position)
        self.mPositionAnim:StartForward()

        if (showFront) then
            if (mIsShowingFront) then
                self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, 0, 0, 0))
                self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0, -1.0, 0.25, 0))
                self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, 0, 0, 0))
            else
                self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, Murl.Math.PI, 0, 0))
                self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0, Murl.Math.HALF_PI, 0.5, 0))
                self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, 0, 0, 0))
            end
        else
            if (mIsShowingFront) then
                self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, 0, 0, 0))
                self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0, Murl.Math.HALF_PI, 0.5, 0))
                self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, Murl.Math.PI, 0, 0))
            else
                self.mRotationAnim:ModifyKeyValue(0, Murl.Math.Vector.new(0, Murl.Math.PI, 0, 0))
                self.mRotationAnim:ModifyKeyValue(1, Murl.Math.Vector.new(0.5, Murl.Math.PI - 0.5, 0.5, 0))
                self.mRotationAnim:ModifyKeyValue(2, Murl.Math.Vector.new(0, Murl.Math.PI, 0, 0))
            end
        end
        self.mRotationAnim:ModifyKeyTime(1, duration / 2)
        self.mRotationAnim:ModifyKeyTime(2, duration)
        self.mRotationAnim:StartForward()
        self.mIsShowingFront = showFront
    end,

    IsMoving = function (self)
        return self.mPositionAnim:IsOrWasRunning()
    end,

    EnableButton = function (self, enable)
        self.mButton:SetEnabled(enable)
    end,

    WasPressed = function (self)
        return self.mButton:WasPressed()
    end,
}

-- add this instance to the global card instances table
table.insert(mCards, logicIAppGraph)

return Murl.Logic.IAppGraph.new(logicIAppGraph)

This script is automatically created and processed when the corresponding graph node is loaded. The script should return a Logic.IAppGraph object.

  • In the first section (line 4) the Lua parameters are evaluated. The Lua controller script gets exactly two parameters. The first parameter is the name (id) of the script resource and the second parameter is the name (id) of the controller node.
  • In the second section (line 8) a lua function table is created. This functions act as callback functions and have the same meaning as the equally named C++ Logic::IAppGraph methods.
  • In the third section (line 175) the function table is added to a global table mCards to allow the logic code to access the individual card instances.
  • Finally (line 177) an instance of the Logic.IAppGraph class is created and returned

The functions and variables of the logic.IAppGraph table correspond to the methods and member variables of the card_game_card_instance.cpp from Tutorial #03.

Script Processor

The logic processor instance is instantiated directly in the package.xml as already shown above.

-- Copyright 2014 Spraylight GmbH

-- Parameters for Logic IAppProcessor
local resourceId, replication = ...
-- print("\nLUA IAppProcessor new", resourceId, replication)

-- the global table holding all card instances
mCards = {}

CARDBACKSIDE_BLACK = 0
CARDBACKSIDE_RED = 1

-- Logic IAppProcessor callbacks
local logicIAppProcessor =
{
    STATE_IDLE = 0,
    STATE_DEAL = 1,
    STATE_PLAY = 2,
    mStateMachine = 0,

    mCardsPerSuit = 13,
    mCardSizeX = 90,
    mCardSizeY = 130,

    mScreenTimeline = Murl.Logic.TimelineNode.new(),
    mGameInfoText = Murl.Logic.TextGeometryNode.new(),
    mStackButton = Murl.Logic.ButtonNode.new(),

    mRng = Murl.Util.TT800.new(),
    mCardDistribution = Murl.UInt32Array.new(),
    mCardsToDeal = Murl.SInt32Array.new(),

    mCardStack = Murl.SInt32Array.new(),
    mCardTray = Murl.SInt32Array.new(),
    mPlayfield = Murl.SInt32Array.new(),

    mGameStartTimeout = Murl.Logic.Timeframe.new(),
    mGameEndTimeout = Murl.Logic.Timeframe.new(),

    mDealCount = 0,
    mNumberOfGamesPlayed = 0,

    OnInit = function (self, state)
        -- print("\nLUA IAppProcessor OnInit", resourceId)

        state:GetLoader():UnloadPackage("startup")

        local root = state:GetGraphRoot()
        local processor = state:GetCurrentProcessor()

        processor:AddGraphNode(self.mScreenTimeline:GetReference(root, "/game_screen/screen_timeline"))
        processor:AddGraphNode(self.mGameInfoText:GetReference(root, "/game_screen/info_text"))
        processor:AddGraphNode(self.mStackButton:GetReference(root, "/game_screen/stack_button"))

        if (not processor:AreGraphNodesValid()) then
            return false
        end

        self.mScreenTimeline:Start(0.0, 0.5)

        processor:AddStepable(self.mGameStartTimeout:GetStepable())
        processor:AddStepable(self.mGameEndTimeout:GetStepable())

        return true
    end,

    OnDeInit = function (self, state)
        -- print("\nLUA IAppProcessor OnDeInit", resourceId)
        local ret = true

        return ret
    end,

    OnProcessTick = function (self, state)
        if (self.mScreenTimeline:WasRunning()) then
            self:EnterDeal(state)
        end

        local deviceHandler = state:GetDeviceHandler()
        if (deviceHandler:WasRawButtonPressed(Murl.RAWBUTTON_BACK)) then
            deviceHandler:TerminateApp()
        end

        --print("State", self.mStateMachine)
        if (self.mStateMachine == self.STATE_DEAL) then
            self:ProcessTickDeal(state)
        elseif (self.mStateMachine == self.STATE_PLAY) then
            self:ProcessTickPlay(state)
        end
    end,

    OnRunStateChanged = function (self, state, currentState, previousState)
        -- print("\nLUA IAppProcessor OnRunStateChanged", resourceId)

        if (self.mGameStartTimeout:IsOrWasRunning()) then
            return
        end

        if (currentState == Murl.IEnums.APP_RUN_STATE_PAUSED) then
            self.mGameInfoText:SetText("- Paused -")
            if (self.mStateMachine == self.STATE_PLAY) then
                self:UpdatePlayfield(0.0, true)
            end
        elseif (currentState == Murl.IEnums.APP_RUN_STATE_RUNNING) then
            self.mGameInfoText:SetText("- Play -")
            if (self.mStateMachine == self.STATE_PLAY) then
                self:UpdatePlayfield(0.3, false)
            end
        end
    end,

    EnterDeal = function (self, state)
        -- print("\nLUA IAppProcessor EnterDeal", resourceId)
        self.mStateMachine = self.STATE_DEAL
        self.mNumberOfGamesPlayed = self.mNumberOfGamesPlayed + 1

        self:ShuffleCards(self.mNumberOfGamesPlayed % 3)
        self.mDealCount = 0
        self.mGameStartTimeout:Start(1.0)
        self.mGameInfoText:SetText("")
    end,

    ProcessTickDeal = function (self, state)
        -- print("LUA IAppProcessor ProcessTickDeal", resourceId)
        if (self.mGameStartTimeout:IsRunning()) then
            return
        end

        if (self.mCardsToDeal:GetCount() > 0) then
            local index = self.mCardsToDeal:Pop()
            local n = self.mCardStack:GetCount()
            self.mCardStack:Add(index)
            mCards[index + 1]:SetObtained(true)
            mCards[index + 1]:SetSortDepth(n + 1)
            mCards[index + 1]:SetCardPosition(self:GetStackPosition(n), false, 0)
            mCards[index + 1]:EnableButton(false)
            if ((self.mNumberOfGamesPlayed % 2) == 0) then
                mCards[index + 1]:SetBack(CARDBACKSIDE_RED)
            else
                mCards[index + 1]:SetBack(CARDBACKSIDE_BLACK)
            end
        elseif (self.mDealCount < self.mPlayfield:GetCount()) then
            local index = self.mCardStack:Pop()
            local showFront = (self.mDealCount >= 18)
            mCards[index + 1]:SetSortDepth(self.mDealCount + 1000)
            mCards[index + 1]:MoveCardToPosition(self:GetPlayfieldPosition(self.mDealCount), showFront, 0.5)
            mCards[index + 1]:EnableButton(showFront)
            self.mPlayfield[self.mDealCount] = index
            self.mGameStartTimeout:Start(0.1)
            self.mDealCount = self.mDealCount + 1
        else
            self:MoveToTray(self.mCardStack:Pop())
            self:UpdateStackButton()
            self:EnterPlay(state)
        end
    end,

    EnterPlay = function (self, state)
        -- print("\nLUA IAppProcessor EnterPlay", resourceId)
        self.mStateMachine = self.STATE_PLAY
        self.mGameInfoText:SetText("- Play -")
    end,

    ProcessTickPlay = function (self, state)
        -- print("LUA IAppProcessor ProcessTickPlay", resourceId)
        if (self.mGameEndTimeout:IsRunning()) then
            return
        end
        if (self.mGameEndTimeout:WasRunning()) then
            self:EnterDeal(state)
            return
        end

        local canAnyMoveToTray = false
        local isPlayfieldEmpty = true
        for i, index in Murl.ipairs(self.mPlayfield) do
            if (index >= 0) then
                isPlayfieldEmpty = false
                if (mCards[index + 1].mIsShowingFront) then
                    local canMoveToTray = self:CanMoveToTray(index)
                    if (canMoveToTray) then
                        canAnyMoveToTray = true
                    end
                    if (mCards[index + 1]:WasPressed()) then
                        if (canMoveToTray) then
                            self:MoveToTray(index)
                            self.mPlayfield[i] = -1
                            self:UpdatePlayfield(0.3, false)
                        end
                    end
                end
            end
        end

        if (isPlayfieldEmpty) then
            -- Pyramid complete
            self.mGameInfoText:SetText("Congratulations!")
            self.mGameEndTimeout:Start(3.0)
        elseif (self.mCardStack:GetCount() > 0) then
            if (self.mStackButton:WasPressed()) then
                self:MoveToTray(self.mCardStack:Pop())
                self:UpdateStackButton()
            end
        else
            if (not canAnyMoveToTray) then
                -- No more moves
                self.mGameInfoText:SetText("Game Over")
                self.mGameEndTimeout:Start(3.0)
            end
        end
    end,

    UpdateStackButton = function (self)
        local additionalSizeX = self.mCardStack:GetCount() * 4
        if (additionalSizeX > 0) then
            self.mStackButton:SetScaleFactorX(self.mCardSizeX + additionalSizeX)
            self.mStackButton:SetScaleFactorY(self.mCardSizeY + 4)
            local position = self:GetStackPosition(0)
            position.x = position.x + (additionalSizeX / 2)
            self.mStackButton:GetTransformInterface():SetPosition(position)
        end
    end,

    GetStackPosition = function (self, index)
        local position = Murl.Math.Vector.new(Murl.Math.Vector.ZERO_POSITION)
        position.x = -400 + (index * 4)
        position.y = -260
        return position
    end,

    GetTrayPosition = function (self, index)
        local position = Murl.Math.Vector.new(Murl.Math.Vector.ZERO_POSITION)
        position.x = 200 + (index * 2)
        position.y = -260
        return position
    end,

    GetPlayfieldPosition = function (self, index)
        local position = Murl.Math.Vector.new(Murl.Math.Vector.ZERO_POSITION)
        local DISTANCE_X = self.mCardSizeX + 10
        local DISTANCE_Y = self.mCardSizeY - 50

        if (index < 3) then
            position.x = (index * 3) * DISTANCE_X - (6 * DISTANCE_X / 2)
            position.y = DISTANCE_Y + DISTANCE_Y / 2
        elseif (index < 9) then
            position.x = (index - 3) * DISTANCE_X - (7 * DISTANCE_X / 2)
            position.x = position.x + (Murl.Math.Floor((index - 3) / 2) * DISTANCE_X)
            position.y = DISTANCE_Y / 2
        elseif (index < 18) then
            position.x = (index - 9) * DISTANCE_X - (8 * DISTANCE_X / 2)
            position.y = -DISTANCE_Y / 2
        else
            position.x = (index - 18) * DISTANCE_X - (9 * DISTANCE_X / 2)
            position.y = -DISTANCE_Y - DISTANCE_Y / 2
        end

        position.y = position.y + 100
        return position
    end,

    IsCardFree = function (self, index)
        -- Playfield index
        --        0           1           2
        --      3   4       5   6       7   8
        --    9  10  11  12  13  14  15  16  17
        -- 18  19  20  21  22  23  24  25  26  27

        local left = -1
        if (index < 3) then
            left = (index * 2) + 3
        elseif (index < 9) then
            left = (Murl.Math.Floor((index - 3) / 2)) + index + 6
        elseif (index < 18) then
            left = index + 9
        end

        local right = -1
        if (left >= 0) then
            right = left + 1
        end

        local free = true
        if (left >= 0 and left < self.mPlayfield:GetCount()) then
            if (self.mPlayfield[left] >= 0) then
                free = false
            end
        end
        if (right >= 0 and right < self.mPlayfield:GetCount()) then
            if (self.mPlayfield[right] >= 0) then
                free = false
            end
        end
        return free
    end,

    CanMoveToTray = function (self, index)
        local isValid = false
        local trayCard = self.mCardTray:Top()
        if (trayCard >= 4 * self.mCardsPerSuit or index >= 4 * self.mCardsPerSuit) then
            -- Joker
            isValid = true
        else
            local playCard = index % self.mCardsPerSuit
            local trayCardPlus = (trayCard + 1) % self.mCardsPerSuit
            local trayCardMinus = (trayCard + self.mCardsPerSuit - 1) % self.mCardsPerSuit
            if (playCard == trayCardPlus or playCard == trayCardMinus) then
                isValid = true
            end
        end
        return isValid
    end,

    MoveToTray = function (self, index)
        local n = self.mCardTray:GetCount()
        self.mCardTray:Add(index)
        mCards[index + 1]:SetSortDepth(n + 2000)
        mCards[index + 1]:MoveCardToPosition(self:GetTrayPosition(n), true, 0.5)
        mCards[index + 1]:EnableButton(false)
    end,

    UpdatePlayfield = function (self, duration, isPause)
        for i, index in Murl.ipairs(self.mPlayfield) do
            if (index >= 0) then
                local isFree = false
                if (not isPause) then
                    isFree = self:IsCardFree(i)
                end
                local card = mCards[index + 1]
                card:SetSortDepth(i + 1000)
                card:SetCardPosition(self:GetPlayfieldPosition(i), isFree, duration)
                if (isFree) then
                    card:EnableButton(true)
                end
            end
        end
    end,

    ShuffleCards = function (self, numberOfJokers)
        for _, card in ipairs(mCards) do
            card:SetObtained(false)
        end

        if (numberOfJokers > 2) then
            numberOfJokers = 2
        end
        self.mCardDistribution:Empty()
        self.mCardDistribution:SetCount(#mCards - 2 + numberOfJokers, 1)

        self.mCardsToDeal:Empty()
        for _ in Murl.ipairs(self.mCardDistribution) do
            local index
            index, self.mCardDistribution = self.mRng:DrawNoReplacement(self.mCardDistribution)
            self.mCardsToDeal:Add(index)
        end

        self.mCardStack:Empty()
        self.mCardTray:Empty()
        self.mPlayfield:Empty()
        self.mPlayfield:SetCount(28, -1)
    end
}

return Murl.Logic.IAppProcessor.new(logicIAppProcessor)

This script is automatically loaded and processed when the corresponding resource package is loaded. The script should return an instance of the Logic.IAppProcessor class.

  • In the first section (line 4) the parameters are evaluated. The Lua script gets exactly two parameters. The first parameter is the name of the script resource and the second parameter is the replication number.
  • In the second section (line 14) a Lua function table is created. The functions act as callback functions and have the same meaning as the equally named C++ methods of the Logic::IAppProcessor class.
  • Finally (line 364) an instance of the Logic.IAppProcessor class is created and returned.

The functions and variables of the logicIAppProcessor table correspond to the methods and member variables of the card_game_logic.cpp from Tutorial #03.

The most obvious difference between the Lua implementation and the C++ implementation is the creation and initialization of the card instances, which in the Lua implementation occur automatically when the graph nodes are created.

tut0204_lua_card_game.png
Lua Card Game output window


Copyright © 2011-2024 Spraylight GmbH.