Skip to content

LogicLanguage

Max Brauer edited this page Aug 25, 2024 · 1 revision

Logic Language Description

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.

Directory Layout

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.

Code Structure

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.

Node mode

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
    }
}

Node phase

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" }
}

Node scene

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.

Node label

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;
}

Node character

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
        }
    }
}

Node voting

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 handler choice will be called with it. If multiple options has the most votes the handler unanimous will be called to handle such cases.
  • each: The function func targets will 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 handler choice will be executed with the result. If there was a timeout and the player hasn't selected an option the handler unanimous will be called.
  • multi_each: The same as each but 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 handler unanimous. There is no handler choice in 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 }
    }
}

Node option

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;

Node win

Specifies a win condition that can be checked at certain points in the game. That are:

  1. Before the start of each scene
  2. 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:

  1. You can define func check to check if the current game is finished and func winner to return the collection of all characters that won the game. This allows to stop a game and have no winner!
  2. You can define func has_one to 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
    }
}

Node sequence

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.

Node event

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:

  1. target now {...}: The code will be executed right at the time when the event was triggered and then never again.
  2. target phase <name> {...}: The code will be executed everytime the phase <name> was entered. This will be executed after the phases start handler.
  3. target scene <name> {...}: The code will be executed everytime the scene <name> was entered. This will be executed after the scenes start handler.

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 AutoFinishRounds is 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.

Functions

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
}

Statements

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.

Spawn new voting

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);

Spawn new sequence

Spawns a new sequence and handle its current cycle.

spawn sequence NameOfSequence($target);

$target is the character for which the sequence should be generated.

Spawn new random event

Spawns a random event.

spawn event *;

Send a notification

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;

Send a player notification

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);

Assigning a local variable

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.

Expression: Pipe

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)

Expression: Compare

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

Expression: Boolean operators

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

Expression: Integer operators

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.

Expression: Explicit collection

You can generate a new collection by listing the elements and surround them with curly braces { ... }. More Informations at the chapter Collections.

Expression: if expression

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.

Expression: if let expression

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.

Expression: for let expression

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.

Expression: Global collections

They are some global collections defined that are always available. The full list is documented in the auto generated documentation Functions.md.

Expression: Variables

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.

Expression: Integers and Strings

You can use normal integers and strings in code. Strings have the same escaping rules as C# strings.

Expression: Calls

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.

Expression: Typed identifier

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

Expression: constants

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.

Types

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 true or a false.
  • 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.

Collections

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]

Mutability

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

Limits that can yield to problems:

  1. 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.
  2. 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.
  3. 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:

  1. You cannot have an infinite sequence of scenes that have no active content. If such is found the game is stopped.
  2. 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.
  3. You can only loop over all elements of an existing collection. It is not possible to iterate manually.
  4. 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.