Originally, writing a ruleset required knowing the JSON specification for rulesets, and involved a lot of balancing brackets and not forgetting commas. Writing mods requires jq on top of that. MahjongScript was created for the purpose of unifying these tasks in some composable manner, while also avoiding these errors. Based on Elixir syntax, MahjongScript (.majs) is essentially a list of commands which compile down to jq. This jq is then applied to the empty object in order to create a JSON ruleset, or it can be applied to an existing JSON ruleset to mod it.
There are four types of data in MahjongScript:
- JSON: numbers, strings, arrays, and objects
- Conditions: conditions with
and,or,not - Actions: function calls or
do-blocks containing actions - Sigils: set specifications, match specifications
The entirety of a MahjongScript file is a list of top-level commands that use these data in some way. A list of commands is provided at the bottom.
The following values are JSON:
- Numbers:
42,1.5 - Strings:
"example" - Arrays:
["points", 1000] - Objects:
%{points: 1000}
Note that objects differ from typical JSON syntax {"points": 1000}. This is a consequence of this language being based on Elixir's map syntax. However, this also means you can declare multiline strings:
%{str: """
This is my multiline string
The indents before these lines are removed
since the ending quotes below are indented too
"""}
In addition, there is a concept of a numeric "amount". An amount is either a number or one of the following strings which translate into some amount:
- the name of a counter
"tiles_in_wall""num_discards""num_aside""num_facedown_tiles""num_facedown_tiles_others""half_score""dice""pot""honba""riichi_value""honba_value""points""points2""score""minipoints"
Any condition in the supported list of conditions can be written like
(our_turn and has_score(1000)) or has_score(0, as: "shimocha")This compiles down to the equivalent bracket-ridden JSON
[[["our_turn", {"name": "has_score", "opts": [1000]}], {"name": "has_score", "as": "shimocha", "opts": [0]}]]You can also write comparisons where the left side is a counter name and the right side is an amount. For example:
"my_score" >= 100
compiles to {"name": "counter_at_least", "opts": ["my_score", 100]}.
Supported comparisons are ==, !=, <, >, <=, >=.
There are special forms at_least and at_most which can be used like this:
at_least(2, has_existing_yaku("Riichi"), has_existing_yaku("Tanyao"), has_existing_yaku("Pinfu"))
This is true when at least 2 of the given conditions are true. In general, at_least(n, cond1, cond2, ...) is true when at least n of the given conditions are true.
There is also at_most(n, cond1, cond2, ...) which is similar: it is true when no more than n of the given conditions are true, AND at least one of the conditions is true. You can bypass the second check by specifying true as one of the conditions, since then one of the conditions is trivially true.
Actions are always seen in do-blocks. For example you can define a function with a do-block:
def myfun do
put_down_riichi_stick
add_score(-1000)
endThis compiles to the JQ
.functions.myfun = [
["put_down_riichi_stick"]
["add_score", -1000]
]You can also write the above as a one-liner using semicolons to separate lines:
def myfun do put_down_riichi_stick; add_score(-1000) endThis works for all do-blocks.
You may use any action in the supported list of actions. In addition, there is shorthand for some often-used actions:
def myfun2 do
# if-blocks compile down to "when" actions
if true do
push_message("Hello world!")
end
# if-else-blocks compile down to "ite" actions
if no_tiles_remaining do
ryuukyoku
else
draw
end
# unless-blocks compile down to "unless" actions
unless no_tiles_remaining do
draw(1, "opposite_end")
end
# cond-blocks compile down to a string of "ite" actions
cond do
"x" == 1 -> push_message("case 1, checked first")
"x" == 2 -> push_message("case 2, checked second")
"x" == 3 ->
push_message("case 3")
push_message("still case 3")
true -> push_message("fallback case")
end
# as-blocks compile to "as" actions
as everyone do
push_message("says hi")
end
# you can also use as: (like in conditions)
push_message("says hi", as: "everyone")
# this becomes ["set_counter", "counter_name", "score"]
counter_name = "score"
# this becomes ["add_counter", "counter_name", 1]
counter_name += 1
endIn addition, you may call any user-defined function:
def myfun3 do
myfun
myfun2(val: 1)
endThis compiles to [["run", "myfun"], ["run", "myfun2", {"val": 1}]]
To make a function take parameters like this, simply use the parameter e.g. "$myparam" somewhere in the function. Then if a parameter myparam: 100 is provided, it will replace all instances of "$myparam" with 100 before running the function.
def myfun3 do
push_message("has a score of $score!", %{"score": "$myscore"})
end
# elsewhere:
def myfun4 do
my_score = "score"
myfun3(score: "my_score")
endSigils look like ~s"mysigil". They're just specially marked strings that expect some special syntax.
In particular, ~s specifies a set to be used, for example, in the define_set command.
define_set myset, ~s"0 1 2"This compiles to
.set_definitions["myset"] = [[0, 1, 2]]A more complex set using all aspects of the set grammar is the following:
define_set myset, ~s"0 1 2@myattr&myattr2 | 1z 2z 3z"This compiles to
.set_definitions["myset"] = [[0, 1, {"offset": 2, "attrs": ["myattr", "myattr2"]}], ["1z", "2z", "3z"]]Sets are mostly used in match definitions, but they are also extensively used in fu calculations.
~m specifies a match definition. The most common use for these is in use in the match condition:
match(["hand", "calls", "winning_tile"], ~m"exhaustive, iipeikou:1, mentsu:2, pair:1")This compiles to
{"name": "match", "opts": [
["hand", "calls", "winning_tile"],
[["exhaustive", [["iipeikou"], 1], [["mentsu"], 2], [["pair"], 2]]]
]}Another example:
match(["hand", "calls", "winning_tile"], ~m"(haku hatsu chun):2, (haku_pair hatsu_pair chun_pair):1 | (haku hatsu chun):3")This compiles to
{"name": "match", "opts": [
["hand", "calls", "winning_tile"],
[
[[["haku", "hatsu", "chun"], 2], [["haku_pair", "hatsu_pair", "chun_pair"], 1]],
[[["haku", "hatsu", "chun"], 3]]
]
]}~t"111m222p333s@attribute" and ~T"11m 12m 13m@attribute" are both ways to specify tiles. The lowercase ~t lets you specify an array of tiles using the standard mahjong compact format. The uppercase ~t lets you specify any of Riichi Advanced's extended tiles (see tiles.md).
~t"111m222p333s@attribute"
# compiles to [
# "1m","1m","1m","2p","2p","2p",
# {tile: "3s", attrs: ["attribute"]},
# {tile: "3s", attrs: ["attribute"]},
# {tile: "3s", attrs: ["attribute"]}
# ]
~T"11m 12m 13m@attr1&attr2"
# compiles to [
# "11m", "12m",
# {tile: "13m", attrs: ["attr1", "attr2"]}
# ]Tile sigils are basically only used for interpolation into rules text and for rigging the hand/wall. In the future it will also be used to interpolate into messages.
on after_initialization do
add_rule("2 Han", "Honitsu", "%{example_hand}", %{example_hand: ~t"123345888p11z22z 2z"})
end
set starting_hand, %{east: ~t"19m19p19s1234567z"}
set starting_draws, ~T"45m@_rainbow&_anim&_dora"Consider the following snippet from the Sichuan Bloody ruleset (sichuan.majs):
define_button pon,
...
call_conditions:
(status("void_manzu") and not_call_contains(["1m", "2m", "3m", "4m", "5m", "6m", "7m", "8m", "9m"], 1))
or (status("void_pinzu") and not_call_contains(["1p", "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p"], 1))
or (status("void_souzu") and not_call_contains(["1s", "2s", "3s", "4s", "5s", "6s", "7s", "8s", "9s"], 1)),
...
define_button daiminkan,
...
call_conditions:
(status("void_manzu") and not_call_contains(["1m", "2m", "3m", "4m", "5m", "6m", "7m", "8m", "9m"], 1))
or (status("void_pinzu") and not_call_contains(["1p", "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p"], 1))
or (status("void_souzu") and not_call_contains(["1s", "2s", "3s", "4s", "5s", "6s", "7s", "8s", "9s"], 1)),
...
define_button ankan,
...
call_conditions:
(status("void_manzu") and not_call_contains(["1m", "2m", "3m", "4m", "5m", "6m", "7m", "8m", "9m"], 1))
or (status("void_pinzu") and not_call_contains(["1p", "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p"], 1))
or (status("void_souzu") and not_call_contains(["1s", "2s", "3s", "4s", "5s", "6s", "7s", "8s", "9s"], 1)),
...
define_button kakan,
...
call_conditions:
(status("void_manzu") and not_call_contains(["1m", "2m", "3m", "4m", "5m", "6m", "7m", "8m", "9m"], 1))
or (status("void_pinzu") and not_call_contains(["1p", "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p"], 1))
or (status("void_souzu") and not_call_contains(["1s", "2s", "3s", "4s", "5s", "6s", "7s", "8s", "9s"], 1)),
...To avoid repeating code, the command define_const can be used to define constants. Constants can hold any MahjongScript data type, including do-blocks of actions! So the above becomes:
define_const no_voided_calls,
(status("void_manzu") and not_call_contains(["1m", "2m", "3m", "4m", "5m", "6m", "7m", "8m", "9m"], 1))
or (status("void_pinzu") and not_call_contains(["1p", "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p"], 1))
or (status("void_souzu") and not_call_contains(["1s", "2s", "3s", "4s", "5s", "6s", "7s", "8s", "9s"], 1))
define_button pon,
...
call_conditions: @no_voided_calls,
...
define_button daiminkan,
...
call_conditions: @no_voided_calls,
...
define_button ankan,
...
call_conditions: @no_voided_calls,
...
define_button kakan,
...
call_conditions: @no_voided_calls,
...Constants are referenced with the @ symbol. The underlying mechanism is that each reference to a constant is replaced by the string "@my_constant", and the engine walks through the entire JSON, replacing instances of "@my_constant" with its corresponding value.
When writing mods, you can extend the item at the path "constants.my_constant" using the apply command, for example:
# from the ruleset
define_const always_yakuhai, ["5z", "6z", "7z"]
# from the mod
apply append, "constants.always_yakuhai", "4z"Since constants are expanded at runtime, one way to configure behavior is to put your configurable condition or action into a constant, and have mods modify or replace the constant. For instance:
define_const closed_hand, has_no_call_named("chii", "pon", "daiminkan", "kakan")
# but in cosmic riichi:
define_const closed_hand, has_no_call_named("ton", "chii", "chon", "chon_honors", "daiminfuun", "pon", "daiminkan", "kapon", "kakakan", "kafuun", "kakan")
# all instances of @closed_hand will now use the latest definitionFor security reasons, constant names cannot contain uppercase letters.
Consider the following:
define_const can_chankan, status("can_chankan", as: "caller")
define_button chankan,
display_name: "Ron",
show_when: not_our_turn
and someone_else_just_called
and status_missing("furiten", "just_reached")
and +@can_chankan
and match(["hand", "calls"], ["tenpai"])
and match(["hand", "calls", "last_called_tile"], ["win"]),
...The idea is that one of the conditions in show_when has been factored out as a constant, so that the Kokushi Ankan Chankan mod can just modify that constant, instead of modifying every button. Since conditions are internally represented by a list [cond1, cond2, ...], the constant @can_chankan is actually internally an array [{"name": "status", "as": "caller", "opts": ["can_chankan"]}]. Replacing it directly would result in something like [cond1, cond2, [{"name": "status", "as": "caller", "opts": ["can_chankan"]}], ...] -- turning it into an OR condition, which may be undesirable.
The prefix +@ solves this by "splatting" the constant. That is, whenever the splatted constant reference appears inside an array, it injects its contents directly into the surrounding array.
You can splat conditions and even do-blocks, since both are represented internally as arrays.
The user of a .majs mod (e.g. a ruleset) can pass variables into it. For instance, the Tobi mod accepts a below variable specifying the minimum score a player can have.
Variables are referenced by prepending with !. Here's the Tobi mod:
apply set, "score_calculation.tobi", !belowFor security reasons, variables cannot contain uppercase letters.
You can also write conditionals at the top-level, for example:
if !min == "Mangan" do
...
endThis runs the commands inside the if the variable min is set to "Mangan". You can also do if-else-end and unless-end, but not cond. The allowable operators within the condition itself are as follows:
- Any variable
!variable l == rorequals(l, r): Check equalityl in r: Check iflis an element of arrayrnot cond: Logical NOT (negatescond)l and r: Logical AND (true ifl,rare both true)l or r: Logical OR (true if one ofl,ris true)true: Always truefalse: Always falsedefined("foo"): See below
This is meant to be used when multiple mods define the same things, but you only want it to be defined once. Here's the basic example:
unless defined("pao") do
define "pao"
on before_win do
...
end
...
endEssentially this means that only the first mod that hits this top-level conditional will evaluate the commands inside of it, since "pao" will be set thereafter.
These defines only work for MahjongScript mods, so if you put defines in a majs ruleset, they will not transfer to mods. If you want to condition mod evaluation based on stuff in a ruleset, you will probably want to define a variable instead.
A reference cheatsheet for all of the following commands appears at the bottom of this page.
def my_function_name do
action1
action2
if condition do
action3
else
action4
end
unless condition2 do
action5
end
as everyone do
action6
end
endThe above showcases all the special forms: if/do/end and if/do/else/end is are typical conditionals, unless is like if but inverts its condition, and as lets you switch the current player (and sort of serves as a loop, if multiple players are specified).
set initial_points, 25000To set (or otherwise modify) arbitrary paths, see apply below.
# append to list of existing handlers
on before_win do
actions
end
# prepend to list of existing handlers
on before_win, prepend: true do
actions
endon can be used to append or prepend to the list of existing handlers. In the above example, the second handler will run before the first handler.
define_set pair ~s"0 0"This command can only take set sigils (see above).
Defined sets are used in match sigils.
define_match mymatch1, ~m"pair:7"
define_match mymatch2, ~a"FF XXXX0a NEWS XXXX0b"
define_match mymatch3, "existing_match_1", "existing_match_2"This command can only take match sigils (see above). You may specify multiple match sigils, separated by commas -- it will act as an OR of the given matches.
define_match mymatch1, ~m"pair:7", ~a"FF XXXX0a NEWS XXXX0b"After defining them, you may use these match definitions by referencing them in the match condition:
# mymatch1 OR mymatch2
match(["hand", "calls", "winning_tile"], ["mymatch1", "mymatch2"])Note that you can directly pass in a match sigil to match, so define_match is simply a convenience command.
match(["hand", "calls", "winning_tile"], ~m"pair:7")Same syntax and function as define_match, but if the match exists, it will extend the match with the given match definitions.
define_match mymatch, ~m"mentsu:4, pair:1"
extend_match mymatch, ~m"pair:7"is the same as
define_match mymatch, ~m"mentsu:4, pair:1", ~m"pair:7"# arbitrary json
define_const foo, "asdf"
def bar do
print(@foo) # prints "asdf"
end
# sigils
define_const bar, ~m"pair:7"
# conditions
define_const bar, match(["hand", "calls", "winning_tile"], ~m"pair:7")
# do-blocks
define_const baz do # note the lack of comma before do
action1
action2
endSee the explanation of constants above.
define_yaku list_name, display_name, value, conditionIf you define a yaku with the same display_name of an existing yaku, then obtaining both yaku adds the value of both yakus.
After the condition you may optionally specify a list of yaku names as shorthand for the below:
define_yaku list_name, display_name, value, condition, supercedes_list
# is the same as
define_yaku list_name, display_name, value, condition
define_yaku_precedence display_name, supercedes_listdefine_yaku_precedence "Daisangen", ["Shousangen", "Haku", "Hatsu", "Chun"]
define_yaku_precedence "Renhou", [1,2,3,4]You may specify a list of yaku display_names that the given yaku overrides. This means whenever the given yaku on the left is awarded, it erases all of the overridden yaku on the right. You can also have it override itself, effectively making it so the yaku only exists to override other yaku.
You can also specify a list of numbers -- this specifies all yaku of a given value. You can also mix display_names and numbers.
remove_yaku yaku_list, name
remove_yaku yaku_list, [name1, name2]replace_yaku list_name, display_name, value, condition, optional_supercedes_listThis is essentially the same as remove_yaku display_name followed by define_yaku, except it will do nothing if the yaku doesn't exist in the first place.
define_button id,
display_name: display_name,
show_when: condition,
precedence_over: list_of_ids,
unskippable: false,
cancellable: false
do
actions
endNote that any existing button of the same id will be overwritten. See the button documentation in the main documentation.
define_auto_button id,
display_name: display_name,
desc: string,
enabled_at_start: false
do
actions
endNote that any existing auto button of the same id will be overwritten. See the auto buttons documentation in the main documentation.
# append a mod category to the list
define_mod_category "Other"
# prepend a mod category to the list
define_mod_category "Rules", prepend: trueYou can have multiple instances of a category, but this is largely useless since define_mod below only adds to the first instance.
See mods.md to see how this all works.
define_mod id,
name: string,
desc: string,
default: false,
order: 0,
deps: list of ids,
conflicts: list of ids
category: nameNote that if category is not specified, the mod is simply appended to the end of the mod list.
See mods.md to see how this all works.
config_mod id,
name: config name,
values: ["Mangan", "Yakuman"],
default: "Yakuman"See mods.md to see how this all works.
remove_mod id
remove_mod id1, id2, id2Here's how it's used for kuikae:
define_play_restriction "any", just_called and last_called_tile_matches("kuikae")After just declaring riichi, this prevents playing any tile that gets you out of tenpai:
define_play_restriction "any", status("riichi") and status_missing("just_reached") and not_is_drawn_tile
define_play_restriction "any", status("riichi", "just_reached") and needed_for_hand("tenpai")This example forbids discarding flowers when you have the status "cannot_discard_flowers":
define_play_restriction "flower", status("cannot_discard_flowers")define_play_effect works similarly to an event handler. It looks like
define_play_effect flower do
push_message("played a flower")
end
The argument flower is a tile spec that restricts which kinds of tiles to run on, in this case flower tiles. If you want this to run on every tile, specify any. See the documentation on tile specs for all other options.
# add to an existing path
apply add, "initial_points", 5000
# set values at arbitrary paths
apply set, "score_calculation.tenpairenchan", true
# append to a function
apply append, "functions.myfunc" do
as everyone do
add_score(1000)
end
endThe allowed methods for apply <method> are:
"set": set the value at the given path."initialize": set the value at the given path, but only if it doesn't exist"add": add a numeric value."prepend": prepend an element or an array to an array."append": append an element or an array to an array, or create the array if it doesn't exist."merge": merge an object to an existing object."subtract": subtract a numeric value."delete": remove an element or an array of elements from an array."multiply": multiply a numeric value."deep_merge": merge an object to an existing object, and merge shared keys recursively."divide": divide a numeric value."modulo": modulo a numeric value."delete_key": remove a key or an array of keys from an object. (specify keys as strings)- any of the following C binary numeric operations theoretically work too:
"atan2", "copysign", "drem", "fdim", "fmax", "fmin", "fmod", "frexp", "hypot", "jn", "ldexp", "modf", "nextafter", "nexttoward", "pow", "remainder", "scalb", "scalbln", "yn"
If the parent node for the given path doesn't exist, the command does nothing (with the exception of apply initialize or apply set, in which case the path is created). You can also make an exception for this by prepending set_ to the method, such as apply set_append -- this will append but default to set if the path doesn't exist.
The path syntax is simple: it's .key to access a key and [0] to access the first element of an array. e.g. toplevel_key.some_key[1].
replace all, "", ["pon"], ["pon", "daiminkan", "kakan"]
replace all, "available_mods",
%{type: "dropdown", name: "below", values: [0, 1, 1000, 1001]},
%{type: "dropdown", name: "below", values: [0, 1, 1000, 1001], default: 1}Essentially: given a path and two values from and to, look at all subnodes of the given path and replace all instances of from with to.
define_preset "Mahjong Soul", [
"riichi_kan",
%{name: "honba", config: %{value: 100}},
%{name: "yaku/riichi", config: %{bet: 1000, drawless: false}},
%{name: "nagashi", config: %{is: "Mangan"}},
%{name: "tobi", config: %{below: 0}},
%{name: "uma", config: %{_1st: 10, _2nd: 5, _3rd: -5, _4th: -10}},
"agarirenchan",
...
]Here is a cheatsheet for all the commands that exist.
Defining actions
def fn_name do <actions> end: Defines a new callable functionfn_namewith actions<actions>, or overwrites an existing one. You may then usefn_nameas an action to call it. Later mods can extend this function viaapply append, "functions.fn_name" do <actions> end.on before_win do <actions> end: Defines a new handler for the eventbefore_win. This means<actions>will run after all existing event handlers.on before_win, prepend: true do <actions> end: Same, but<actions>run before all existing event handlers.
define_play_effect <tile_spec> do <actions> end: Defines<actions>to run immediately after playing a tile matching the tile spec<tile_spec>. List of all tile specs.
Setting variables
set win_timer, 30: Sets the top-level JSON keywin_timerto a specific value (30).define_set shuntsu, ~s"0 1 2": Definesshuntsuto refer to the set0 1 2. This allows any match specification to mentionshuntsu, e.g.~m"shuntsu:4"define_match win, ~m"(shuntsu koutsu):4 pair:1": Defineswinas the match specification given by<match>. Note that AI bots will use the match definition namedwinwhen making decisions.define_const always_yakuhai, ["5z", "6z", "7z"]: Define the constant@always_yakuhai. At load time, all instances of@always_yakuhaiwill be replaced with["5z", "6z", "7z"], and all instances of+@always_yakuhaiin an array will insert the elements"5z", "6z", "7z"at that spot in the containing array. Later mods can change the final value of a constant viaapply append, "constants.always_yakuhai", "4z".replace all, ".wall", "5z", "0z": Replace all instances of"5z"with"0z"under the given path"wall".
Yaku
define_yaku_precedence "Honroutou", ["Chanta", "Junchan"]: Makes it so having the yaku"Honroutou"makes you ineligible for"Chanta"and"Junchan". You can also supply point values in the array (e.g.1to invalidate all 1-point yaku) or a yaku list like"yaku"(to invalidate all yaku in the yaku listyaku).define_yaku yaku, "Houtei", [1, "Han"], no_tiles_remaining, ["Haitei"]: Define a yaku named"Houtei"to be inserted into the yaku list namedyaku. This awards value[1, "Han"]if the winning hand fulfills the conditionno_tiles_remaining. Other allowed values include1and[1, "Han", 1, "⛁"]. The final argument["Haitei"]is optional and defines a list of yaku this yaku supercedes. It is basically shorthand fordefine_yaku_precedence "Houtei", ["Haitei"]. You maydefine_yakumultiple instances of a given yaku name: if more than one yaku of the same name are awarded, their values are added together. On the winning screen, yaku are displayed in the order they are defined.remove_yaku "yaku", ["Chanta", "Junchan"]: Remove the yaku"Chanta"and"Junchan"from the yaku list namedyaku. You can also supply a single string to remove one yaku.replace_yaku yaku, "Haitei", [1, "Han"], no_tiles_remaining and won_by_draw, ["Houtei"]: This replaces every existing instance of the yaku"Haitei"in the yaku listyakuwith a single new definition. The syntax is identical todefine_yaku.
Buttons
-
define_button chii, display_name: "Chii", show_when: not_our_turn and not_no_tiles_remaining and kamicha_discarded and call_available, precedence_over: ["chii"], call: [[-2, -1], [-1, 1], [1, 2]], call_style: %{ kamicha: ["call_sideways", 0, 1], toimen: [0, "call_sideways", 1], shimocha: [0, 1, "call_sideways"] } do big_text("Chii") call change_turn("self") end
This defines a new button with id
chiiand display name"Chii". The only necessary fields aredisplay_nameandshow_whenas well as the finaldo-block of actions. See the relevant documentation for more information about the other fields. -
define_auto_button _1_auto_sort, display_name: "A", desc: "Automatically sort your hand.", enabled_at_start: true do sort_hand end
This defines a new auto button (bottom left toggles). See the relevant documentation for more information.
Mod management
define_mod shiro_pocchi, name: "Shiro Pocchi", desc: "One of the white dragons is shiro pocchi. Shiro pocchi acts a joker tile when drawn while in riichi." default: false, order: 2, deps: ["yaku/riichi"], conflicts: ["no_honors", "chinitsu"], category: "Other"
Mods are contained in the directory `priv/static/mods`, and the id you supply (here `shiro_pocchi`) should be one of those. The only required fields are `name` and `desc`. See [mods.md](./mods.md) for more info.
- `define_mod_category "Other"`: This simply adds a new header to the mods list. If any mods defined thereafter don't define a `category` field, it will default to the last-added category.
- `config_mod honba, name: "value", values: [100, 500, 1000]`: This adds a dropdown to the `honba` mod when enabled, with name `value` and allowable dropdown options `100`, `500`, and `1000`. The resulting value is accessible in the mod itself (a MahjongScript file) as the variable `!value`.
- `remove_mod honba`: Removes the `honba` mod from the mod list.
- ```elixir
define_preset "Mahjong Soul", [
%{name: "honba", config: %{value: 100}},
%{name: "yaku/riichi", config: %{bet: 1000, drawless: false}},
...
"first_gets_riichi_sticks"
]
This defines a set of mods (a modpack) which can be seen in the game settings menu. See the Riichi ruleset for examples. A mod is either just its id, or the structure %{name: id, config: %{config_value: "value"}} if the mod has config values.
Other
For these two, please see the relevant section.
define_play_restriction: Prevent certain tiles from being played depending on conditions. Example:define_play_restriction "flower", !unskippableapply: Directly modify the underlying JSON in some way. Example:apply append, "constants.always_yakuhai", "4z"