Show / Hide Table of Contents

Class PolymorphicAction<T>

A decorator to enable subtype polymorphism for action classes.

By convention, concrete action subclasses are named with verb phrases, e.g., Heal, Sell.

One downside of this compared to the vanilla IAction is the fact that it uses reflection under the hood. This may cause compatibility issues on certain platforms, and is slightly slower.

Inheritance
Object
PolymorphicAction<T>
Implements
IAction
Namespace: Libplanet.Action
Assembly: Libplanet.dll
Syntax
public sealed class PolymorphicAction<T> : object, IAction where T : IAction
Type Parameters
Name Description
T

An action base class which implements IAction and has subclasses. Usually an abstract class.

Remarks

Every concrete action subclass of T has to be marked with the ActionTypeAttribute. Even if a superclass is marked with the ActionTypeAttribute its subclass also should be marked with the ActionTypeAttribute if it is concrete.

Examples

The following example shows how polymorphic actions look like (compare this with an IAction example without subtype polymorphism):

using System;
using System.Collections.Generic;
using Bencodex.Types;
using Libplanet;
using Libplanet.Action;
// Instead of having multiple in-game actions in a class,
// in this example, we declare one abstract base class
// and its three concrete subclasses.
public abstract class ActionBase : IAction
{
    public ActionBase() { }
    public ActionBase(Address targetAddress)
    {
        TargetAddress = targetAddress;
    }
    public Address TargetAddress { get; private set; }
    // Leaves Execute() abstract so that concrete subclasses
    // implement their own logic.
    public abstract IAccountStateDelta Execute(IActionContext context);
    IValue IAction.PlainValue =>
        new Bencodex.Types.Dictionary(new Dictionary<IKey, IValue>
        {
            [(Text)"target_address"] = (Binary)TargetAddress.ToByteArray(),
        });
    void IAction.LoadPlainValue(
        IValue plainValue)
    {
        var dictionary = (Bencodex.Types.Dictionary)plainValue;
        TargetAddress =
            new Address(dictionary.GetValue<Binary>("target_address"));
    }
}
// PolymorphicAction<T> requires concrete action classes marked with
// ActionTypeAttribute.
// There is only one required parameter to ActionTypeAttribute,
// which takes a unique identifier of the action type.
// This is used for serialization and deserialization under the hood.
[ActionType("create_character")]
public sealed class CreateCharacter : ActionBase
{
    public override IAccountStateDelta Execute(IActionContext context)
    {
        var state =
            context.PreviousStates.GetState(TargetAddress);
        if (!TargetAddress.Equals(context.Signer))
            throw new Exception(
                "TargetAddress of CreateCharacter action only can be " +
                "the same address to the Transaction<T>.Signer."
            );
        else if (!(state is null))
            throw new Exception("Character was already created.");
        return context.PreviousStates.SetState(
            TargetAddress,
            new Bencodex.Types.Dictionary(new Dictionary<IKey, IValue>
            {
                [(Text)"hp"] = (Integer)20,
            })
        );
    }
}
[ActionType("attack")]
public sealed class Attack : ActionBase
{
    public override IAccountStateDelta Execute(IActionContext context)
    {
        var state =
            (Bencodex.Types.Dictionary)context.PreviousStates.GetState(TargetAddress);
        return context.PreviousStates.SetState(
            TargetAddress,
            (Bencodex.Types.Dictionary)state
                .SetItem(
                    (Text)"hp",
                    (Integer)Math.Max(state.GetValue<Integer>("hp") - 5, 0))
        );
    }
}
[ActionType("heal")]
public sealed class Heal : ActionBase
{
    public override IAccountStateDelta Execute(IActionContext context)
    {
        var state =
            (Bencodex.Types.Dictionary)context.PreviousStates.GetState(TargetAddress);
        return context.PreviousStates.SetState(
            TargetAddress,
            (Bencodex.Types.Dictionary)state
                .SetItem(
                    (Text)"hp",
                    (Integer)Math.Min(state.GetValue<Integer>("hp") + 5, 20))
        );
    }
}

Note that when it's rendered through IRenderer<T>, an instance of PolymorphicAction<T> is passed instead of its InnerAction:

public class Renderer : IActionRenderer<PolymorphicAction<ActionBase>>
{
    public void RenderAction(IAction action,
                             IActionContext context,
                             IAccountStateDelta nextStates)
    {
        if (action is PolymorphicAction<ActionBase> polymorphicAction)
        {
            switch (polymorphicAction.InnerAction)
            {
                // render things here
            }
        }
    }
    // ... other method implementations
}

Constructors

| Improve this Doc View Source

PolymorphicAction()

Do not use this constructor. Use PolymorphicAction(T) instead.

Declaration
public PolymorphicAction()
| Improve this Doc View Source

PolymorphicAction(T)

Creates a new PolymorphicAction<T> instance wrapping an innerAction.

Declaration
public PolymorphicAction(T innerAction)
Parameters
Type Name Description
T innerAction

An instance of T (or one of its subtypes) to wrap.

Exceptions
Type Condition
MissingActionTypeException

Thrown when the class of the given innerAction is not annotated with ActionTypeAttribute.

Properties

| Improve this Doc View Source

InnerAction

The wrapped action object of T (or one of its subtypes).

Declaration
public T InnerAction { get; }
Property Value
Type Description
T
| Improve this Doc View Source

PlainValue

Declaration
public IValue PlainValue { get; }
Property Value
Type Description
IValue

Methods

| Improve this Doc View Source

Execute(IActionContext)

Executes the main game logic of an action. This should be deterministic.

Through the context object, it receives information such as a transaction signer, its states immediately before the execution, and a deterministic random seed.

Other “bound” information resides in the action object in itself, as its properties (or fields).

A returned IAccountStateDelta object functions as a delta which shifts from previous states to next states.

Declaration
public IAccountStateDelta Execute(IActionContext context)
Parameters
Type Name Description
IActionContext context

A context object containing addresses that signed the transaction, states immediately before the execution, and a PRNG object which produces deterministic random numbers. See IActionContext for details.

Returns
Type Description
IAccountStateDelta

A map of changed states (so-called "dirty").

Remarks

This method should be deterministic: for structurally (member-wise) equal actions and IActionContexts, the same result should be returned. Side effects should be avoided, because an action's Execute(IActionContext) method can be called more than once, the time it's called is difficult to predict.

For changing in-memory game states or drawing graphics, implement the IRenderer<T> interface separately and attach it to a BlockChain<T> instance.

For randomness, never use nor any other PRNGs provided by other than Libplanet. Use Random instead. Random guarantees the same action has the consistent result for every node in the network.

Also do not perform I/O operations such as file system access or networking. These bring an action indeterministic. You maybe fine to log messages for debugging purpose, but equivalent messages could be logged multiple times.

Although it might be surprising, floating-point arithmetics are underspecified so that it can make different results on different machines, platforms, runtimes, compilers, and builds.

Lastly, you need to be aware and keep in mind that there is a global state named on .NET; if you format numbers, dates and times, currencies, or other such things into strings and parse these strings back these can rely on , so that the same action make different results on two differently configured systems like Thai language and French language. In order to make these types of conversions deterministic, you have to explicitly pass .

For more on determinism in general, please read also Tendermint ABCI's docs on determinism.

Lastly, you can conduct static analysis on your code using Libplanet.Analyzers. The analyzer can be enabled by adding its NuGet package into your project as a dependency.

See Also
IActionContext
| Improve this Doc View Source

LoadPlainValue(Dictionary)

Declaration
public void LoadPlainValue(Dictionary plainValue)
Parameters
Type Name Description
Dictionary plainValue
| Improve this Doc View Source

LoadPlainValue(IValue)

Deserializes serialized data (i.e., data PlainValue property made), and then fills this action's properties (or fields) with the deserialized values.

Declaration
public void LoadPlainValue(IValue plainValue)
Parameters
Type Name Description
IValue plainValue

Data (made by PlainValue property) to be deserialized and assigned to this action's properties (or fields).

See Also
PlainValue
| Improve this Doc View Source

ToString()

Declaration
public override string ToString()
Returns
Type Description
String

Operators

| Improve this Doc View Source

Implicit(T to PolymorphicAction<T>)

For convenience, an inner action T can be implicitly casted to PolymorphicAction<T>.

Declaration
public static implicit operator PolymorphicAction<T>(T innerAction)
Parameters
Type Name Description
T innerAction

An instance of T (or one of its subtypes) to wrap.

Returns
Type Description
PolymorphicAction<T>

A PolymorphicAction<T> wrapping the given innerAction.

Exceptions
Type Condition
MissingActionTypeException

Thrown when the class of the given innerAction is not annotated with ActionTypeAttribute.

Implements

IAction
  • Improve this Doc
  • View Source
In This Article
Back to top Copyright © 2018–2021 Planetarium