Custom Versus Rulesets
NOTE: The reference here is in progress and may expand later as rulesets become more powerful. This documentation is auto-generated from the game.
Spirit Drop has a gameplay loop that can refer to ruleset objects. You can create these rulesets from JSON files that are formatted appropriately. When v0.2.1 comes out, an in-game editor will be included if you don't like writing in JSON; it will also be the recommended way to create rulesets as it is able to identify potential problems and expression errors. The parser interprets JSONC (JSON with Comments); however, if you do use JSONC comments, the in-game editor will not be able to preserve them! To add a proper comment, include a property with a key that starts with "_c".
To create a ruleset JSON from scratch, create a .json file with this template (JSON schema coming soon, but include the line for later):
{ "name": "Your Ruleset Name", "author": "your_username", "identifier": "unique_ruleset_id", "versionNumber": "1", "description": "<describe your ruleset here>", "$schema": "https://rayblastgames.com/spiritdrop/ruleset-schema.json", }

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
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 and the context are cleared (except in userDefinedRules). When a rule triggers another event that is not a user defined event, the scope variables and the context are saved for after the new event finishes, so you should not rely on scope variables assuming that they have been set by another event.
→ modeStartRules
Ran when the mode starts.
Context:
- player → null
- group → null
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"modeStartRules": [ // Array of rules ]
→ boardCreateRules
Ran whenever a board is created.
Context:
- player → player of created board
- group → group the board is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"boardCreateRules": [ // Array of rules ]
→ modePassiveRules
Ran every frame (delta time varies, do not assume 60 fps).
Context:
- player → null
- group → null
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"modePassiveRules": [ // Array of rules ]
→ boardPassiveRules
Ran every frame for each board (delta time varies, do not assume 60 fps).
Context:
- player → current player
- group → group the board is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"boardPassiveRules": [ // Array of rules ]
→ syncedModeVariableRules
Ran whenever a synced mode variable changes. Use the variable changedVariableName to branch.
Context:
- player → null
- group → null
- result → null
- attack → null
- garbage → null
- changedVariableName → name of synced mode variable
"syncedModeVariableRules": [ // Array of rules ]
→ syncedGroupVariableRules
Ran whenever a synced group variable changes. Use the variable changedVariableName to branch.
Context:
- player → null
- group → group that had the variable changed
- result → null
- attack → null
- garbage → null
- changedVariableName → name of synced group variable
"syncedGroupVariableRules": [ // Array of rules ]
→ syncedPlayerVariableRules
Ran whenever a synced player variable changes. Use the variable changedVariableName to branch.
Context:
- player → current player
- group → group the player is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → name of synced player variable
"syncedPlayerVariableRules": [ // Array of rules ]
→ syncedBoardVariableRules
Ran whenever a synced board variable changes. Use the variable changedVariableName to branch.
Context:
- player → player of current board
- group → group the board is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → name of synced board variable
"syncedBoardVariableRules": [ // Array of rules ]
→ attackCommitRules
Ran whenever a piece is placed, right before attack and cancel is used. This event allows calculating resultAttackPower, resultBaseAttack, and resultGarbageCancel.
Context:
- player → current player
- group → group the board is associated with
- result → new result
- attack → null
- garbage → null
- changedVariableName → null
"attackCommitRules": [ // Array of rules ]
→ pieceSpawnRules
Ran when a piece appears after spawn delay or a Hold.
Context:
- player → player of current board
- group → group the board is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"pieceSpawnRules": [ // Array of rules ]
→ placementRules
Ran after the attackCommit rules.
Context:
- player → player of current board
- group → group the board is associated with
- result → final result
- attack → null
- garbage → null
- changedVariableName → null
"placementRules": [ // Array of rules ]
→ attackPushRules
Ran whenever an attack is created by a board.
Context:
- player → player pushing the attack
- group → group the player is associated with
- result → null
- attack → new attack
- garbage → null
- changedVariableName → null
"attackPushRules": [ // Array of rules ]
→ attackReceiveRules
Ran immediately after a board receives an attack.
Context:
- player → player of board receiving the attack
- group → group the board is associated with
- result → null
- attack → attack that started travel
- garbage → null
- changedVariableName → null
"attackReceiveRules": [ // Array of rules ]
→ beforeGarbageAcceptRules
Ran when an attack arrives at a board.
Context:
- player → player of board receiving the garbage
- group → group the board is associated with
- result → null
- attack → null
- garbage → incoming garbage
- changedVariableName → null
"beforeGarbageAcceptRules": [ // Array of rules ]
→ garbageAcceptRules
Ran after an attack pushes garbage onto a board's garbage meter.
Context:
- player → player of board that accepted the garbage
- group → group the board is associated with
- result → null
- attack → null
- garbage → added garbage
- changedVariableName → null
"garbageAcceptRules": [ // Array of rules ]
→ beforeGarbageIntakeRules
Ran whenever the player allows garbage to enter into the Rise Meter.
Context:
- player → player of board pushing garbage to the Rise Meter
- group → group the board is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"beforeGarbageIntakeRules": [ // Array of rules ]
→ garbageIntakeRules
Ran after garbage has been added to the Rise Meter.
Context:
- player → player of board that pushed garbage to the Rise Meter
- group → group the board is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"garbageIntakeRules": [ // Array of rules ]
→ riseLineRules
Ran after each line enters the board from the Rise Meter.
Context:
- player → player of board that pushed a line to the bottom of the stack
- group → group the board is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"riseLineRules": [ // Array of rules ]
→ beforeBreakdownRules
Ran before a player is about to be declared topped out. canContinue is initialized to false and countAsKO is initialized to true.
Context:
- player → player of board about to top out
- group → group the board is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"beforeBreakdownRules": [ // Array of rules ]
→ playerBreakdownRules
Ran after a player is declared topped out, regardless of if they canContinue.
Context:
- player → player of board that topped out
- group → group the board is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"playerBreakdownRules": [ // Array of rules ]
→ playerRespawnRules
Ran after a player that topped out finishes wiping their board to continue playing.
Context:
- player → player of board that wiped their stack
- group → group the board is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"playerRespawnRules": [ // Array of rules ]
→ buttonPressRules[0]
Ran when the player sends a General1 input.
In the JSON format, this is stored as a nested array of rules, where each value in the root array is another array of rules corresponding to each General button. General1 triggers index 0, General2 triggers index 1, etc. The number of arrays in here should range from 1 to 4. You cannot have more than 4 because only 4 General buttons can be used in the game.
Context:
- player → player pressing General1
- group → group the player is associated with
- result → null
- attack → null
- garbage → null
- changedVariableName → null
"buttonPressRules": [ // General1 Rules [ // Array of rules ], // General2 Rules [ // Array of rules ] ]
→ userDefinedRules
Set of events that can be triggered with the userDefined rule.
In the JSON format, this is not an array, but an object. Each property inside the object has an array of rules and can be any key that is not an existing event name.
Context:
- player → (unchanged)
- group → (unchanged)
- result → (unchanged)
- attack → (unchanged)
- garbage → (unchanged)
- changedVariableName → (unchanged)
"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
→ assign
Gives a variable a value.
If you want to assign something inside a collection, use the [[update rule]] instead.
→ add
Adds a value to a numeric variable or collection.
Typically, you want to deal with numbers, but when it comes to [[collections]], you can make them grow instead.
→ subtract
Subtracts a value from a variable or collection.
Typically, you want to deal with numbers, but when it comes to [[collections]], you can make them shrink instead.
→ multiply
Multiplies a number to a variable.
→ update
Updates a [[collection]] with a key-value pair.
→ if
Controls flow based on a truth [[statement]] For complex expression [[ternary]], you should use this rule instead, as ternary requires evaluating the values to pick from (may change later), while rules included here 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.
→ 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
Keep in mind there is also a 2 second time limit (50 ms during a board update), and then the loop will be terminated
→ base
Executes a subset of rules from the base ruleset in the same event. If an event is not defined in the current ruleset, this is implicitly used for that event, otherwise you need to use this rule to restore the base ruleset's functionality.
WARNING: if you use either of these arguments, careful consideration must be taken when you update the base ruleset.
→ 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.
→ userDefined
Triggers a user defined event. Note that the context does not change, and the scope variables carry over to the new event.
→ groupBoardsRelative
Applies rules to all boards of the group in the [[context]].
This is a relative rule and thus modifies the context:
- board → each board in the group
- group → the group the board is associated with
→ alivePlayersRelative
Applies rules to all alive players; be careful not to nest this rule inside more alivePlayersRelative rules.
This is a relative rule and thus modifies the context:
- board → each alive player's board
- group → the group the board is associated with
→ aliveGroupsRelative
Applies rules to all alive groups.
This is a relative rule and thus modifies the context:
- board → null
- group → each alive group
→ playerIDRelative
Applies rules to one player.
→ groupIDRelative
Applies rules to one group.
This is a relative rule and thus modifies the context:
- board → each player's board in the group if it exists
- group → the group the player is associated with
→ 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.
→ 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 creates 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]]. WARNING: heavy use of the data argument can make the ruleset ineligible for mass battles!
→ target
Causes the current board to change target styles and their target.
→ playSound
Plays a sound effect in the Sounds folder.
If a customSoundName is provided, soundType, ordinalA, and ordinalB are ignored.
→ createBoard
Creates a board for the player in the [[context]]; this will immediately trigger a [[boardCreate event]]
This is a relative rule and thus modifies the context:
- board → newly created board (in the new event)
- group → assigned group for the board (in the new event)
- result → null (in the new event)
- attack → null (in the new event)
- garbage → null (in the new event)
If the player already has a board, this has no effect.
→ destroyBoard
Deletes the board assigned to the player in the [[context]], effective immediately. This allows the player to receive a new board in the same round.
→ resumePlaying
Lets the player continue playing with their existing board If the player was declared finished, they are no longer finished and the board will continue processing events If the player just topped out, the top out is no longer permanent If the player is having their board permanently wiped, it will restart and revive If the player has gotten a Game Over (or has already dropped out of view), their board is summoned again, revived, and their Game Over stack wiped
The breakdown state takes a constant 1.95 seconds, the wipe state takes 0.05 seconds times the board height, and the countdown will always be 1.5 seconds. A 21 tall board will always have a 4.5 seconds break.
→ stopPlaying
Causes the current board to halt and be put in the finished state.
→ startRound
Causes all players to jump to the next round. This triggers synchronization in multiplayer. No more ruleset events are ran until the mode completes the first part of its between-round transition and everyone in multiplayer is ready.
→ endMode
Allows the mode to end. No more ruleset events will occur afterward. Versus mode will trigger its ending transition.
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 or just take some time to process.
Name | CLR Type | Permissions | Description |
---|---|---|---|
aliveEnemyCount | uint16 | Read | Number of opponents that have not topped out |
aliveGroupCount | uint16 | Read | Number of groups that have at least one board that hasn't topped out |
alivePlayerCount | uint16 | Read | Number of boards that have not topped out |
allClearCount | uint32 | Read | Number of all clears or all clear minis that were performed |
allowHardDrop | bool | Read / Write | Allows the use of the Hard Drop input; if not allowed, the Hard Drop Fallback is used |
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 |
allowSonicDrop | bool | Read / Write | Allows the use of the Sonic Drop input; if not allowed, Sonic Drop inputs will become Soft Drop inputs |
asdMax | double | Read / Write | Maximum allowed Auto-Shift Delay; default is infinity; is applied after the placement event |
asdMin | double | Read / Write | Minimum allowed Auto-Shift Delay; default is 0; is applied after the placement event |
aspMax | double | Read / Write | Maximum allowed Auto-Shift Period; default is infinity; is applied after the placement event |
aspMin | double | Read / Write | Minimum allowed Auto-Shift Period; default is 0; is applied after the placement event |
attackerCount | uint16 | Read | Number of boards that are targeting the current board |
attackers | List<int16> | Get | List of player IDs that are targeting the current board |
garbageIntakeDelay | 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 | double | Read / Write | Determines how long it takes for the attack to travel to its destination; writing to this will reschedule the GarbageAccept event |
autoDropInterval | 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 AutoDrop event |
backToBack | int32 | Read | Current Back-to-Back chain; is negative when there is no chain |
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 |
boardHeight | byte | Read / Set | Number of rows in the grid, or more specifically, the position of the spawn window |
boardIsAlive | bool | Get | If true, the current board is alive |
boardIsLead | bool | Read | If true, the current board is in focus in the view; should only be used for effects |
boardStack | 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 the data format for each byte For a nested list where each list represents a row, use boardStack2D |
boardWidth | byte | Read / Set | Number of columns in the grid |
changedVariableName | string | Read | The name of the variable that was changed in the syncedModeVariableRules, syncedGroupVariableRules, syncedPlayerVariableRules, and syncedBoardVariableRules events |
combo | int32 | Read | Current combo count; is negative when no line clears occurred |
currentPieceName | string | Read | Returns the name of the piece currently in control |
enableGhost | bool | Read / Write | Reveals or hides the location a piece will end up when Hard Drop or Sonic Drop is used |
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 |
garbageAsynchronousIntake | bool | Read / Write | If true, the player will be able to play while the Rise Meter is pushing rows |
garbageData | object | Read / Write | Object transferred with an attack; if pushed to the Rise Meter, the garbageType will determine what the rows become using this data |
garbageIntake | string | Read / Set | 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 |
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 |
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 |
garbageLineClearIntake | bool | Read / Write | If true, the Garbage Meter will push garbage regardless even if the player clears a line |
garbageRegisterA | byte | Read / Write | One byte of information; if pushed to the Rise Meter, the garbageType will determine what the rows become using this data |
garbageRegisterB | byte | Read / Write | One byte of information; if pushed to the Rise Meter, the garbageType will determine what the rows become using this data |
garbageRegisterC | byte | Read / Write | One byte of information; if pushed to the Rise Meter, the garbageType will determine what the rows become using this data |
garbageRowCount | byte | Read / Write | The number of rows in the attack or garbage; alternatively can be used simply as a quantity |
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 |
garbageType | enum(GarbageType) | Read / Write | Determines how the Rise Meter interprets the data in the garbage
|
groupCount | uint16 | Read | Number of groups in the mode |
holdCapacity | byte | Read / Write | Maximum number of pieces in the hold queue |
holdLimit | uint16 | Read / Write | Maximum number of times the player can move pieces in and out of the Hold queue; when this is exhausted, Re-Hold can be used if enabled |
koHistory | List<List<object>> | Get | This is an expensive variable to translate; use latestKO 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 same 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 list that gives you the board index that was KO'd, the board index of the killer (or the KO'd if self-destructed), the round it occurred in, and the roundTime it occurred |
lineClearDelayMax | 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 |
lineClearDelayMin | 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 |
localInputPresses | Dictionary<InputCode,bool> | Read | Returns a dictionary of all the client's input codes that were pressed this frame; they are updated right before the modePassiveRules event; note that this is input data that is not recorded in a replay, try using the General buttons if you can |
localInputState | Dictionary<InputCode,bool> | Read | Returns a dictionary of all the client's input codes and their state; they are updated right before the modePassiveRules event; note that this is input data that is not recorded in a replay, try using the General buttons if you can |
localPlayerID | int32 | Get | The player ID that corresponds to the user of the client |
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 |
modeTime | double | Read | Number of seconds since the mode started the first round |
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, setting this variable has no effect. |
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 |
pieceX | sbyte | Read / Write | The horizontal coordinate of the origin of the current piece; note that you can no-clip by setting it |
pieceY | sbyte | Read / Write | The vertical coordinate of the origin of the current piece; note that you can no-clip by setting it |
playerCount | uint16 | Read | Number of player definitions there are |
playerIsLocal | bool | Read | If true, the current board is controlled by the player of this client |
previewCount | byte | Read / Write | Controls the number of next piece previews visible to the player; currently the limit is 7 |
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 |
reaperState | bool | Read | If true, the player escaped a top out |
resonanceMax | double | Read / Write | Restricts the resonance level; default is 1.0 |
resonanceMin | double | Read / Write | Enforces a minimum resonance level; default is 0.0 |
resultAttackPower | uint16 | Read / Write (conditional) | A number that can be used to create attacks; can only be written to in the attack commit rules |
resultAttackSent | uint16 | Read | A number that is equal to attack power minus garbage burned; cancel amount is added to this number when it is used |
resultBaseAttack | uint16 | Read / Write (conditional) | 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" |
resultBuildIdentifier | enum(BuildIdentifier) | Read | Is one of the named builds that the player can make
|
resultGarbageCancel | uint16 | Read / Write (conditional) | A number that can be used to bypass garbage to send attack; can only be written to in the attackCommitRules event |
resultGarbageCleared | byte | Read | Number of line clears the piece placement formed involving rows that came from the Rise Meter |
resultIsAerial | bool | Read | Denotes if this was a line clear that was performed over a bunch of space right underneath |
resultIsMajorTwist | bool | Read | Denotes if a major twist was performed |
resultIsMinorTwist | bool | Read | Denotes if a minor twist was performed |
resultIsReaper | bool | Read | Denotes if this was a piece placement that was saved from a top out or involved a line clear at the top |
resultLinesCleared | byte | Read | Number of line clears the piece placement formed |
resultPieceName | string | Read | The name of the piece that was placed |
resultSpecialType | enum(SpecialResult) | Read | Denotes line clears with special conditions
|
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 |
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 |
rotationState | int16 | Read / Write | The orientation index of the current piece; note that you can no-clip by setting it |
roundNumber | int16 | Read | The current round in the game session; starts at 0 and goes up each time the startRound rule is used (which then starts at 1) |
roundTime | double | Read | Number of seconds since the round started |
shiftResets | uint16 | Read / Write | Number of times the player can shift a piece on the stack before it auto-locks; set to 0 to disable lock resets for shifts and 10000+ to give infinite lock resets |
spawnDelayMax | double | Read / Write | Maximum time to summon the next piece; writing to this will reschedule the Spawn events |
spawnDelayMin | double | Read / Write | Minimum time to summon the next piece; writing to this will reschedule the Spawn events |
targetCount | uint16 | Read | Number of boards targeted by the current board |
targets | List<uint16> | Get / Set | A list of player IDs that corresponding to who the current board is targeting |
thereIsAPlayerInDanger | bool | Get | Checks if any alive player is near the top; can be used to allow a different targeting style |
sdmMax | double | Read / Write | Maximum allowed Soft Drop Multiplier; default is infinity; is applied after the placement event |
sdmMin | double | Read / Write | Minimum allowed Soft Drop Multiplier; default is 0; is applied after the placement event |
sdaMax | double | Read / Write | Maximum allowed Soft Drop Addition; default is infinity; is applied after the placement event |
sdaMin | double | Read / Write | Minimum allowed Soft Drop Addition; default is 0; is applied after the placement event |
boardStack2D | List<List<int32>> | Get / Set | Returns a list of lists of bytes that represent each row of the stack of the current board; this is an expensive variable to retrieve and overwrite; rows are ordered from bottom to top, left to right (for example, the 7th cell in the 3rd row would be indexes 2 and 6, or Lua indexes 3 and 7) 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 the data format for each byte For a single list of cells, use boardStack |
groupID | uint16 | Get | Returns the index of the group |
playerID | uint16 | Read | Returns the index of the player |
→ 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 playerBreakdownRules 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.
- BoardStatistics - readonly dictionary of board statistics. If you try to add to, remove from, or update it, it will clone itself and change into a Dictionary.
- List - collection of objects. The elements can be of any type. You can create a list using the list operator.
- Dictionary / Lua Table - collection of key-value pairs. The keys and values can be of any type. All Lua tables are like this, including the simple ones that are indexed from 1. You add with lists containing 2 elements (a key and a value) or other dictionaries, and you subtract keys. 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. You can create a dictionary using the dictionary operator.
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;+
Consumes two numbers and pushes A plus B
If A is a list, then it adds B to the end of A and pushes A back
If A is a dictionary and B is a list of 2 items, the first item is used as the key and the second item is used as the value then A is pushed
If A and B are both dictionaries, all key-value pairs are added to A then A is pushed
If A is a color array, then it is cloned into a list
If A is a statistics object, it is cloned into a dictionary
For performance, do not create temporary lists just for them to be merged into another; use the update operator instead
→ subtract
A;B;-
Consumes two numbers and pushes A minus B
If A is a list, then it removes all instances of B from A and pushes A back
If A is a dictionary, B is used as a key to remove from A then A is pushed
If A and B are both dictionaries, all keys from B are removed from A then A is pushed
If A is a color array, then it is cloned into a list
If A is a statistics object, it is cloned into a dictionary
→ multiply
A;B;*
Consumes two numbers and pushes A times B
→ divide
A;B;/
Consumes two numbers and pushes A divided by B
→ integer divide
A;B;//
Consumes two numbers, converts them to 32-bit integers, and pushes A divided by B rounded down
→ modulo
A;B;%
Consumes two numbers, does A divided by B, and pushes the remainder
→ power
A;B;pow
Consumes two numbers and pushes A raised to the power of B
→ bitwise and
A;B;&
Consumes two numbers, converts them to 32-bit integers, and pushes a number where they share set bits
→ bitwise or
A;B;|
Consumes two numbers, converts them to 32-bit integers, and pushes a number where either have set bits
→ bitwise exclusive or
A;B;^
Consumes two numbers, converts them to 32-bit integers, and pushes a number where either, but not both, have set bits
→ bitwise complement
A;B;~
Consumes a number, converts it to a 32-bit integer, and pushes a number where all bits are flipped
For performance, 'A;~;~;~' is automatically converted to 'A;~'
→ bitshift left
A;B;<<
Consumes two numbers, converts them to 32-bit integers, and pushes a number where A's bits are shifted left B times
→ bitshift right
A;B;>>
Consumes two numbers, converts them to 32-bit integers, and pushes a number where A's bits are shifted right B times
→ logical and
A;B;&&
Consumes two values and pushes true if both values are truthy, or false if only one is truthy or neither are truthy
→ logical or
A;B;||
Consumes two values and pushes true if one or both values are truthy, or false if neither are truthy
→ logical exclusive or
A;B;^^
Consumes two values and pushes true if one value is truthy, or false if both or neither are truthy
→ not
A;!
Consumes one value and pushes true if it is not truthy, or false if it is truthy
For performance, 'A;!;!' is automatically converted to 'A;!!', 'A;!!;!' is automatically converted to 'A;!', and so on
→ truth
A;!!
Consumes one value and pushes true if it is truthy, or false if it is not truthy
For performance, 'A;!!;!!' is automatically converted to 'A;!!' and so on
→ equality
A;B;==
Consumes two values and pushes true if they are considered equal, or false if they are not equal
→ inequality
A;B;!=
Consumes two values and pushes true if they are not considered equal, or false if they are equal
→ greater than
A;B;>
Consumes two numbers and pushes true if A is greater than B, or false if A is less than or equal to B
→ less than
A;B;<
Consumes two numbers and pushes true if A is less than B, or false if A is greater than or equal to B
→ at least / greater than or equal to
A;B;>=
Consumes two numbers and pushes true if A is greater than or equal to B, or false if A is less than B
→ no more than / less than or equal to
A;B;<=
Consumes two numbers and pushes true if A is less than or equal to B, or false if A is greater than B
→ string concatenation
A;B;s+
Consumes two strings and pushes a string where B is appended to A
→ string removal
A;B;s-
Consumes two strings and push a string where all instances of B are removed from A
→ number format
A;B;:
Consumes a number and a string and pushes a string representation of A based on the format provided by B
Valid formats are
:time - 0:00.000
:intTime - 0:00 (rounded to nearest second)
:floorTime - 0:00 (rounded down)
:ceilTime - 0:00 (rounded up)
any valid .NET format such as ':N3'
→ index
A;B;[]
Consumes a collection and pushes an object from A at index/key B
→ update
A;B;C;@
Replaces an item C in collection A at index/key B
→ new list
A;l[]
Pushes a new list object
→ new dictionary
A;d[]
Pushes a new dictionary/table object
→ countof
A;countof
Consumes a collection and pushes the number of properties
→ ternary
A;B;C;?
Consumes a boolean and two objects and pushes B if A is truthy or C if A is not truthy
Due to the nature of postfix expressions, B and C are fully evaluated before A is checked, which can result in wasted processing; keep B and C simple or consider using an if rule
→ insert into index
A;B;C;[]+
Inserts an item C in collection A at index/key B
If A is a dictionary that already has key B, this operator leaves A unaffected
When A is a list, all items after index B are moved
→ remove at index
A;B;[]-
Removes an item in collection A at index/key B
When B is a dictionary, this operator explicitly removes the dictionary instead of the items inside, unlike the subtract operator
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=B
A==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>=B
A≥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<=B
A≤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&B
A and B
Returns true if both A and B are truthy, else returns false
→ or
A|B
A or B
Returns true if A or B are truthy, else returns false
→ exclusive or
A^B
A xor B
Returns true if A or B are truthy but not both, else returns false
→ not
!A
not 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.
- Board - is a reference to a player board that contains a lot of game data; this is not always set
- Group - is a reference to a player group with members; whenever board is set, this one is set as well; whether this is set is situational, a.k.a. you can't be directly in a mode event
- Result - is a reference to a piece result that is accessible in the attack commit and placement events
- Attack - is a reference to an attack object that is accessible in the attack push and attack receive events
- Garbage - is a reference to a garbage object that is accessible in the before garbage accept and garbage accept events
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:
- 16 (0x10) = marked, is visually indicated to the player (currently there's only the asterisk mark)
- 32 (0x20) = tagged, contains data that affects piece results (currently there's only the garbage tag, which is automatically tagged to lines generated by the garbage meter)
- 64 (0x40) = Chaos special, reserved for v0.6
- 128 (0x80) = line clearing (only 1 cell in the row needs this flag)
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.
You can also send attacks that contain specialized rows. Here's an example postfix that creates a row:
l[];l[];1;+;2;+;3;+;4;+;5;+;6;+;7;+;8;+;9;+;10;+;+
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:
- the syncedBoardVariableRules (should be safe; the same does not apply to mode and group variables; if you want to use those, copy the values to synchronized board variables and use those, but make sure they are not constantly set, see below)
- the pieceSpawnRules (completely safe)
- the placementRules (completely safe, arguably better in some cases)
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 - AgStarRay
Click here for AgStarRay's YouTube channel
Twitter - @AgStarRay