RayBlast Games
Home Spirit Drop
Spirit Drop

Back

Custom Versus Rulesets

NOTE: The reference here is in progress and may expand later as rulesets become more powerful.

Spirit Drop holds a versus engine that can refer to ruleset objects. You can create these rulesets from JSON files that are formatted appropriately. An in-game editor will come for v0.1.3.0 if you don't like writing in JSON. The parser interprets JSONC (JSON with Comments), but if your IDE is against comments, you can use keys starting with "_c" anywhere.

To get started, create a .json file with this template (IDE with JSON Schema support or plugin recommended!):

{ "name": "Your Ruleset Name", "author": "your_username", "$schema": "https://rayblastgames.com/spiritdrop/ruleset-schema.json" // Insert rule events here }

After this, you can append event names with array values. Each event contains multiple rule objects. Every rule is identified by "type".

Jump into the Discord server if you have any questions

Table of Contents

  1. Synchronization
  2. Events
    1. modeStartRules
    2. boardStartRules
    3. modePassiveRules
    4. boardPassiveRules
    5. syncedModeVariableRules
    6. syncedGroupVariableRules
    7. syncedBoardVariableRules
    8. attackCommitRules
    9. placementRules
    10. attackPushRules
    11. attackReceiveRules
    12. beforeGarbageAcceptRules
    13. garbageAcceptRules
    14. beforeGarbageIntakeRules
    15. garbageIntakeRules
    16. riseLineRules
    17. beforeTopOutRules
    18. playerTopOutRules
    19. buttonPressRules
    20. userDefinedRules
  3. Rules
    1. add
    2. aliveGroupsRelative
    3. alivePlayersRelative
    4. announce
    5. assign
    6. assignNew
    7. createAttack
    8. endMode
    9. groupBoardsRelative
    10. groupIndexRelative
    11. if
    12. lua
    13. multiply
    14. playerIndexRelative
    15. playSound
    16. startRound
    17. stopPlaying
    18. subtract
    19. target
    20. update
    21. userDefined
    22. while
  4. Variables
    1. Hardcoded Variables
    2. Mode Settings
    3. Board Preferences
    4. Mode Variables
    5. Group Variables
    6. Board Variables
    7. Scope Variables
    8. Board Statistics
    9. Supported CLR and Lua Types
  5. Expressions
    1. add
    2. subtract
    3. multiply
    4. divide
    5. integer divide
    6. modulo
    7. power
    8. bitwise and
    9. bitwise or
    10. bitwise exclusive or
    11. bitwise complement
    12. bitshift left
    13. bitshift right
    14. logical and
    15. logical or
    16. logical exclusive or
    17. not
    18. truth
    19. equality
    20. inequality
    21. greater than
    22. less than
    23. at least / greater than or equal to
    24. no more than / less than or equal to
    25. string concatenation
    26. string removal
    27. number format
    28. index
    29. update
    30. list
    31. dictionary
    32. countof
    33. ternary
  6. Statements
    1. equality
    2. inequality
    3. greater than
    4. less than
    5. at least / greater than or equal to
    6. no more than / less than or equal to
    7. and
    8. or
    9. exclusive or
    10. not
    11. truth
  7. Event Context
  8. Board Data
  9. Lua
  10. Tips

1. Synchronization

To include a variable for synchronization in multiplayer and replays, append one of these to the ruleset:

"syncedModeVariables": [ // Array of mode variables (don't include "mode.") ]

"syncedGroupVariables": [ // Array of group variables (don't include "group.") ]

"syncedBoardVariables": [ // Array of board variables (don't include "board.") ]

Each time you set these variables (read: each time) the values require networking. Setting them every frame will cause several problems such as restricting the max number of players, slowing down the framerate, occupying gigabytes in replay size, or a combination of such. Worth noting is that these variables are only updated when time starts passing, so you may get unexpected behavior if you were to treat them as normal compute variables (using a variable immediately after changing it results in the variable returning its old value). Generally, you want to use the variables within the events that are called for them. These variables should be saved for values that do not change often, such as points rewarded to winners of versus rounds.

Name Who can write Event called Safe to control timings
syncedModeVariables The current host syncedModeVariableRules No
syncedGroupVariables The player with the lowest boardIndex in the group syncedGroupVariableRules No
syncedBoardVariables The player controlling the board syncedBoardVariableRules Yes

2. Events

To include a new event, simply append the name of the event, and then give it an array value. Note that "buttonPressRules" and "userDefinedRules" are structured differently.

Whenever one of these events start, the scope variables are cleared (except for userDefinedRules).

modeStartRules

Array of rules that are ran when the mode starts.
Context:

"modeStartRules": [ // Array of rules ]

boardStartRules

Array of rules that are ran whenever a board is created.
Context:

"boardStartRules": [ // Array of rules ]

modePassiveRules

Array of rules that are ran every frame (delta time varies, do not assume 60 fps).
Context:

"modePassiveRules": [ // Array of rules ]

boardPassiveRules

Array of rules that are ran every frame for each board (delta time varies, do not assume 60 fps).
Context:

"boardPassiveRules": [ // Array of rules ]

syncedModeVariableRules

Array of rules that are ran whenever a synced mode variable changes. NOTE: Need to supply info about how to check what variable changed.
Context:

"syncedModeVariableRules": [ // Array of rules ]

syncedGroupVariableRules

Array of rules that are ran whenever a synced group variable changes. NOTE: Need to supply info about how to check what variable changed.
Context:

"syncedGroupVariableRules": [ // Array of rules ]

syncedBoardVariableRules

Array of rules that are ran whenever a synced board variable changes. This event provides a synchronization point for board events. NOTE: Need to supply info about how to check what variable changed.
Context:

"syncedBoardVariableRules": [ // Array of rules ]

attackCommitRules

Array of rules that are ran whenever a piece is placed, right before attack and cancel is used. This event allows calculating resultAttackPower, resultBaseAttack, and resultGarbageCancel.
Context:

"attackCommitRules": [ // Array of rules ]

placementRules

Array of rules that are ran after attackCommitRules.
Context:

"placementRules": [ // Array of rules ]

attackPushRules

Array of rules that are ran whenever an attack is created by a board.
Context:

"attackPushRules": [ // Array of rules ]

attackReceiveRules

Array of rules that are ran immediately after a board receives an attack.
Context:

"attackReceiveRules": [ // Array of rules ]

beforeGarbageAcceptRules

Array of rules that are ran when an attack arrives at a board.
Context:

"beforeGarbageAcceptRules": [ // Array of rules ]

garbageAcceptRules

Array of rules that are ran after the attack pushes garbage onto a board's garbage meter.
Context:

"garbageAcceptRules": [ // Array of rules ]

beforeGarbageIntakeRules

Array of rules that are ran whenever the player allows garbage to enter into the Rise Meter.
Context:

"beforeGarbageIntakeRules": [ // Array of rules ]

garbageIntakeRules

Array of rules that are ran after garbage has been put into the Rise Meter.
Context:

"garbageIntakeRules": [ // Array of rules ]

riseLineRules

Array of rules that are ran after each line enters the board from the Rise Meter.
Context:

"riseLineRules": [ // Array of rules ]

beforeTopOutRules

Array of rules that are ran before a player declares themselves as topped out. Useful to avert normal lose conditions by unblocking the spawn window.
Context:

"beforeTopOutRules": [ // Array of rules ]

playerTopOutRules

Array of rules that are ran whenever a player locks out or blocks out.
Context:

"playerTopOutRules": [ // Array of rules ]

buttonPressRules

Two-dimensional array of rules that are ran whenever the player presses one of the General buttons.
The number of arrays in here should range between 1 and 4. You cannot have more than 4 because only 4 General buttons can be used in the game.
Context:

"buttonPressRules": [ // General1 Rules [ // Array of rules ], // General2 Rules [ // Array of rules ] ]

userDefinedRules

Custom events that can be triggered with the userDefined rule, gives the ability to create functions.
Instead of being an array, this one is an object. Every userDefined event you create will be a key here.
Note that invoking one of these events does not clear the context, so you could treat these as mode functions, board functions, etc.

"userDefinedRules": { /* userFunction1 - An example event to demonstrate syntax */ "userFunction1": [ // Array of rules ], /* userFunction2 - Another example event to demonstrate syntax Variables used: variable1, board.variable2 */ "userFunction2": [ // Array of rules ] }

3. Rules

add

Adds a value to a variable.

If neither or both expression and luaPath are provided, a parse failure will occur Typically, you want to deal with numbers, but when it comes to collections, you can make them grow instead.

{ // Add 10 to the board's variable1 "type": "add", "variable": "board.variable1", "expression": "10" }

aliveGroupsRelative

Applies rules to all alive groups.

This is a relative rule and thus modifies the context:

{ // Multiply the points of each alive group by 2 "type": "aliveGroupsRelative", "rules": [ { "type": "multiply" "variable": "group.points" "expression": "2" } ] }

alivePlayersRelative

Applies rules to all alive players; be careful not to use this multiple times in a row.

This is a relative rule and thus modifies the context:

{ // Give a point to each alive player "type": "alivePlayersRelative", "rules": [ { "type": "add" "variable": "board.points" "expression": "1" } ] }

announce

Causes a large message to appear on the right side of the screen. Use the s+ operator to print variable values inside your messages.

{ // Tell everyone that this player has x.xx seconds remaining "type": "announce", "expression": "$board.playerName; ' has '; $board.timeLeft; :0.00; ' seconds left!'; s+; s+; s+" // 4 strings in the stack, so do 3 concatenations }

assign

Gives a variable a value.

If neither or both expression and luaPath are provided, a parse failure will occur If you want to assign something inside a collection, use the update rule instead.

{ // Set the board's variable1 to (variable2 + 6) * 3 "type": "assign", "variable": "board.variable1", "expression": "$variable2; 6; +; 3; *" }

assignNew

The same as assign, but if the variable is not assigned yet it will create a scope variable instead of throwing an exception
Note that mode variables, group variables, and board variables will be automatically initialized as null; number type operations will treat this as 0 (zero)

{ // Create variable3 set to the larger of board.variable or half of variable2 "type": "assignNew", "variable": "variable3", "luaPath": "myLuaScript::max", "luaArguments": [ "$board.variable1" "$variable2; 2; /" // Pass in 1/2 of variable2 ] }

myLuaScript.lua

-- returns the larger of the two numbers function max(num1, num2) if(num1 > num2) then result = num1 else result = num2 end return result end

createAttack

Creates an attack object and pushes it through the current board's targeting style; due to limitations, Lua calls must be handled beforehand.

This object is a form of data that can be sent to other boards; you can enact on the data through certain events that initialize it in the attack context.

{ "type": "createAttack", "lineCount": "$resultAttackSent", "mutateChance": "$mutateChance", "cheeseRate": "$cheeseRate", "travelTime": "0.5", }

endMode

Allows the mode to end in 3 seconds

{ "type": "endMode" }

groupBoardsRelative

Applies rules to all boards of the current group. This event cannot be called from a mode event or a button press event unless such an event uses another relative rule.

This is a relative rule and thus modifies the context:

{ // Increase the gravity of all the boards of the group "type": "groupBoardsRelative", "rules": [ { "type": "multiply" "variable": "board.gravityInterval" "expression": "0.9" } ] }

groupIndexRelative

Applies rules to one group.

If neither or both expression and luaPath are provided, a parse failure will occur.

This is a relative rule and thus modifies the context:

{ // Increase the gravity of all the boards of the second group "type": "groupIndexRelative", "expression": "1", "rules": [ { "type": "multiply" "variable": "board.gravityInterval" "expression": "0.5" } ] }

if

Controls flow based on a statement
For complex expression ternary, you should use this rule instead, as ternary requires evaluating the values to pick from, while these rules are skippable.
If you want to use Lua to compute logic, use an assign rule beforehand

If neither onTrue or onFalse are provided a warning will be thrown.

{ // Check lines cleared "type": "if", "statement": "$result.linesCleared >= 4", "onTrue": [ { // Add 4 to the attack total "type": "add" "variable": "result.attackPower" "expression": "4" } ], "onFalse": [ { // Throw away the attack "type": "assign" "variable": "result.attackPower" "expression": "0" } ] }

lua

Calls a Lua script with a specific name and a specific function.
If you want to use this function to compute values (likely), then you should use assign, add, multiply, etc.

{ "type": "lua", "path": "myLuaScript::testFunction", "arguments": [ "$board.statistics[Lines]" ] }

multiply

Multiplies a number to a variable.

If neither or both expression and luaPath are provided, a parse failure will occur

{ // Multiplies the board's variable1 by 2.5 "type": "multiply", "variable": "board.variable1", "expression": "2.5" }

playerIndexRelative

Applies rules to one player.

If neither or both expression and luaPath are provided, a parse failure will occur.

This is a relative rule and thus modifies the context:

{ // Makes the first player receive garbage one at a time "type": "playerIndexRelative", "expression": "0", "rules": [ { "type": "assign" "variable": "board.garbageIntake" "expression": "'individual'" } ] }

playSound

Plays a sound effect in the Sounds folder.

If the expression yields an empty string, then no sound will play at all.

{ // Plays the level up sound "type": "playSound", "expression": "'level up'" }

stopPlaying

Causes the current board to halt and be put in the finished state

{ "type": "stopPlaying" }

startRound

Causes all players to restart their board states in 2 seconds; in multiplayer, syncing occurs

{ "type": "startRound" }

subtract

Subtracts a value from a variable.

If neither or both expression and luaPath are provided, a parse failure will occur Typically, you want to deal with numbers, but when it comes to collections, you can make them shrink instead.

{ // Subtract 10 to the board's variable1 "type": "subtract", "variable": "board.variable1", "expression": "10" }

target

Causes the current board to change target styles.

{ // Sets the player's target style to all attackers and lets them face their wrath "type": "target", "style": "AllAttackers", "attributes": [ "'noDistribution'" ] }

update

Updates a collection with a key-value pair.

If neither or both value and luaPath are provided, a parse failure will occur

{ // Set key 10 in a dictionary to 'You're too slow' "type": "update", "variable": "board.variable5", "key": "10", "value": "'You\\'re too slow" }

userDefined

Triggers a user defined event.
Note that the context does not change and the scope variables are not wiped.

{ "type": "userDefined" "name": "updateBoardStage" }

"userDefinedRules": { "updateBoardStage": [ { "type": "assign" "variable": "board.stage" "expression": "$gameTime; 60; //" } ] }

while

Performs rules while a statement remains true
If you want to use Lua to compute logic, use an assign rule beforehand and after each loop

If rules is empty an error will be thrown as this will inevitably lead to an infinite loop
Keep in mind there is also a 3 second time limit, and then the loop will be terminated

{ "type": "assignNew" "variable": "a" "expression": "$board.level" }, { // Add ceiling(log2(level)) to attack power "type": "while", "statement": "$a > 1", "rules": [ { // Add 1 to the attack total "type": "add" "variable": "result.attackPower" "expression": "1" }, { // Divide a by 2 "type": "multiply" "variable": "a" "expression": "0.5" } ] }

4. Variables

Hardcoded Variables

Variables with get instead of read are considered more expensive to fetch. Variables with set instead of write have side effects.

Name CLR Type Permissions Description
changedVariable string Read The name of the variable that was changed in the syncedModeVariableRules, syncedGroupVariableRules, and syncedBoardVariableRules events
gameTime double Read Number of seconds that the current board has been active; stops progressing once the player tops out or when the stop playing rule is ran
modeTime double Read Number of seconds since the mode started the first round
roundTime double Read Number of seconds since the round started
roundNumber int16 Read The current round in the game session, starts at 1
playerCount uint16 Read Number of player definitions there are
alivePlayerCount uint16 Read Number of boards that have not topped out
aliveGroupCount uint16 Read Number of groups that have at least one board that hasn't topped out
targetCount uint16 Read Number of boards targeted by the current board
targets List<uint16> Get/Set A list of board indexes that corresponding to who the board is targeting
attackerCount uint16 Read Number of boards that are targeting the current board
attackerCount List<uint16> Get A list of board indexes that correspond to who is targeting the board
boardIsLocal bool Read If true, the current board is controlled by a user of the system
boardIsLead bool Read If true, the current board is in focus in the view
localPlayerIsAlive bool Get Returns true when a local player is alive; should be used only for visuals and not for logic
resultLinesCleared
result.linesCleared
byte Read Number of line clears the piece placement formed
resultSpecialType
result.specialType
enum Read Is one of these values:
  • 0 / SpecialResult.None - nothing special
  • 1 / SpecialResult.Tuck - when the player's last action is a movement that puts the piece under an overhang; only achieved with no move resets
  • 2 / SpecialResult.AllClear - when the board has no stack after clearing at least 4 lines
  • 3 / SpecialResult.AllClearMini - when the board has no stack after clearing no more than 3 lines
  • 4 / SpecialResult.FlatClear - when the line clears are all at the top of the stack
  • 5 / SpecialResult.SplitClear - when the line clears are in multiple groups
resultIsMinorTwist
result.isMinorTwist
bool Read Denotes if a minor twist was performed
resultIsMajorTwist
result.isMajorTwist
bool Read Denotes if a major twist was performed
resultGarbageCleared
result.garbageCleared
byte Read Number of line clears the piece placement formed involving rows that came from the Rise Meter
resultIsAerial
result.isAerial
bool Read Denotes if this was a line clear that was performed over a bunch of space right underneath
resultIsReaper
result.isReaper
bool Read Denotes if this was a piece placement that was saved from block out or involved a line clear at the top
resultAttackPower
result.attackPower
uint16 Read/Write
(situational)
A number that can be used to create attacks; can only be written to in the attack commit rules
resultBaseAttack
result.baseAttack
uint16 Read/Write
(situational)
A number that is used for the base attack statistic which appears outside of 2-player, not used otherwise; can only be written to in the attack commit rules. There is no way to validate base attack calculation so do not forget to set this or it will show as "0.00 base atk/min"
resultAttackSent
result.attackSent
uint16 Read A number that is equal to attack power minus garbage burned; cancel amount is added to this number when it is used
resultGarbageCancel
result.garbageCancel
uint16 Read/Write
(situational)
A number that can be used to bypass garbage to send attack; can only be written to in the attack commit rules
allClearCount uint32 Read Number of all clears or all clear minis that were performed
combo int32 Read Current combo count; is -1 when no line clears occurred
backToBack int32 Read Current Back-to-Back chain; is -1 when there is no chain
previousBackToBack int32 Read The Back-to-Back chain that existed before the placement; when this is greater than backToBack, it means a chain just ended
thereIsAPlayerInDanger bool Get Checks if any alive player is near the top
musicLevel int32 Read/Set When assigned to, the music will change. Set to -1 to turn off the music. If duel is set to true, it will be automatically set and setting this variable has no effect.
The following levels can be used:
  1. Stage 1
  2. Stage 2
  3. Stage 3
  4. Stage 4
  5. Stage 5
  6. Stage 6
  7. Versus
  8. 1v1 Winning
  9. 1v1 Losing
  10. 1v1 Tiebreaker
  11. Transcension (Act I music)
gravityInterval double Read/Write Board timing for how long it takes for the current piece to move down one row; writing to this will reschedule the Gravity event
spawnDelayMin
spawnDelayMinimum
double Read/Write Minimum time to summon the next piece; writing to this will reschedule the Spawn events
spawnDelayMax
spawnDelayMaximum
double Read/Write Maximum time to summon the next piece; writing to this will reschedule the Spawn events
lineClearDelayMin
lineClearDelayMinimum
double Read/Write Board timing for how long it takes for the stack to compress after line clears; writing to this will reschedule the LineFall and Spawn events
lineClearDelayMax
lineClearDelayMaximum
double Read/Write Board timing for how long it takes for the stack to compress after line clears; writing to this will reschedule the LineFall and Spawn events
asdMin
asdMinimum
double Read/Write Minimum allowed Auto-Shift Delay; default is 0; is applied after the placement event
asdMax
asdMaximum
double Read/Write Maximum allowed Auto-Shift Delay; default is infinity; is applied after the placement event
aspMin
aspMinimum
double Read/Write Minimum allowed Auto-Shift Period; default is 0; is applied after the placement event
aspMax
aspMaximum
double Read/Write Maximum allowed Auto-Shift Period; default is infinity; is applied after the placement event
lockDelay double Read/Write Board timing for how long it takes for the current piece to automatically lock down; writing to this will reschedule the Lock event
garbageIntake string Read/Write Set to 'individual' to have the Rise Meter take in one garbage at a time or the garbageIntakeLimit, whichever is lower
Set to 'conglomerate' to have the Rise Meter take in as much as the garbageIntakeLimit
attackIntakeDelay
attack.intakeDelay
garbageIntakeDelay
garbage.intakeDelay
double Read/Write Determines how long it sits in the intake section of the Garbage Meter before it can be taken in by the Rise Meter
attackTravelTime
attack.travelTime
double Read/Write Determines how long it takes for the attack to travel to its destination; writing to this will reschedule the GarbageAccept event
garbageLimit int32 Read/Set Determines how many lines of garbage the Garbage Meter can hold; setting this will immediately toss any excess, so do not set to 0 if you're recalculating
garbageIntakeLimit byte Read/Set Determines how many lines of garbage can enter in every piece placement; setting this will immediately "unprime" pending garbage, so do not set to 0 if you're recalculating
garbageSpawnDelay double Read/Write Determines how long the Rise Meter waits before applying rows from the Garbage Meter; has no effect if the Rise Meter already has rows
groupCount uint16 Read Number of groups
aliveEnemyCount uint16 Read Number of opponents that have not topped out
koHistory List<List<object>> Get This is an expensive variable to translate; use the below variable if you are only interested in the latest KO to increase performance, otherwise use this to get a full list of the entries across all rounds in the below format
latestKO List<object> Get This translates the last KO that occurred into data you can use; if no one has been KO'd yet, this will be null, otherwise you will get a tuple that gives you the board index that was KO'd, the board index of the killer (or the KO'd if suicide), the round it occurred in, and the roundTime it occurred
stack
boardStack
board.stack
List<byte>
Dictionary<uint16, byte>
Get/Set Returns a list of bytes that represent the stack of the current board; this is an expensive variable to retrieve and overwrite; data is read from bottom to top, left to right (for example, the 7th cell in the 3rd row would be index 26 (2 * 10 + 6) or Lua index 27)
To apply changes, you must set it back; when providing a dictionary, the keys represent cell positions; this allows you to overwrite parts of the stack without having to retrieve the current data
Refer to this data format for each byte
previewCount byte Read/Write Controls the number of next piece previews visible to the player; currently the limit is 7
nextPieces List<string> Get/Set Returns a list representing the order of the pieces as their name and orientations in the Next Queue (formatted as "name:orientation"); to apply changes, you must set it back; if the queue is too short, it will be filled up with the current bag
bag List<string> Get/Set A list representing the piece and orientation combinations the piece generator can use; to apply changes, you must set it back; WARNING: This will be empty in replays
attackLineCount
attack.lineCount
garbageLineCount
garbage.lineCount
uint16 Read/Write The number of lines in the attack or garbage
boardWidth
board.width
byte Read/Set Number of columns in the grid
boardHeight
board.height
byte Read/Set Number of rows in the grid, or more specifically, the top out height
attackMutateChance
attack.mutateChance
garbageMutateChance
garbage.mutateChance
byte Read/Write Determines the chance the garbage will change columns from the previous garbage; 0 is guaranteed to match column, 200 is guaranteed to change column; for a 10-wide board, 180 will give each column equal chance
attackCheeseRate
attack.cheeseRate
garbageCheeseRate
garbage.cheeseRate
byte Read/Write Determines the chance the garbage will change columns for each generated row; 0 is guaranteed to match column, 200 is guaranteed to change column; for a 10-wide board, 180 will give each column equal chance
garbageAsynchronousIntake bool Read/Write If true, the player will be able to play while the Rise Meter is pushing rows
garbageLineClearIntake bool Read/Write If true, the Garbage Meter will push garbage regardless of if the player cleared a line
currentPieceName string Read Returns the name of the piece currently in control
rotationState int32 Read/Write The orientation index of the current piece; note that you can no-clip by setting it
pieceX sbyte Read/Write The X coordinate of the origin of the current piece; note that you can no-clip by setting it
pieceY sbyte Read/Write The Y coordinate of the origin of the current piece; note that you can no-clip by setting it
reaperState bool Read Is true when the player has escaped block out
resultPieceName
result.pieceName
string Read The name of the piece that was placed
resultBuildIdentifier
result.buildIdentifier
enum Read Is one of the named builds:
  • 0 / BuildIdentifier.None - meets none of the criteria below
  • 1 / BuildIdentifier.TwinQuad - cleared 2 Quads in a row
  • 2 / BuildIdentifier.QuadFrenzy - cleared 3+ Quads in a row
  • 3 / BuildIdentifier.FourWide - player has performed a combo with a Single, and the space above the placed piece is 2+ rows tall and 4 columns wide
  • 4 / BuildIdentifier.TwinQuintuple - cleared 2 Quintuples in a row
  • 5 / BuildIdentifier.QuintupleFrenzy - cleared 3+ Quintuples in a row
  • 6 / BuildIdentifier.TwinSextuple - cleared 2 Sextuples in a row
  • 7 / BuildIdentifier.SextupleFrenzy - cleared 3+ Sextuples in a row
  • 8 / BuildIdentifier.TwinSeptuple - cleared 2 Septuples in a row
  • 9 / BuildIdentifier.SeptupleFrenzy - cleared 3+ Septuples in a row
  • 10 / BuildIdentifier.TwinOctuple - cleared 2 Octuples in a row
  • 11 / BuildIdentifier.OctupleFrenzy - cleared 3+ Octuples in a row
  • 12 / BuildIdentifier.Goodtwist - currently unused, but will represent J or L-Twists that rotate around a 'hook' from upside-down state
  • 13 / BuildIdentifier.Roofless - currently unused, but will represent S or Z-Droptwists that use no overhangs
risePeriod double Read/Write Board timing for how long it takes for each line in the Rise Meter to appear; writing to this will reschedule the GarbageIntake event
moveResets uint16 Read/Write Number of times the player can move a piece on the stack before it auto-locks; set to 0 to disable lock resets for movement and 10000+ to give infinite lock resets
rotationResets uint16 Read/Write Number of times the player can kick a piece before it auto-locks; set to 0 to disable lock resets for rotation and 10000+ to give infinite lock resets
resettableGravity bool Read/Write When enabled, moving a piece off a ledge will let it hover for the full gravityInterval
holdLimit uint16 Read/Write Maximum number of times the player can move pieces in and out of the Hold queue
allowHardDrop bool Read/Write Allows the use of the Hard Drop input; if not allowed, the Hard Drop Fallback is used
allowSonicDrop bool Read/Write Allows the use of the Sonic Drop input; if not allowed, Sonic Drop inputs will become Soft Drop inputs
allowPassthrough bool Read/Write When disabled, attacks will not only burn lines in the garbage meter, but lines that will be inserted in the garbage meter as well; note that if you send 0 line attacks to opponents that send attacks with lines when passthrough is disabled, the attack receive event will NOT trigger for the 0 line attacks
enableGhost bool Read/Write Reveals or hides the location a piece will end up when Hard Drop is used
groupIndex
group.index
uint16 Read Returns the index of the group
boardIndex
board.index
uint16 Read Returns the index of the board

Mode Settings

Mode settings are global constants. Mode settings are readonly and are initialized as a number set by the host before the mode begins. To expose this in the lobbies and in the AI Versus setup screen, you need a block of JSON like this in the settings portion. Supported types for min and max are numbers only. If you want the UI to display something other than numbers, you can include an enumValues attribute.
name is the ruleset variable name, accessible through mode.settings.name or simply settings.name
uiName is what appears in the menus
min is the minimum value it can be; can be a integer (up to 64-bit signed or unsigned), or a double-precision floating-point number
max is the maximum value it can be
default is the default value it will start with in the menus; this is also the value the AI use
enumValues is an array of enum strings; these strings can be referred to in the rules

"settings": [ { "name": "goalPoints" "uiName": "Goal Points" "min": 1 "max": 255 "default": 3 } ]

Board Preferences

Board preferences are unique to each player. Board preferences are readonly and are initialized as a number set by the player before the mode begins. They are structured in JSON the same way as mode settings. All board preferences must be prefixed with board.preferences. or simply preferences..

"preferences": [ { "name": "targetStyle" "uiName": "Target Style" "description": "(explanation on how this works)" "enumValues": [ "quickAccess", "cycle" ] } ]

{ // Check target style "type": "if", "statement": "$board.preferences.targetStyle == quickAccess", "onTrue": [ { "type": "target", "style": "AllAttackers" } ] }

Mode Variables

Mode variables are global variables and can be accessed anywhere. Mode variables are initialized as null before the mode begins. All mode variables must be prefixed with mode..

Name CLR Type Initial Value Description
goalPoints byte 1 A number that is used to determine the winner(s). When it is greater than 1, a bar at the bottom of the screen will describe which groups are winning (if anyone is by themselves, they are technically their own group). You will need to check if any group's points is at least this before using the endMode rule.
goalLead byte 1 A number that is used to describe how much of a lead a group needs to be winning by before they are declared the winner. This makes the middle section of the points bar wider, and the goal points will appear to be increasing if a lead is not maintained. Refer to the playerTopOutRules in the Eliminations Ruleset to see how to implement.
duel bool false Changes the behavior of musicLevel.
button1Text string null Text that says what the General1 input does
button2Text string null Text that says what the General2 input does
button3Text string null Text that says what the General3 input does
button4Text string null Text that says what the General4 input does

Group Variables

Group variables are unique to each group and can be accessed by member boards. They persist across rounds. Group variables are initialized as null before the mode begins. All group variables must be prefixed with group..

Name CLR Type Initial Value Description
points byte 0 A number that is used to determine who is winning. This also rewards spirit points at the end, based on goalPoints.
koCount uint32 0 Number of KOs this group has accrued across all its members.

Board Variables

Board variables are unique to each player. They persist across rounds. Board variables are initialized as null before the mode begins. All board variables must be prefixed with board..

Name CLR Type Initial Value Description
displayText List<string> null Text that appears on the right side of the board; if you provide anything other than a list it will be formatted as one line of text; set to null to not add any text; for performance, only create the list when the number of lines change, and use the update rule or operator to change the text inside
modeBar1 float 0f A number that fills up the first meter on the right. -infinity is hidden, 0.0 is empty, 1.0 is filled once, 2.0 is filled twice, etc.
modeBar1Colors Color[] <the 12 rainbow colors but with rose replaced with white> An array of colors used for each fill. It is recommended not to set this very often. When a list of strings is provided it will be parsed as hex values on the next frame, and you cannot modify it afterward. If the value is null it will just be a single white.
modeBar2 float -infinity A number that fills up the second meter on the right. -infinity is hidden, 0.0 is empty, 1.0 is filled once, 2.0 is filled twice, etc.
modeBar2Colors Color[] <the 12 rainbow colors but with rose replaced with white> An array of colors used for each fill. It is recommended not to set this very often. When a list of strings is provided it will be parsed as hex values on the next frame, and you cannot modify it afterward. If the value is null it will just be a single white.
attackMultiplier double 1.0 A number shown at the top of the mode bars (if it's not a number, it will be used as a string).
level int32 0 This number is used to control the typeface of the attackMultiplier shown. If it's 0, the multiplier is shown as gray, otherwise it's one of modeBar1Colors. It gets bigger when level is at least 10.
koCount uint32 0 Number of KOs since the beginning of the first round.

Scope Variables

Scope variables are temporary variables that are used to assist in computing. Scope variables are cleared on every event except for the user defined events. Due to how easy it is to accidentally create scope variables, the game chooses to throw an exception if you try to use the assign rule instead of the assignNew rule when the variable is uninitialized.

Board Statistics

Statistics are unique to each board. They are automatically managed by the game and are readonly. To access them, either do $board.statistics[Lines] or $board.statistics;Lines;[]. You can also refer to them by ID. They are all of type uint32. If you try to add to, remove from, or update $board.statistics itself, it will clone itself and turn into a dictionary.

Enum Name ID Description
Attack 0 The total number of attack power created from the attack commit event
AttackSent 1 The total number of attack created from the attack commit event that did not get cancelled by the garbage meter
BaseAttack 12 The total number of base attack created from the attack commit event
GarbageClears 6 The total number of line clears that came from the Rise Meter
Inputs 4 The total number of key presses accepted by the game
Lines 5 The total number of line clears
Pieces 7 The total number of pieces placed
Power 8 The current power level of the board; currently unused
AccumulatedPower 9 The total power ever gained, not including losses
PeakPower 10 The highest power level ever achieved in the round

Supported CLR and Lua Types

NOTE: Need to jot down this section.

Collections

Three types of collections are supported. The add, subtract and update rules as well as the add, subtract and update operators can modify them.

5. Expressions

All expressions are in postfix. You can use a mix of constants, variables, and operators, all separated by semicolons (use \\; to use actual semicolons). All variable references must be appended with $. All strings must be wrapped in single-quotes. The game will check for illegal expressions, such as when you pop more than you push.
At some point, infix expressions will be supported, but currently there's too many operators at the moment to make it worth implementing at the moment.
NOTE: Will need to work on operators involving collections

add

A;B;+
Adds A and B and pushes the result.
If A is a list, B will be appended to it instead. If A is a dictionary, B will be used as a key-value pair if it's a list of 2 elements, or the entirety of B will be inserted if it's a dictionary. This is an operator that can modify the original value, so be careful with this operator.
For performance, the update operator is more efficient than creating a temporary list or dictionary, but if B has been created beforehand there's no difference.

subtract

A;B;-
Subtracts B from A and pushes the result
When doing this on two strings, a lexical comparison is performed. The result will be positive if A follows B, negative if A precedes B, and zero if they are in the same position in the sort.
If A is a list, B will be removed from it instead. If A is a dictionary, B will be used as a key to remove, or all the keys of B will be removed from A if B is a dictionary. This is an operator that can modify the original value, so be careful with this operator.
If you were looking to remove portions of a string, use the string remove operator instead.

multiply

A;B;*A;B;•
Multiplies A and B and pushes the result

divide

A;B;/A;B;÷
Divides A by B and pushes the result

integer divide

A;B;//
Divides A by B, truncates off the remainder, and pushes the result

modulo

A;B;%
Divides A by B and pushes the remainder

power

A;B;**A;B;powA;B;ⁿ
Raises A to the B power and pushes the result

bitwise and

A;B;&
Pushes an integer that shares the same 1's as A and B

bitwise or

A;B;|
Pushes an integer that has 1's in A or B

bitwise exclusive or

A;B;^
Pushes an integer that has 1's in A or B but not in both

bitwise complement

A;~
Inverts all the bits in A and pushes the result
For performance, A;~;~;~ will be automatically converted to A;~

bitshift left

A;B;<<
Moves all the bits in A B times to the left, making the number larger
The behavior is dependent on integer type. All integers below 32-bit are converted to 32-bit. For signed integers, an arithmetic shift is performed (sign is preserved), and for unsigned integers a logical shift is performed.

bitshift right

A;B;>>
Moves all the bits in A B times to the right, making the number smaller
The behavior is dependent on integer type. All integers below 32-bit are converted to 32-bit. For signed integers, an arithmetic shift is performed (sign is preserved), and for unsigned integers a logical shift is performed.

logical and

A;B;&&A;B;and
Pushes true if both A and B are truthy, else pushes false

logical or

A;B;||A;B;or
Pushes true if A or B are truthy, else pushes false

logical exclusive or

A;B;^^A;B;xor
Pushes true if A or B are truthy but not both, else pushes false

not

A;!A;not
For performance, A;!;! will be automatically converted to A;!! and A;!!;! will be automatically converted to A;!
Pushes false if A is truthy, else pushes true

truth

A;!!A;‼
Pushes true if A is truthy, else pushes false
For performance, A;!!;!! will be automatically converted to A;!!
A value is considered truthy if it is not null/nil/undefined, not zero, not false, not an empty string, and not an empty collection

equality

A;B;==
Pushes true if A and B are considered equal, else pushes false
When comparing a string and a number, the length of the string is used

inequality

A;B;!=
Pushes false if A and B are considered equal, else pushes true
When comparing a string and a number, the length of the string is used

greater than

A;B;>
Pushes true if A is considered greater than B, else pushes false
When comparing strings, this returns true if A comes after B lexically
When comparing a string and a number, the length of the string is used

less than

A;B;<
Pushes true if A is considered greater than B, else pushes false
When comparing strings, this returns true if A comes before B lexically
When comparing a string and a number, the length of the string is used

at least / greater than or equal to

A;B;>=A;B;≥
Pushes true if A is considered greater than or equal to B, else pushes false
When comparing strings, this returns true if A comes after B lexically or is in the same position lexically
When comparing a string and a number, the length of the string is used

no more than / less than or equal to

A;B;<=A;B;≤
Pushes true if A is considered less than or equal to B, else pushes false
When comparing strings, this returns true if A comes before B lexically or is in the same position lexically
When comparing a string and a number, the length of the string is used

string concatenation

A;B;s+
Puts A in front of B and pushes the combined string
Numbers are given default format

string removal

A;B;s-
Removes any occurrences of B in A and pushes the resulting string
Numbers are given default format

number format

A;:0.00
Turns A into a string and pushes it; everything after the colon is considered the format
Refer to .NET formatting for valid formats; they all use Current Culture (the user's system settings), so do not use the result in comparisons.
There is also a special format A;:time, which will display the number as a timer "0:00.000", displaying hours only when needed. It also does not display decimal digits if the number is an integer.

index

A;B;[]
Pushes the value from A at index or with key B

update

A;B;C;@
Modifies A by assigning key B with value C, then pushes A
If A is a dictionary, key B will be set to C. If A is a list, index B will be set to C if B is a valid integer index.

list

l[]
Pushes a new list

dictionary

d[]t[]
Pushes a new dictionary/table

countof

A;countofA;countOf
Gets the number of elements in A if A is a list, or the number of key-value pairs in A if A is a dictionary/table, then pushes that number to the stack

ternary

A;B;C;?
Pushes B if A is truthy, else pushes C
Due to the nature of expressions, B and C are fully evaluated, so for complex ternary, use the if rule for performance

6. Statements

Statements are in infix and produce booleans. At first glance, they seem to have the same operators as expressions, but they operate fundamentally differently. No assignment or manipulation can be performed, and thus no Lua is allowed. You can control evaluation order using parentheses.

If you want to use postfix expressions, surround the postfix in curly braces (not technically always required but highly recommended).
$variable1 < {$board.variable2;5;+}

equality

A=BA==B
Returns true if A and B are considered equal, else returns false
When comparing a string and a number, the length of the string is used

inequality

A!=B
Returns false if A and B are considered equal, else returns true
When comparing a string and a number, the length of the string is used

greater than

A>B
Returns true if A is considered greater than B, else returns false
When comparing strings, this returns true if A comes after B lexically
When comparing a string and a number, the length of the string is used

less than

A<B
Returns true if A is considered greater than B, else returns false
When comparing strings, this returns true if A comes before B lexically
When comparing a string and a number, the length of the string is used

at least / greater than or equal to

A>=BA≥B
Returns true if A is considered greater than or equal to B, else returns false
When comparing strings, this returns true if A comes after B lexically or is in the same position lexically
When comparing a string and a number, the length of the string is used

no more than / less than or equal to

A<=BA≤B
Returns true if A is considered less than or equal to B, else returns false
When comparing strings, this returns true if A comes before B lexically or is in the same position lexically
When comparing a string and a number, the length of the string is used

and

A&BA and B
Returns true if both A and B are truthy, else returns false

or

A|BA or B
Returns true if A or B are truthy, else returns false

exclusive or

A^BA xor B
Returns true if A or B are truthy but not both, else returns false

not

!Anot A
Returns false if A is truthy, else returns true

truth

A
Returns true if A is truthy, else returns false
A value is considered truthy if it is not null/nil/undefined, not zero, not false, not an empty string, and not an empty collection

7. Event Context

The event context is a special state that is shared across rules. The nature of the values set depends on the events and rules being ran.

NOTE: Need to clarify what each contains.

8. Board Data

The board stack is a highly specialized collection of bytes. When getting and setting board data, it will be in this format, from bottom to top, left to right:

00 05 00 00 00 00 03 03 00 00
05 05 05 00 00 03 03 09 09 09
2E 2E 2E 10 2E 2E 2E 2E 2E 2E
2E 2E 2E 2E 2E 2E 2E 2E 2E 10

The right digit is the mino color. When it is 0, the cell is interpreted as being empty. 1 is red, 12 is rose, 13 is white, 15 is black, etc.
The left digit is the metadata, which is a set of 4 flags. Add numbers together to get desired cell state:

2E,2E,2E,2E,2E,2E,2E,2E,2E,10,2E,2E,2E,10,2E,2E,2E,2E,2E,2E,
05,05,05,00,00,03,03,09,09,09,00,05,00,00,00,00,03,03,00,00

[46,46,46,46,46,46,46,46,46,16,46,46,46,16,46,46,46,46,46,46,5,5,5,0,0,3,3,9,9,9,0,5,0,0,0,0,3,3,0,0]

The above array is what you'll receive when getting the board stack.

9. Lua

There are several rules that can call Lua 5.2 functions. These reside in Lua scripts that exist in the Lua folder in the game directory.
Lua support is powered by the MoonSharp plugin. You can define multiple functions in a Lua script and be able to reference them directly in rulesets.

Supported CLR Types (used by the game):

Supported Lua Types:

NOTE: Will need to experiment more with Lua scripting to give more information.

There are some differences in this interpretation of Lua, as it is sandboxed:

10. Tips

Because the game is not framerate-based, it is important that everyone is in sync. All board events are time-driven, so if you change a variable that reschedules an event, a desync might occur and can potentially cause a "Time cannot go backwards" exception (shouldn't happen by the way so report it if it does). The game will try its best to prevent these situations but there's a lot of places to plug up. Your best opportunities to change board timings are by changing them in:

If you change them outside of these situations, replays and multiplayer will visually desync but will fix themselves up afterward. It's kinda quantum-like.

If performance is low, or players are disconnecting in the middle of your custom ruleset, consider any synchronized variables that are constantly being assigned to. If synchronized board variables are used to control board timings, check if the value is actually different, since the ruleset is allowed to use synchronized variables as a sort of event-type value (think of a switch statement). The reason frequent assignment causes performance issues is because of the replay protocol causing all boards to savestate for playback. You can get a visual indicator of how bad it is by viewing a replay and seeing how many markers appear on the seek bar.

Adding a list to another list will make a multi-dimensional jagged list. If you want to append the contents of a list to another you must add each element individually.

When declaring the winner in an eliminations style battle, you need to account for latency. First you check if alivePlayerCount is no more than 1. If only 1 person has not topped out, wait until their gameTime exceeds everyone else's. If everyone is topped out, loop through all the boards and solve for the one with the longest gameTime.

Discord - RayRay26#6142

Click here for RayRay's YouTube channel

Twitter - @gamemakeraray