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.
Implements
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);
// Makes Render() no-op by default, but overrideable by subclasses.
public virtual void Render(
IActionContext context,
IAccountStateDelta nextStates)
{
}
// Makes Unrender() no-op by default,
// but overrideable by subclasses.
public virtual void Unrender(
IActionContext context,
IAccountStateDelta nextStates)
{
}
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,
})
);
}
void IAction.Render(
IActionContext context,
IAccountStateDelta nextStates)
{
var c = new Character
{
Address = TargetAddress,
Hp = (Integer)nextStates.GetState(TargetAddress),
};
c.Draw();
}
void IAction.Unrender(
IActionContext context,
IAccountStateDelta nextStates)
{
Character c = Character.GetByAddress(TargetAddress);
c.Hide();
}
}
[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))
);
}
void IAction.Render(
IActionContext context,
IAccountStateDelta nextStates)
{
Character c = Character.GetByAddress(TargetAddress);
c.Hp = ((Bencodex.Types.Dictionary)nextStates.GetState(TargetAddress))
.GetValue<Integer>("hp");
c.Draw();
}
void IAction.Unrender(
IActionContext context,
IAccountStateDelta nextStates)
{
Character c = Character.GetByAddress(TargetAddress);
var target =
(Bencodex.Types.Dictionary)context.PreviousStates.GetState(TargetAddress);
c.Hp = target.GetValue<Integer>("hp");
c.Draw();
}
}
[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))
);
}
void IAction.Render(
IActionContext context,
IAccountStateDelta nextStates)
{
Character c = Character.GetByAddress(TargetAddress);
var target =
(Bencodex.Types.Dictionary)context.PreviousStates.GetState(TargetAddress);
c.Hp = target.GetValue<Integer>("hp");
c.Draw();
}
void IAction.Unrender(
IActionContext context,
IAccountStateDelta nextStates)
{
Character c = Character.GetByAddress(TargetAddress);
var target =
(Bencodex.Types.Dictionary)context.PreviousStates.GetState(TargetAddress);
c.Hp = target.GetValue<Integer>("hp");
c.Draw();
}
}
Constructors
| Improve this Doc View SourcePolymorphicAction()
Do not use this constructor. Use PolymorphicAction(T) instead.
Declaration
public PolymorphicAction()
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 |
Exceptions
Type | Condition |
---|---|
MissingActionTypeException | Thrown
when the class of the given |
Properties
| Improve this Doc View SourceInnerAction
The wrapped action object of T
(or one of its subtypes).
Declaration
public T InnerAction { get; }
Property Value
Type | Description |
---|---|
T |
PlainValue
Declaration
public IValue PlainValue { get; }
Property Value
Type | Description |
---|---|
IValue |
Methods
| Improve this Doc View SourceExecute(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, write such code in the Render(IActionContext, IAccountStateDelta) method instead. The Render(IActionContext, IAccountStateDelta) method is guaranteed to be called only once, and only after an action is transmitted to other nodes in the network.
For randomness, never use
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
For more on determinism in general, please read also Tendermint ABCI's docs on determinism.
See Also
| Improve this Doc View SourceLoadPlainValue(Dictionary)
Declaration
public void LoadPlainValue(Dictionary plainValue)
Parameters
Type | Name | Description |
---|---|---|
Dictionary | plainValue |
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
| Improve this Doc View SourceRender(IActionContext, IAccountStateDelta)
Does things that should be done right after this action is spread to the network or is “confirmed” (kind of) by each peer node.
Usually, this method updates the in-memory game states (if exist), and then sends a signal to the UI thread (usually the main thread) so that the graphics on the display is redrawn.
Declaration
public void Render(IActionContext context, IAccountStateDelta nextStates)
Parameters
Type | Name | Description |
---|---|---|
IActionContext | context | The equivalent context object to
what Execute(IActionContext) method had received.
That means PreviousStates are
the states right before this action executed.
For the states after this action executed,
use the |
IAccountStateDelta | nextStates | The states right after this action executed, which means it is equivalent to what Execute(IActionContext) method returned. |
ToString()
Declaration
public override string ToString()
Returns
Type | Description |
---|---|
String |
Unrender(IActionContext, IAccountStateDelta)
Does things that should be undone right after this action is invalidated (mostly due to a block which this action has belonged to becoming considered a stale).
This method takes the equivalent arguments to Render(IActionContext, IAccountStateDelta) method.
Declaration
public void Unrender(IActionContext context, IAccountStateDelta nextStates)
Parameters
Type | Name | Description |
---|---|---|
IActionContext | context | The equivalent context object to
what Execute(IActionContext) method had received.
That means PreviousStates are
the states right before this action executed.
For the states after this action executed,
use the |
IAccountStateDelta | nextStates | The states right after this action executed, which means it is equivalent to what Execute(IActionContext) method returned. |
Remarks
As a rule of thumb, this should be the inverse of Render(IActionContext, IAccountStateDelta) method with redrawing the graphics on the display at the finish.
Operators
| Improve this Doc View SourceImplicit(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 |
Returns
Type | Description |
---|---|
PolymorphicAction<T> | A PolymorphicAction<T> wrapping the given
|
Exceptions
Type | Condition |
---|---|
MissingActionTypeException | Thrown
when the class of the given |