Actions & Reducers

The Redux State

You’ll see a state parameter a lot. This is the Redux state, which stores all the game state, as described in Architecture. It is an Immutable.js Map with fields nodes, globals, board, toolbox, and goal.

Mutable vs Immutable Expressions

Two “types” of expressions exist in Reduct-Redux. Most of the time, you will work with immutable expressions, which are Immutable.js Map data structures, and are manipulated through methods like get, set, and withMutations. However, it’s a lot easier to build up an AST as plain old JavaScript objects during parsing. Thus, there is a conversion step needed, and some semantics functions have to work with both types. (This should be mostly transparent to you.) action.startLevel() handles the conversion.

Working with Immutable.js

We can’t describe all of Immutable.js here, so please read the documentation for that library. However, the key things are:

  • Use get to get a field. Children are stored indirectly, so you’ll need a reference to the overall state to get access to children.

    // expr is a plain old JavaScript object
    console.log(expr.id);
    console.log(expr.child.value);
    
    // expr is an Immutable.js Map stored in Redux
    // state is the Redux store's state
    console.log(expr.get("id"));
    console.log(state.getIn([ "nodes", expr.get("id"), "value" ]));
    // getIn is a convenience function; here the use roughly
    // translates to state["nodes"][expr["id"]]["value"] if these
    // were all plain old JavaScript objects
    

    Learn to use temporary variables and helpers like getIn.

  • Use set to change a field. This does not modify the original object, instead you get a new copy of the object! Thus, if you’re modifying a nested object, you also have to re-set the field on the parent object.

    // Hypothetical example where node ID 5 has its value incremented
    const oldNode = nodes.get(5);
    nodes.set(5, oldNode.set("value", oldNode.get("value") + 1));
    

    If you’re modifying a lot of fields on the same object, check out the withMutations method.

Expression Fields

To work with expressions, you’ll need to know what fields they have.

Every expression always has an id field, as well as a field for each of the fields and subexpressions defined in the semantics (see Defining Expressions). Additionally, if the expression is a child of another, it should have a parent and parentField. The former is the ID of the parent expression, and the latter is the name of the field in which this expression is stored on the parent.

Remember, fields storing child expressions always have numeric IDs, not objects, as values.

Holes are simply expressions of type "missing". This assumption is unfortunately hard-coded (see Areas of Improvement). When a hole is filled with an expression, the original hole is placed in a field whose name is the original field name with __hole. For example, if the argument field is a hole and is filled in, then the reducer will add an argument__hole field that contains the ID of the original hole expression. (This is because the reducer doesn’t know how to generate new expressions on the fly; consequently, you can’t make a hole where there wasn’t one before, either.)

Notches are stored in fields named notch<n> where <n> is the index of the notch. The expression stored in the notch will have its parent and parentField set as normal.

Actions

import * as action from "./reducer/action";
import * as undo from "./reducer/undo";
action.startLevel(stage, goal, board, toolbox, globals)

Redux action to start a new level.

Takes trees of normal AST nodes and flattens them into immutable nodes, suitable to store in Redux. Also runs the semantics module’s postParse hook, if defined, and creates the initial views for these expressions.

Flattened trees are doubly-linked: children know their parent, and which parent field they are stored in.

Arguments:
  • stage (basestage.BaseStage) –
  • goal (Array) – List of (mutable) expressions for the goal.
  • board (Array) – List of (mutable) expressions for the board.
  • toolbox (Array) – List of (mutable) expressions for the toolbox.
  • globals (Object) – Map of (mutable) expressions for globals.
action.victory()

We’ve won the level.

Clear the board/goal, which has the side effect of stopping them from drawing anymore.

action.smallStep(nodeId, newNodeIds, newNodes)

Node nodeId took a small step to produce newNode which contains newNodes as nested nodes. All of these are immutable nodes.

action.betaReduce(topNodeId, argNodeId, newNodeIds, addedNodes)

Node topNodeId was applied to argNodeId to produce newNodeIds which contain addedNodes as nested nodes.

A beta-reduction can produce multiple result nodes due to replicators.

action.detach(nodeId)

Detach the given node from its parent.

action.fillHole(holeId, childId)

Replace the given hole by the given expression.

action.attachNotch(parentId, notchIdx, childId, childNotchIdx)

Attach the child to the given parent through the given notches

action.define(name, id)

Define the given name as the given node ID.

action.useToolbox(nodeId, clonedNodeId, addedNodes)

Take the given node out of the toolbox.

action.raise(nodeId)

Raise the given node to the top.

This is a visual concern, but the stage draws nodes in the order they are in the store, so this changes the z-index. We could make the board an immutable.Set and store the draw-order elsewhere, but we would have to synchronize it with any changes to the store. I figured it was easier to just break separation in this case.

action.unfold(nodeId, newNodeId, addedNodes)

Unfold the definition of nodeId, producing newNodeId (and adding addedNodes to the store).

action.unfade(source, nodeId, newNodeId, addedNodes)

Replace a node with its unfaded variant temporarily.

action.fade(source, unfadedId, fadedId)

Replace an unfaded node with its faded variant.

undo.undo()

Undo the last action.

undo.redo()

Redo the last undone action.

undo.undoable(options)

Given a reducer, return a reducer that supports undo/redo.

Arguments:
  • options (Object) –
  • options.restoreExtraState (function) – After an undo or redo, given the previous and new state, restore any state that lives outside of Redux. (For instance, positions of expressions on the board.)
  • options.actionFilter (function) – Given an action, if true, then simply modify the state and do not add the previous state to the undo stack.
  • options.extraState (function) – Given the previous and new state, record any state that lives outside of Redux.

Reducers

import * as reducer from "./reducer/reducer";
reducer.nextId()

Returns the next unique ID. Used to assign IDs to nodes and views.

reducer.reduct(restorePos)

The core reducer for Reduct-Redux. Interprets actions and generates the new state. Needs a semantics module and the views, in order to record positions of objects on the board for undo/redo.

Arguments:
  • restorePos (function) – a function that transforms the position of a view after undo/redo. Useful to adjust positions so that things stay within bounds (e.g. if the game has resized since the view’s position was recorded).