-
Notifications
You must be signed in to change notification settings - Fork 1
LogicLanguage
This project defines a custom language that is used to specify game modes in a compact and easy way. This language abstracts all the implementation details of the server and is easier portable and understand.
For tooling is a VS code extension provided that should be automatically loaded
if this project is opened in this editor. If not, open your extension browser
and search for werewolf-logic. This extension is only available in this
project and not yet published. This extension does only provide syntax
highlighting.
There is a tool located in /tools/LogicCompiler that reads the written logic,
parses, validates and generates target source code that can then be compiled and
used by the server. This tool has a lot of checks build in.
Each game mode is located in their own directory in /logic. In this directory
you can have one or more *.w5logic files and it is also allowed to use nested
directories. You have to define at least one mode in one of these files.
A file contains any number of nodes. Each node has their own format and usage which is described below. Nodes are introduced with their keyword and then their name.
mode TestMode {
// code
}
Almost all nodes (except the option node) can inherit from one or more other
nodes. The code is later merged into one node in the order of their appearance
in the inheritance list. You can also specify a node as abstract if you only
want to use this node to inherit from. Be careful! Abstract nodes cannot be used
anywhere else in the code! Also does not inheritance create any relation - only
the code is copied!
abstract character BaseChar1 {}
abstract character BaseChar2 {}
character Detailed : BaseChar1, BaseChar2 {}
The name of the game node must only be unique for the specific type. For
example, you can have a scene named Villager and a character named Villager.
But it is not allowed to have two character with the same name.
Below is only each node explained. Most nodes can have custom functions
attached. The description on what functions are available is located in the auto
generated documentation Node-Methods.md.
Defines a game mode that will be shown in the UI. It also defines a list on all character types and win conditions that are available. The order of each listing is also the order that will be presented in the UI or will be checked each time.
mode MyGameMode {
// A list of just the supported character names. There is no type prefix!
character {
Char1, Char2
}
// A list of all win conditions. There is no type prefix!
win {
Condition1, Condition2
}
}
The main game loop is split into several phases. A starting phase is defined (it is only allowed to have exactly one starting phase - all other outcomes will result in an error and the game wont start) and each phase defines the next phase dynamically. This can be used to define e.g. the day/night cycle.
Each phase setups the game environment for the player like background image or color scheme.
phase Night {
func start { true }
func next { phase Day }
func background { "/content/night.png" }
func theme { "#33aa88" }
}
Each phase consists of at least one scene and they have their chronological order implicitely defined. This means, you only have to defined which scenes has to be used somewhere before or after other scenes. The compiler will read this information and put them all in a linear order and complains if your specification contain loops.
Each scene can decide on its own if they should be executed with the current game state or just be skipped. A scene that doesn't create new content like a voting or a sequence is immediately be closed and the next one is started if the game settings says so.
scene SceneD {
phase Day;
after { SceneA } // SceneD should be somewhere after SceneA
before { SceneF, SceneG } // SceneD should be somewhere after SceneF and SceneG
}
If a scene has no active content (a voting or a sequence) and the game rule
AutoFinishRounds is enabled, it will automatically be finished and the next
scene will be started. If this was the last scene in the current phase, a new
phase will be started and so on. If for some reasons the same scene should be
started again and still has no active content, the game is stopped. There is no
winner and the scoreboard is not updated. It is not recommended to do this.
A label is a single piece of information that can be assigned to the current
game or to any character, phase, scene or voting. You will most likely only
check its presence and change or apply behavior depending on it. The target is
specified using the target { <list of allowed targets> } specification with
the following keywords:
-
mode: The current game. Consists through the entire game -
character: Any character. Can be shown to the user in the UI. -
phase: Any phase. Will be discarded after the current phase is over. -
scene: Any scene. Will be discarded after the current scene is over. -
voting: Any voting. Will be discared after the voting is finished.
A label can have multiple allowed targets. If no targets are defined, the label cannot be assigned to any target.
At most times you will only check for a label by its name but you can also apply
custom data to it to provide a scope. This is done using the
with <type> <name>; specification and as types are only character, string,
int and bool allowed.
Two label are considered as equal if they have the same name and if all the custom data is set to equal values.
label HasHealPotion {
target { character }
}
label IsInLove {
target { character }
with character crush;
}
A character is a role that is assigned to a single player. This role can be hidden from other player but is always visible to the current player and the game master (if he doesn't participate in the game directly).
The game master can specify in the lobby settings how many of which character should exists in the current game and at start they will be randomly assigned to the player.
A character always starts in the enabled state but can be disabled in the game.
A player with a disabled character is not allowed to do anything in the game
(e.g. perform a vote or chat) and will be omitted in the @character
collection. A disabled character is also marked as such in the UI. You can use
this, to for example mark the current player as "killed" or being at a distant
place.
There can be no further information assigned to a character directly and you have to use labels to specify current states.
character Wolf {
func view {
if $viewer == character Wolf {
character Wolf
} else {
character Unknown
}
}
}
A voting is the main interaction between the player and the game itself. The game displays them a voting with one or more options and they can decide for one or more options. Depending on the voting result can the game perform changes to the current game state.
If a scene doesn't contain any voting or active sequence, it will automatically be finished and the next one will be started (can be changed using a game rule).
They are three different voting modes depending on the optional target rule:
-
all: (Default). A single voting is displayed to all recipients and they can vote for one option. The option with the most votes will be used as the result and the handlerchoicewill be called with it. If multiple options has the most votes the handlerunanimouswill be called to handle such cases. -
each: The functionfunc targetswill be evaluated and for each player will be a new voting created. The player can vote for one option but the result wont interefer with other players. The handlerchoicewill be executed with the result. If there was a timeout and the player hasn't selected an option the handlerunanimouswill be called. -
multi_each: The same aseachbut the player can select multiple options and deselect one if voting for them again. A system option (you don't need to add it manually) is displayed to mark this voting as finished and to start the handlerunanimous. There is no handlerchoicein this case.
A voting can be shown to only specific players and also define if a player is allowed to vote for it. A game master with no character assigned can always see all votings but cannot vote for any of them.
voting AreYouHappy {
target all;
func voting_option {
{ option Yes, option AlsoYes }
}
}
An option is displayed in a voting and a player can vote for it. You can also use any existing character instance and it will automatically converted into a system option for that character.
option Yes;
Specifies a win condition that can be checked at certain points in the game. That are:
- Before the start of each scene
- After a voting is finished
Every win condition has to be registered in the game mode to be active.
If a win condition determines that a game is now finished, it stops the game and present the set of winning player in the UI. There is no further code executed after this point for the current game.
They are two modes for a win condition depending on what functions you define:
- You can define
func checkto check if the current game is finished andfunc winnerto return the collection of all characters that won the game. This allows to stop a game and have no winner! - You can define
func has_oneto return a collection of potential winner. If this collection is not empty, the game is stopped and the list of characters are assigned as the winner. If the returned collection is empty, it will be handled as if the game is not finished.
win VillagerWin {
func check {
empty(@character | has(character Wolf))
}
func winner {
@character
}
}
A sequence contains a list of actions that will be executed one after another. You can think of it as a list of scenes but they can indefinitely recursively executed. Only when a step is finished the next step of a sequence will be executed. When a sequence is finished it can continue the parent sequence. A scene can only be continued if it has no active sequences!
A sequence is always attached to a single character and all steps can access it
using the $target variable.
sequence HunterIsKilled {
step vote {
spawn voting HunterKill with targets({$target});
}
step die {
enabled($target, false);
}
}
Be careful when inheriting sequences! All inherited steps are executed first and in the order of appearance of the inheritance list. If two steps share the same name, the later defined step is merged into the first one and the later code is appended to the first one.
There is currently no limit on how many sequences you can nest. There is also no check if the same sequence is called for the same character twice. This can lead to an overflow if you write code that create infinite sequences.
If you want to have random events in your game you can mimic this using a handler that selects one label out of a big list and applies it to the game. For each scene or phase you have then to check if such a label exists and executes actions depending on. This works well for a small list of possible events but doesn't scale well.
An event node is simpler by grouping all the related code together and applying actions only if the event is enabled if the desired phases or scenes are entered. And the event is automatically removed if it decides if it is the right time.
They are three ways to specify a hook to execute code at a specific point in time:
-
target now {...}: The code will be executed right at the time when the event was triggered and then never again. -
target phase <name> {...}: The code will be executed everytime the phase<name>was entered. This will be executed after the phasesstarthandler. -
target scene <name> {...}: The code will be executed everytime the scene<name>was entered. This will be executed after the scenesstarthandler.
There will be no event handler triggered if a scene is not entered. This can be for one of the following reasons:
- The scene itself is not included in the scene list.
- The scene itself decided it is not the right time to be executed.
- The scene didn't generate any active content (a voting or a sequence) and the
game rule
AutoFinishRoundsis set.
There is currently only one way to spawn a new (random) event that is defined in the same game mode:
spawn event *;
Currently, there is no way to spawn specific events.
Each node can have a list of functions attached to it. What functions are
allowed or required can be seen in the auto generated documentation
Node-Methods.md.
Some functions can have a return value and the last statement in the block is used to for this.
func without_return {
foo() // there is no value returned
}
func with_return {
foo() // the return value of foo() is actually used for this function
}
A function consists of multiple statements and each can operate with the current game state. All statements have a return type and some statements can contain other.
Some statements are grouped together as expressions to highlight the difference in their usage. All expressions are statements but not all statements are expressions.
All statements that are not an expression do return the type Void.
Spawns a new voting of the provided type.
spawn voting NameOfVoting;
This statement can be extended to further specify how the voting should be spawned.
-
with choices(<choices_col>): Overwrites the list of options with the specified collection. -
with eligable(<character_col>): Overwrites who is eligable to vote. -
with viewer(<character_col>): Overwrites who is allowed to see this voting. -
with targets(<character_col>): Overwrites the targets of this voting (only if the voting is set to a multi-voting).
Overwriting a collections does also result in, that the original handler of the voting are never called.
An extended statement can look like this:
spawn voting NameOfVoting with targets({$target}) with choices($choices);
Spawns a new sequence and handle its current cycle.
spawn sequence NameOfSequence($target);
$target is the character for which the sequence should be generated.
Spawns a random event.
spawn event *;
Sends a notification to all user. For game state updates is this not needed, as it will automatically be handled in the runtime.
notify NotificationID; // 1
notify "NotificationID"; // 2
let $notificationId = "NotificationID";
notify $notificationId; // 3
All of the statements above will send the same notification ID to all user. The id is later looked up in the language file and a message will be shown.
You can also specify a sequence name that will be looked up in the language file. This allows to have custom notification texts for each sequence.
notify sequence SequenceName NotificationID;
Sends a notification with a character list associated. This is used to highlight that something has been changed for that set of characters.
notify character NotificationId($characters);
A local variable that can be used for later is assigned like this:
let $name = value_expression();
The variable is only valid in the current scope and destroyed after leaving it.
You cannot use a $name that is already used in the current scope with one
exception: If the former variable was also created by a let-statement (not a
if-let-expression or for-let-expression) and has the same identical type. In
such cases is the previous value of the variable overwritten but the variable
remains in the same scope as before. In general you should not try to overwrite
a value and keep it to a minimum.
The pipe expression | takes the collection to the left and applies it through
the pipeable function on the right. This allows to filter collections and reduce
them to the elements requested. The return type depends on the function on the
right.
@character
| has_character(character Wolf)
It is allowed to filter multiple times using the pipe operator.
@character
| has(label A)
| has(label B)
Compares the value with the left and the right and returns a boolean value with the result. Only the following operators are supported:
-
==: Check if both sides are equal -
!=: Check if both sides are unequal -
<: Check if left is smaller than right -
<=: Check if left is smaller or equal than right -
>: CHeck if left is greater than right -
>=: CHeck if left is greater or equal than right
5 < 3 // returns false
Both sides are expected to have the same type. For == and != is an
additional use case: If one of both sides has a type name and the other one an
instance of the corresponding type, it checks if the instance is indeed of that
type.
$target == character Wolf // returns true if $target is indeed a Wolf character
Boolean operators take in boolean values and return boolean values. The following boolean operators are supported:
-
left || right: or-operation: returns true if one of both sides are true -
left && right: and-operation: returns true if both sides are true -
!value: negation: returns true if value is false
Integer operators take in two integer values and return a new one. The following integer operators are supported:
-
left + right: addition -
left - right: subtraction -
left * right: multiplication -
left / right: division
Keep in mind that a division by zero is forbidden and will crash the code! It might be possible that a fallback method will be implemented to handle such cases.
You can generate a new collection by listing the elements and surround them with
curly braces { ... }. More Informations at the chapter Collections.
An if expression will execute a code block depending on a condition and optionally execute another code block if this condition is not fullfilled.
if length($list) < 5 {
// do something when $list has fewer than 5 elements
} else {
// do something when $list has 5 or more elements
}
The if expression can return the value from the last statement in each block.
add($target, if $isGood { label Good } else { label Bad });
If one of both branches return a Void type, then will this if expression also return a Void type. The else branch is optional if no return value is expected and the if expression will then always return a Void type.
If no return type from the if statement is expected, then no value will be returned at all.
This expression is similar to an if expression but allows to unpack an optional value and execute code depending on the existence of the value.
if let $char = $character {
// execute if $char exists
} else {
// execute if $char doesn't exists
}
In the success block is a new variable introduced that is defined right after
the let keyword (in this example $char).
You can use $_ as variable name to signify that no variable should be created
and the real value of the input can be ignored. This is usefull to mute warnings
of unused variables.
This expression loops over all elements in the provided collection and executes the code for each element.
for let $char = @character {
// handle each $char
}
If a return value is expected, a new collection with all the results of all the last statements for each element of the original collection will be generated.
// returns { 2, 4, 6 } in random order
shuffle(for let $x = { 1, 2, 3 } { $x * 2 })
You can use $_ as variable name to signify that no variable should be created
and the real value of the input can be ignored. This is usefull to mute warnings
of unused variables.
They are some global collections defined that are always available. The full
list is documented in the auto generated documentation Functions.md.
Any variable starts with a $ and is only allowed to have upper and lowercase
letters. Numbers and underscores are only allowed if its not the first character
after the $ sign.
You can use normal integers and strings in code. Strings have the same escaping rules as C# strings.
They are a list of predefined functions available that can just be called. A
full list is available in the auto generated documentation Functions.md.
rand(5) // returns a number between 0 and 4
It is not possible to add own functions.
You can reference any predefined node type using the type of the node and the name of it.
mode MyGame // references to the game mode MyGame
character Wolf // references to the character Wolf
You can use true and false normally and they will be used as boolean values.
For all other names is a node searched with the same name and the type of it is returned. Similar like to Typed identifier. This is only allowed if the name is not used by multiple nodes.
All statements produce a return type and each variable has one assigned to it. Types specify what kind of operation or statement can be done with the value and what kind of value we currently have.
The following types are supported:
-
Bool: Represents a boolean value. The value can either be a
trueor afalse. - Int: Any numeric value that can be represented by an signed 64 bit integer.
- String: Any character sequence. The same escaping rules as in C# applies here.
- Mode: Represents the current game
- Phase: Represents the instance of a phase
- Scene: Represents the instance of a scene
- Label: Represents the instance of a label
- Character: Represents the instance of a character
- Voting: Represents the instance of a voting
- Option: Represents the instance of an option
- Win: Represents the instance of a win condition
- Sequence: Represents the instance of a sequence
- Event: Represents the instance of an event
- ...Type: The type information of Mode, Phase, Scene, Label, Character, Voting, Option, Win, Sequence or Event. There is no instance!
And the following special types:
- Void: There is no value or instance at this point. Accessing a value with this type is prohibited by the compiler. A code block with the last statement returning a Void doesn't return anything and the behavior of the surrounding code will be applied to it.
- Optional: Some values can additionally be optional. It can have either a value or not. You cannot use the value that is stored inside directly, but you have to unpack it using an if-let-construct. This ensures that you always handle the case when no value exists at this point.
The language supports infinitely nested collections of the same type. A
collection can simply be created by using curly brackets { item1, item2 }.
If a statement expects a collection of a higher level than currently provided,
all of its items are encapsulated in a new collections. Lets say we a have a
collection with two items { a, b } and a function that expects a collection
level of 2, it will automatically promote the argument into { {a}, {b} }.
A special case is an empty collection {}. It has no type assigned to it and
inherits the type of the surrounding context. This can lead to a problem if no
type information can be inferred at all. E.g. is this statement illegal:
let $foo = {} // let expects an explicit type
But providing a context for which type information can be inferred is okay:
let $foo = if true { {} } else { @characters } // $foo is Character[1]
Each statement has a mutability assigned to it. A mutable statement is a statement that changes the current game state in some way. This can be:
- Adding or removing label
- Creation of a new voting
- sending notifications
- ...
Some statements can inherit the mutability from one of its contained statements
(e.g. the if statement).
Some functions do expect mutable code. For such are all statements checked if they mutate the current game state in some way and if this is not the case, they will be reported as a warning.
Functions that expect a return value, usually doesn't expect mutability.
Limits that can yield to problems:
- Division by zero can crash the current code. This will stop the current execution but the game is still continued. This can result in an inconsistent game state.
- You are not checked on how many sequences you create. If you create to many, you can overflow the memory which will crash the whole server.
- Through the hooks of label addition and removal, you can simply create an infinite loop. There is no detection and this will freeze the current thread. Eventually this will trigger a stack overflow which will create the same problems as if you divided by zero.
Limits that have a prevention build in:
- You cannot have an infinite sequence of scenes that have no active content. If such is found the game is stopped.
- The maximum index you can ask from a collection is 2^31-1. If you ask for a larger number, even if the collection has that many elements, you will get the same result as if you asked for an element that doesn't exists.
- You can only loop over all elements of an existing collection. It is not possible to iterate manually.
- They are no custom functions build in. This makes the language simpler and also prevents the problem with infinite function recursions. You should rethink you logic if you really need custom functions to simplify your workload. If its still required, create an issue at Github to add this as a build in function to the language - it might be usefull at other places too.