Dungeon Hopper

Dungeon Hopper

  • Docs

›Terminology And Basics

Getting Started

  • What is this?

Terminology And Basics

  • The lifecycle of the game
  • Chains and Chain Templates
  • Calling Conventions

Topics

  • Game Loop
  • Decorators
  • Action
  • Algorithms
  • Movs Algorithms
  • Sequence
  • Modifiers
  • Dynamic Stats
  • Retouchers
  • Tinkers
  • Status effects
  • Items
  • Weapons
  • Pools
  • World Generation
  • Mods

Chains and Chain Templates

Chains

What are chains?

The concept of a chain is essential in this project. They allow for flexible dynamic customizable algorithms, and easier splitting of code into components.

These chains are just a modified form of responsibility chains, called just 'chains' for convenience.

Example

Essentially, a chain is just a series of handler functions that operate on an event. You may want to have a chain that, for example, checks for multiple things like this:


local APPLE = 1
local POTATO = 2
local PEAR = 3


local chain = Chain()

local function checkApple(event)
    if event.actor == APPLE then
        event.propagate = false
    end
end

local function checkPotato(event)
    if event.actor == POTATO then
        event.propagate = false
    end
end

-- register the handlers.
-- their number is unlimited.
-- in this case, we used just two
chain:addHandler(checkApple)
chain:addHandler(checkPotato)

-- now, test out our chain
local event = { actor = PEAR, propagate = true }

-- do a pass through all handlers in order.
-- the second argument indicates that we should stop if event.propagate is false
chain:pass(event, Chain.checkPropagate)

print(event.propagate)
-- >> true


-- test for an apple
event = { actor = APPLE, propagate = true }

chain:pass(event, Chain.checkPropagate)

print(event.propagate)
-- >> false

The beauty of this method, though, is that the chain may be modified on the fly!

event = { actor = APPLE, propagate = true }
chain:pass(event)
-- event.propagate = false

-- remove the handler that check apple
chain:removeHandler(checkApple)

event = { actor = APPLE, propagate = true }
chain:pass(event)
-- event.propagate = true

A chain's handler may not merely check something, but also perform any action and update the event according to the results of that action. Believe me, these things are immensely useful!

Stop condition

One more thing, the stop condition may be programmed as a function. The Chain.checkPropagate you've seen previously is actually just a simple function. Here's how it's defined.

Chain.checkPropagate = function(event)
    return not event.propagate 
end

So, once event.propagate turns false, it will stop the propagation of the event, that is, the handlers after the one that made the stop condition be satisfied won't be executed.

The order of execution

There is a special version of chains called Sorted Chains or, simply, schains. These store handlers with priority numbers and sort them based on those numbers on each traversal. This version of chains is by default available globally throughout the project as Chain. If you wish to use the normal version of chains, require it:

local Chain = require 'lib.chains.chain'
-- if you wish to use both or emphasize the fact that 
-- you're using the sorted version, you may include it too
local SChain = require 'lib.chains.schain' 

Same goes for Chain Template and Sorted Chain Template.

Ranks

There are four predefined ranks: HIGHEST, HIGH, MEDIUM, LOW, LOWEST. The Ranks enum is available throughout the project.

The handlers will be executed in order from highest to lowest.

chain:addHandler({ h1, Ranks.LOW     })
chain:addHandler({ h2, Ranks.HIGHEST })
-- although h2 was added after h1, it will be executed before h1

If you're adding new handlers without specifying the ranks, they will end up in the medium rank.

-- this:
chain:addHandler({ myHandler, Ranks.MEDIUM })

-- is equivalent to this:
chain:addHandler(myHandler)

The handlers added to the same rank are always executed in the order they were added.

chain:addHandler({ h1, Ranks.MEDIUM })
chain:addHandler({ h2, Ranks.MEDIUM })
-- h1 is always executed before h2

Ranks to Numbers conversion

Under the hood, ranks are mapped to priority numbers, which are plain integers. They are stored in the Numbers.rankMap array. The Numbers enum is available throught the project. As a result, stuff like this is also possible:

chain:addHandler({ h1, Ranks.MEDIUM })
chain:addHandler({ h2, Numbers.rankMap[Ranks.MEDIUM] + 1 })
-- h2 will be executed just before h1

Ranks themselves are simply numbers from 1 to 5, which are recognozed by the SChain as priority categories instead of actual priority numbers. These three statements are equivalent:

chain:addHandler(h1)
chain:addHandler({ h1, Ranks.MEDIUM })
chain:addHandler({ h1, 3 })

Under the hood, when you add handlers to the same rank, their priority numbers are decremented with each new handler.

chain:addHandler({ h1, Ranks.LOW }) -- maps to 200000 (Numbers.rankMap[Ranks.LOW] == 200000)
chain:addHandler({ h2, Ranks.LOW }) -- maps to 199995
chain:addHandler({ h3, Ranks.LOW }) -- maps to 199990

-- the other ranks are unaffected
chain:addHandler({ h4, Ranks.MEDIUM }) -- still 300000
chain:addHandler({ h5, Ranks.HIGH   }) -- still 400000

-- other chain instances are intact too
anotherChain:addHandler({ h6, Ranks.LOW }) -- maps to 200000

NOTE: The priority numbers of a chain instance are not restored once a handler is removed.

Technically, you may use floats, but you are recommended to use unsigned integers. Also, do not use numbers from 1 to 5 for priority numbers as those are processed as indicating ranks instead.

Why is it useful?

For example, the water tile prevents attacks, digs and moves by placing a high-priority handler that blocks the propagation of the event on the corresponding do chains: attack, dig and move. It must place the handler onto the do chain, since the tried action should succeed so that we don't have a situation like: the entity tried to attack -> it did not succeed because of being submerged -> the entity is getting unstuck, that is, the submerge handlers are removed -> the entity tries the next handler in the action chain, that is, tries to move, since the attack weren't successful. The moving succeeds, which is not what should've happened. By having the handler be of high priority, we guarantee that no other action will be tried, since the attack is considered successful.

Another example would be a ring that is destroyed if the player is about to take damage and prevents the damage. Such a ring would have low or lowest priority so that it is broken after all other damage reducers have had their turn at diminishing attack damage.

Chain Templates

Overview

Chain templates offer a way of planning out a standart structure of a set of chains.

Example

local ChainTemplate = require 'lib.chains.chaintemplate'

local template = ChainTemplate() 

local function handler00(event) end
local function handler01(event) end
local function handler10(event) end

-- add a chain with 2 handlers
template:addChain('chain0')
template:addHandler('chain0', handler00)
template:addHandler('chain0', handler01)

-- add a chain with 1 handler
template:addChain('chain1')
template:addHandler('chain1', handler10)

-- add a chain with 0 handlers
template:addChain('chain2')

local chains = template:init()

print(inspect(chains))
-- {
--     chain0 = Chain,
--     chain1 = Chain,
--     chain2 = Chain
-- }

Checking if a chain is set

For checking the existence of a chain, use template.isSetChain(chainName)

← The lifecycle of the gameCalling Conventions →
  • Chains
    • What are chains?
    • Example
    • Stop condition
    • The order of execution
  • Chain Templates
    • Overview
    • Example
    • Checking if a chain is set
Dungeon Hopper
Docs
Getting Started
Community
User ShowcaseStack OverflowProject ChatTwitter
More
BlogGitHub RepoStar
Facebook Open Source
Copyright © 2020 Your Name or Your Company Name