Cluster pays
TL;DR
Выплата идёт за связные группы из N+ одинаковых символов, где «связно» означает 4-связное соседство по reel/row (без диагоналей). Минимальный кластер обычно 5. Чаще всего совмещается с tumbling reels — победившие кластеры исчезают, новые символы падают сверху, повторно evaluate.
Note
Ту же 4-связность использует Connect&Collect, но там это не pay-table по размеру группы, а путь спецсимволов к reward-узлам.
Как работает
Конфиг
Вместо paytable с фиксированными kind обычно используется pay group с диапазонами:
pay_group = {
((5,5), "H1"): 50,
((6,7), "H1"): 100,
((8,10), "H1"): 500,
((11,30), "H1"): 5000,
...
}
paytable = convert_range_table(pay_group) # раскрывает в обычный {(5,"H1"):50, (6,"H1"):100, (7,"H1"):100, ...}Зачем: на 6×5 поле возможны кластеры до 30 символов; вручную писать 25 строк per symbol — нечитаемо.
Алгоритм evaluate (BFS)
visited = matrix[num_reels][num_rows] = false
wins = []
для каждой позиции (r,c):
если visited[r][c] или symbol(r,c) — special-non-paying: continue
base_sym = symbol(r,c)
если base_sym — wild: continue (wild сам не стартует кластер)
cluster = BFS_4connected(r, c, base_sym, allow_wild=True)
для каждой позиции в cluster: visited[pos] = true
size = len(cluster)
если size >= MIN_CLUSTER и (size, base_sym) в paytable:
win = paytable[(size, base_sym)] × multiplier
wins.append({symbol, kind: size, win, positions: cluster})
return {totalWin, wins}
BFS — стандартный, очередь, 4 соседа. Сложность O(num_reels × num_rows).
Wild — особый случай
Wild может одновременно принадлежать нескольким кластерам разных базовых символов. Алгоритм: при BFS из base_sym X wild считается членом кластера X, но visited[wild_pos] не ставится для других итераций. Wild не «потребляется» одним кластером.
H1 H1 W H2 H2
H1 W W H2 H2
H1 W H2 H2 L1
- Кластер H1 (с реки 0–1, ряды 0–2) включает все H1 и три W → размер 7.
- Кластер H2 включает все H2 и те же три W → размер 8.
Wild сам не стартует кластер (иначе wild-only «кластеры» зачитывались бы как (N, "W"), что обычно в paytable нет или явно запрещено).
Мат-эффект
- Hit frequency — сильно вариативная: на 6×5 поле выпадение 5+ соседних — типичная задача, цифра зависит от reelstrip-композиции.
- Volatility — по умолчанию ниже paylines, но с tumble loop становится высокой (комбо-цепочки могут давать огромные wins при удачном raid).
- State space для cert-симуляции — размер board (N reels × M rows независимых позиций по reelstrip-стопу).
- Часто RTP сильно весит во freegame (с multiplier-grid и global mult).
Варианты и подвиды
- Чистый cluster без tumble (редко).
- Cluster + tumble (стандарт, см. tumble).
- Cluster + multiplier grid — в freegame каждая позиция «активируется» при первом win и накапливает множитель (часто 2× → 4× → … → 512×). Sweet Bonanza, Sugar Rush.
- Global multiplier во freegame, инкрементируемый каждым tumble.
- Diagonal-allowed — редко, обычно строго 4-связно.
Реализационные заметки
Бэк (TS)
function evaluateCluster(
board: SymbolName[][],
paytable: Map<string, number>,
wildSyms: SymbolName[],
minCluster: number,
): WinData {
const numReels = board.length;
const numRows = board[0].length;
const visited = matrix(numReels, numRows, false);
const wins: WinDetail[] = [];
for (let r = 0; r < numReels; r++) {
for (let c = 0; c < numRows; c++) {
if (visited[r][c]) continue;
const sym = board[r][c];
if (wildSyms.includes(sym) || !isPayingSymbol(sym)) continue;
const cluster = bfs4Connected(board, r, c, sym, wildSyms);
// mark visited only for non-wild positions
cluster.filter(p => !wildSyms.includes(board[p.reel][p.row])).forEach(p => {
visited[p.reel][p.row] = true;
});
if (cluster.length >= minCluster) {
const key = `${cluster.length}:${sym}`;
const basePay = paytable.get(key) ?? 0;
if (basePay > 0) {
wins.push({symbol: sym, kind: cluster.length, win: basePay, positions: cluster, meta: {}});
}
}
}
}
return {totalWin: sum(wins, w => w.win), wins};
}
function bfs4Connected(board, startR, startC, sym, wildSyms): Position[] {
const queue = [{reel: startR, row: startC}];
const cluster: Position[] = [];
const seen = new Set<string>();
seen.add(`${startR}:${startC}`);
while (queue.length) {
const {reel, row} = queue.shift()!;
cluster.push({reel, row});
for (const [dr, dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
const nr = reel + dr, nc = row + dc;
const key = `${nr}:${nc}`;
if (nr < 0 || nr >= board.length || nc < 0 || nc >= board[0].length) continue;
if (seen.has(key)) continue;
const ns = board[nr][nc];
if (ns === sym || wildSyms.includes(ns)) {
seen.add(key);
queue.push({reel: nr, row: nc});
}
}
}
return cluster;
}Edge cases
- Wild-only board — никаких выигрышей (wild не стартует кластер). Разве что мы в paytable явно прописали
(N, "W"). - Вложенные/перекрывающиеся кластеры через wild — учтены тем, что мы visited только non-wild позиции.
- Большой кластер (>чем последнее значение в pay_group) — берём максимальный диапазон.
convert_range_tableэто автоматизирует.
Фронт
Эмитится winInfo с positions всех клеток кластера + meta.overlay (центр кластера для размещения badge с win-amount):
'meta': {
'overlay': {'reel': float, 'row': float} # «центр масс» кластера
}Фронт рисует pulsing-highlight всего кластера, badge посередине, потом tumbleBoard event запускает анимацию падения новых символов.
Compliance / cert
- Help / paytable: раскрывать paytable с диапазонами и правило wild-substitution.
- Visualization кластера должна быть читаемой — UKGC RTS требует «clear and accurate» представления выигрыша.
- Tumble-механика и multiplier-grid требуют чёткого описания «как накапливается множитель».
Примеры реализаций
- Pragmatic Play Sweet Bonanza (6×5, cluster + tumble + grid mult).
- Pragmatic Play Sugar Rush (7×7, cluster + tumble).
- NetEnt Aloha! Cluster Pays (родоначальник нынешней волны).
- Stake math-sdk sample:
games/0_0_cluster/.