Rusty Rabbit
Behavior tuning and direct code viewing

The Gold Room

Gold is now the behavior room. Silver owns voices and member-facing writing, while Gold focuses on the current Rabbot's live behavior JavaScript and the guide needed to tune it safely.

On this page Behavior JS JS Guide
Member session Not logged in.
Editing status Editing is locked: this is a fluffle Rabbot, so Developer access is required.
Viewing: Rusty Rabbit Fluffle  |  Developer access: locked  |  Log in
kit avatar

Kit — Gold Room

Current source: Rusty Rabbit Fluffle. Loading from fluffle structure.

Current rabbot folder:
users/fluffle/kit/

Current Rabbot page: open public page  |  Behavior JS: open kit.js

Behavior JS

This is the live behavior file for the current Rabbot. In normal use, this is the main file here that can directly alter how the Rabbot behaves during play.

Current behavior file:
users/fluffle/kit/personality/kit.js
What to change here:
The white fields are the editable bodies of filterActions(...) and scoreAction(...). The dark blocks are locked structure and should stay unchanged here.
Editing is locked. A member may edit only their own Rabbot behavior file, while fluffle behavior editing requires Developer access.
After saving, use your normal play room or match board to test the updated behavior live.
(function(global){
  console.log('LIVE KIT LOADED PROBE A');
  window.__kitFilterWrapInstalled = window.__kitFilterWrapInstalled || false;

  function num(v, fallback){
    const n = Number(v);
    return Number.isFinite(n) ? n : (fallback || 0);
  }

  function allActions(ctx){
    return Array.isArray(ctx && ctx.actions) ? ctx.actions.filter(Boolean) : [];
  }

  function isTileAction(action){
    return !!(action && action.type === 'tile' && action.move);
  }

  function isTokenAction(action){
    return !!(action && action.type === 'token' && action.move);
  }

  function isEraseTileAction(action){
    return !!(action && action.type === 'eraseTile' && action.move);
  }

  function isEraseTokenAction(action){
    return !!(action && action.type === 'eraseToken' && action.move);
  }

  function getTileCountForSeat(ctx, seat){
    if(!ctx) return 0;
    const tc = ctx.tileCount;

    if(typeof tc === 'number') return num(tc, 0);
    if(Array.isArray(tc)) return num(tc[seat], 0);

    if(typeof tc === 'function'){
      try{
        return num(tc(seat), 0);
      }catch(_){
        try{
          return num(tc(), 0);
        }catch(__){
          return 0;
        }
      }
    }

    if(tc && typeof tc === 'object'){
      return num(tc[seat], 0);
    }

    return 0;
  }

  function countMyPlacedTiles(ctx){
    return getTileCountForSeat(ctx, ctx && ctx.seat);
  }

  function countOppPlacedTiles(ctx){
    if(!ctx) return 0;

    if(typeof ctx.rivalSeat === 'number'){
      return getTileCountForSeat(ctx, ctx.rivalSeat);
    }

    if(typeof ctx.playerCount === 'function'){
      let total = 0;
      try{
        const pc = num(ctx.playerCount(), 0);
        for(let s = 0; s < pc; s++){
          if(s === ctx.seat) continue;
          total += getTileCountForSeat(ctx, s);
        }
        return total;
      }catch(_){
        return 0;
      }
    }

    return 0;
  }

  function tileAt(action, i, j){
    return isTileAction(action) &&
      num(action.move.i) === num(i) &&
      num(action.move.j) === num(j);
  }

  function findTileAction(ctx, i, j){
    const actions = allActions(ctx);
    for(const action of actions){
      if(tileAt(action, i, j)) return action;
    }
    return null;
  }

  function nearestTargetBonus(action, targets, weights){
    if(!isTileAction(action)) return 0;

    const i = num(action.move.i);
    const j = num(action.move.j);
    let best = 0;

    for(let k = 0; k < targets.length; k++){
      const t = targets[k];
      const w = Array.isArray(weights) ? num(weights[k], 0) : 0;
      const dist = Math.abs(i - t.i) + Math.abs(j - t.j);
      const score = Math.max(0, w - dist * 8);
      if(score > best) best = score;
    }

    return best;
  }

  function openingSecondMovePreference(ctx){
    const actions = allActions(ctx).filter(isTileAction);
    if(!actions.length) return null;

    const preferred = [
      { i: -1, j:  0, w: 100 },
      { i:  1, j:  0, w:  88 },
      { i:  0, j: -1, w:  84 },
      { i:  0, j:  1, w:  84 }
    ];

    let best = null;
    let bestScore = -Infinity;

    for(const action of actions){
      const i = num(action.move.i);
      const j = num(action.move.j);
      let s = -999999;

      for(const p of preferred){
        if(i === p.i && j === p.j){
          s = p.w;
          break;
        }
      }

      if(s > bestScore){
        bestScore = s;
        best = action;
      }
    }

    console.log('KIT SECOND MOVE CANDIDATES', actions.map(a => ({
      type: a.type,
      move: a.move || null
    })));

    return bestScore > -999999 ? best : null;
  }

  function openingThirdMovePreference(ctx){
    const myTiles = countMyPlacedTiles(ctx);
    const oppTiles = countOppPlacedTiles(ctx);
    if(myTiles !== 2 || oppTiles < 1) return null;

    const actions = allActions(ctx).filter(isTileAction);
    if(!actions.length) return null;

    const centralReplyTargets = [
      { i:  1, j:  0 },
      { i:  0, j: -1 },
      { i:  0, j:  1 }
    ];

    const ownCornerTargets = [
      { i: -2, j:  0 },
      { i: -1, j: -1 },
      { i: -1, j:  1 }
    ];

    let best = null;
    let bestScore = -Infinity;

    for(const action of actions){
      let s = 0;
      s += nearestTargetBonus(action, centralReplyTargets, [52, 46, 46]);
      s += nearestTargetBonus(action, ownCornerTargets, [34, 30, 30]);

      if(action.move && action.move.replace) s -= 10;

      if(s > bestScore){
        bestScore = s;
        best = action;
      }
    }

    return best;
  }

  function forceAction(ctx){
    const actions = allActions(ctx);
    if(!actions.length) return null;

    const myTiles = countMyPlacedTiles(ctx);

    if(myTiles === 0){
      return findTileAction(ctx, 0, 0) || null;
    }

    if(myTiles === 1){
      return openingSecondMovePreference(ctx) || null;
    }

    if(myTiles === 2){
      return openingThirdMovePreference(ctx) || null;
    }

    return null;
  }

  function filterActions(ctx){
}

  function scoreAction(ctx){
}

  function chooseBest(ctx, pool){
    if(!pool.length) return null;

    let best = null;
    let bestScore = -Infinity;

    for(const action of pool){
      const s = scoreAction({ ...ctx, action });
      if(s > bestScore){
        bestScore = s;
        best = action;
      }
    }

    return best || pool[0] || null;
  }

  function chooseSafeFallback(ctx){
    const actions = allActions(ctx);
    if(!actions.length) return null;

    const myTiles = countMyPlacedTiles(ctx);
    const tileActions = actions.filter(isTileAction);

    if(myTiles === 0){
      return findTileAction(ctx, 0, 0) || tileActions[0] || actions[0];
    }

    if(myTiles === 1){
      return openingSecondMovePreference(ctx) || tileActions[0] || actions[0];
    }

    if(myTiles === 2){
      return openingThirdMovePreference(ctx) || tileActions[0] || actions[0];
    }

    return actions.find(isTokenAction)
      || actions.find(isEraseTokenAction)
      || actions.find(isEraseTileAction)
      || tileActions[0]
      || actions[0]
      || null;
  }

  function chooseAction(ctx){
    const forced = forceAction(ctx);
    if(forced) return forced;

    const pool = filterActions(ctx);
    const best = chooseBest(ctx, pool);
    if(best) return best;

    return chooseSafeFallback(ctx);
  }

  if(global && global.RustyPersonalities && typeof global.RustyPersonalities.register === 'function'){
    global.RustyPersonalities.register({
      level: 1,
      label: 'Kit',
      forceAction: forceAction,
      filterActions: filterActions,
      scoreAction: scoreAction,
      chooseAction: chooseAction
    });
  }
})(window);

Trigame Rabbot Behavior JS Guide

This page is a plain-English guide to the behavior JavaScript files used by Trigame Rabbots. It is meant as a practical “master key” so you can tune a Rabbot directly without having to rediscover what each part of the file does.

Big idea:
A behavior file does not usually contain the whole AI. It supplies a flavor layer that nudges move choice. The engine generates legal moves first, then the behavior file:
  1. optionally filters which legal moves are even considered
  2. scores each remaining move
  3. the engine chooses the best-scoring move

1. Basic file shape

(function(){
  const register = window.RustyPersonalities && window.RustyPersonalities.register;
  if(!register) return;

  register({
    level: 5,
    id: "hare_brained",
    label: "Hare-Brained",

    filterActions(ctx){
      ...
    },

    scoreAction(ctx){
      let score = 0;
      ...
      return score;
    }
  });
})();

2. Top-level fields

FieldWhat it doesTypical value / rangeAdvice
level A numeric slot or identity level used by the game. Usually a positive integer such as 1, 2, 5, etc. Do not reuse the same level for two different intended personalities unless you mean to replace one.
id Internal machine name. Lowercase string with underscores, such as newb_bble. Keep it unique and stable. Good for filenames and debugging.
label Human-readable display name. Any short string. This is what players see. Changing this is safe.
filterActions(ctx) Optional hard preference stage. Lets the rabbit throw away many legal moves before scoring. Returns an array of actions. Use carefully. This is powerful and can make a rabbit feel stubborn or “forced.”
scoreAction(ctx) Main personality scoring function. Returns one number. This is the safest place to tune personality.

3. The two main tuning methods

3a. filterActions(ctx)

Use this when you want a rabbit to behave in a very strong, rule-like way.

filterActions(ctx){
  const tileActions = ctx.actions.filter(a => a.type === 'tile');
  if(!tileActions.length) return ctx.actions;

  if(ctx.phase === 0 && ctx.tileCount <= 10) return tileActions;
  if(ctx.phase === 1 && ctx.tileCount <= 6) return tileActions;

  return ctx.actions;
}

That example means: “if tiles exist, and we are early enough in the turn / game, consider only tile moves.”

Warning:
filterActions() can overpower everything else. If you make it too strict, a rabbit may stop looking smart and start looking blind.

3b. scoreAction(ctx)

This adds or subtracts points from candidate moves.

scoreAction(ctx){
  let score = 0;

  if(ctx.action.type === 'tile'){
    score += 140;
  }

  if(ctx.action.type === 'token'){
    score -= 10;
  }

  return score;
}

The engine compares scores across moves. Higher usually wins.

4. The most important ctx values

NameMeaningExpected valuesNotes
ctx.actions All currently legal candidate moves. Array Used mostly by filterActions().
ctx.action The one move currently being scored. Object Used mostly by scoreAction().
ctx.action.type What kind of move this is. 'tile', 'token', 'eraseTile', 'eraseToken' The most important branch point.
ctx.action.move The move payload. Object For tiles, this often holds cell info. For tokens, socket/key info.
ctx.phase Current turn phase. Usually 0 or 1 Phase 0 = first action, Phase 1 = second action. Bonus token actions are handled separately by the engine.
ctx.seat Which player seat this rabbit is. Integer, usually starting at 0 Seat 0 is player 1, seat 1 is player 2, and so on.
ctx.level The rabbit level/slot currently being used. Positive integer Often matches the file’s level.
ctx.tileCount How many tiles are currently on the board. 0 and up Most personalities use low thresholds like 1, 2, 3, 5, 8, 10.
ctx.tokenCount How many tokens are on the board. 0 and up Often less important than tile count, but still useful.
ctx.boardLocked Whether the board is locked. true / false Good for defensive checks if needed.
ctx.bonusCount How many bonus token actions are currently available. 0 and up Mostly relevant in advanced tuning.

5. Common helper functions you can call from ctx

HelperPurposeTypical returnHow to think of it
ctx.adjacentPlacedCount(move) Counts neighboring placed tiles touching the candidate tile. Usually 0 to a few Bigger = more connected growth.
ctx.withTempTileForSeat(move, seat, fn) Temporarily imagines the tile being placed, runs a function, then restores the board. Whatever fn returns Excellent for “if I place this tile, what does it open up?”
ctx.openSocketMoves(true) Looks at currently open token sockets. Array Good for valuing expansion potential.
ctx.estimateBestTokenGainForSeat(seat, N) Roughly estimates token payoff for the seat. Number Use as a bonus multiplier, not as gospel.
ctx.ownTokenCountNearCell(seat, cell) Counts this rabbit’s own tokens near a location. 0 and up Good for local-cluster personalities.

6. What score numbers mean

There is no universal magic number, but these are good rough meanings:

Score amountTypical meaning
1 to 6Tiny nudge
8 to 20Noticeable preference
20 to 50Strong bias
60 to 120Very strong bias
140+Almost a rule, unless another move is boosted similarly
Important:
The score is only meaningful relative to other move scores in the same moment. A +20 can be huge in one rabbit and tiny in another.

7. Practical tuning patterns

Make a rabbit more tile-happy

if(ctx.action.type === 'tile'){
  score += ctx.tileCount <= 2 ? 120 : 60;
}
if(ctx.action.type === 'token'){
  if(ctx.tileCount <= 4) score -= 20;
}

Make a rabbit like connected growth

if(ctx.action.type === 'tile'){
  score += (ctx.adjacentPlacedCount(ctx.action.move) || 0) * 20;
}

Make a rabbit love local clusters

if(ctx.action.type === 'tile'){
  score += ctx.ownTokenCountNearCell(ctx.seat, ctx.action.move) * 26;
}
if(ctx.action.type === 'token'){
  const cell = ctx.action.move.cell || null;
  if(cell) score += ctx.ownTokenCountNearCell(ctx.seat, cell) * 18;
}

Make a rabbit prefer future options

if(ctx.action.type === 'tile'){
  score += ctx.withTempTileForSeat(
    ctx.action.move,
    ctx.seat,
    ()=> Math.min(24, (ctx.openSocketMoves(true).length || 0) * 0.24)
  ) || 0;
}

Make a rabbit delay tokening

if(ctx.action.type === 'token'){
  if(ctx.tileCount <= 5) score -= 10;
}

8. Safe value ranges for common knobs

KnobSafe starter rangeAggressive rangeWarning
Flat tile bonus +20 to +60 +80 to +180 Too high can force tiny-minded rabbits to become expansion drones.
Flat token penalty early -4 to -12 -18 to -60 Too negative can make a rabbit ignore easy wins.
Neighbor multiplier 8 to 18 20 to 30 Too high can make rabbits hug existing tiles and stop exploring.
tileCount thresholds 1,2,3,4 5,6,8,10 Big thresholds can keep a rabbit in “opening mode” too long.
filterActions() hard tile cutoff Phase 0 only, up to tileCount <= 3 Phase 0 and 1, up to tileCount <= 8 or more This is one of the most dangerous knobs.

9. Warnings and gotchas

  • Do not tune from one match. Use batches and tournaments.
  • Multi-player results are messy. Player B can easily determine whether A or C wins.
  • Duplicated personalities skew tests. Two Warrens in one match do not tell the same story as one Warren.
  • Small-board habits are hard to break. Sometimes a rabbit needs hard filtering, not just score nudges.
  • Replace bonuses are touchy. Over-rewarding replacement tiles can create odd behavior.
  • Temporary-board helpers are powerful. They are great, but piling too many of them together can make a rabbit overfit to one kind of move.

10. Suggested workflow for editing rabbits

  1. Change only one rabbit at a time.
  2. Change only one main idea at a time:
    • more tiles
    • more local clustering
    • better token cash-in
    • more blocking
  3. Run a small tournament.
  4. Read both:
    • who won
    • how the board actually behaved
  5. If needed, rename or reorder rabbits only after behavior is stable.

11. Simple personality recipes

Personality styleHow to build it
Token-happy rabbit Boost token, reduce tile, especially when tileCount is small.
Expansion rabbit Boost tile, delay token, reward connected growth.
Cluster rabbit Use ownTokenCountNearCell() for both tile and token scoring.
Lane-builder Use withTempTileForSeat() plus openSocketMoves().
Punisher / closer Reward token gains and capture-related follow-up more than raw tile count.

12. Final advice

The easiest way to make a rabbit stronger fast:
Make it place more tiles, then give it just enough sense to cash in on the growth.

But the most interesting fluffle is not one where every rabbit uses the same trick. Let some rabbits cause the mess, and let others exploit it.