npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

clarityxo-ai

v0.1.0

Published

Pure-TypeScript AI engine for ClarityXO Tic-Tac-Toe

Downloads

170

Readme

clarityxo-ai

A pure-TypeScript AI engine for ClarityXO — a decentralized Tic-Tac-Toe game built on the Stacks blockchain.

This package has zero blockchain dependencies and works in Node.js, browsers, and edge runtimes. Feed it a board state (fetched from clarityxo-sdk or anywhere else), and get back the best move, a ranked list of all moves, a threat analysis, or a full simulation report.


Table of Contents


Installation

npm install clarityxo-ai

Or, if you're inside the ClarityXO monorepo:

npm install clarityxo-ai -w frontend   # add to the frontend workspace

Quick Start

import { AIEngine, Difficulty, emptyBoard } from 'clarityxo-ai';

const engine = new AIEngine({ difficulty: Difficulty.Hard });
const board  = emptyBoard();

const move = engine.bestMove(board, 'X');
console.log(move); // { row: 1, col: 1 } — center is always optimal on move one

Using with clarityxo-sdk

import { getBoard } from 'clarityxo-sdk';          // blockchain read
import { AIEngine, Difficulty } from 'clarityxo-ai'; // pure AI logic

const rawBoard = await getBoard(gameId);   // [[CellValue × 3] × 3]
const engine   = new AIEngine({ difficulty: Difficulty.Hard });

const move = engine.bestMove(rawBoard, 'X');
// submit `move` back to the blockchain via clarityxo-sdk

API Reference

AIEngine

The main entry point for move selection.

import { AIEngine, Difficulty } from 'clarityxo-ai';

const engine = new AIEngine({
  difficulty: Difficulty.Hard,
  mctsIterations: 500, // only relevant when difficulty === MCTS
});

bestMove(board, player): Move | null

Returns the single best move for player on the given board. Returns null if the board is full.

const move = engine.bestMove(board, 'X');
// { row: 0 | 1 | 2, col: 0 | 1 | 2 }

rankedMoves(board, player): ScoredMove[]

Returns every legal move sorted best-to-worst with minimax scores. Always uses full (perfect-play) minimax regardless of the engine's configured difficulty — useful for move analysis and UI overlays.

const ranked = engine.rankedMoves(board, 'X');
// [
//   { row: 0, col: 2, score: 1, depth: 1 },  // winning move
//   { row: 1, col: 0, score: 0, depth: 1 },  // draw
//   ...
// ]

Scores are from the current player's perspective: +1 is a forced win, 0 is a draw, -1 is a loss.


GameAnalyzer

Deep position analysis with threat detection and human-readable explanations.

import { GameAnalyzer } from 'clarityxo-ai';

const analyzer = new GameAnalyzer();
const report   = analyzer.analyze(board, 'X');

analyze(board, player): AnalysisReport

Analyzes the position for player to move. The analysis pipeline runs in priority order:

  1. Immediate win — does player have a win-in-1?
  2. Forced block — does the opponent have a win-in-1?
  3. Fork opportunity — can player create two simultaneous threats?
  4. Minimax evaluation — is the position theoretically won, drawn, or lost?
{
  evaluation: 'winning' | 'losing' | 'draw' | 'neutral',
  bestMove:   { row: 1, col: 2 } | null,
  threats: [
    {
      player:      'O',
      direction:   'row',
      index:       0,
      missingCell: { row: 0, col: 2 },
      type:        'win-in-1',
    },
    // ...
  ],
  explanation: "Block (0, 2) or you lose next turn.",
}

Simulator

Run self-play simulations between any two AI difficulty levels.

import { Simulator, Difficulty } from 'clarityxo-ai';

const sim    = new Simulator();
const result = await sim.run({
  games:    1000,
  playerX:  Difficulty.Hard,
  playerO:  Difficulty.Easy,
});

console.log(result);
// { xWins: 982, oWins: 0, draws: 18, totalGames: 1000, avgMoves: 7.4 }

Work is chunked in batches of 100 games via setTimeout so the event loop stays responsive in browsers and edge runtimes.

run(options): Promise<SimulationResult>

| Option | Type | Description | |---|---|---| | games | number | How many complete games to simulate | | playerX | Difficulty | Algorithm used for the X player | | playerO | Difficulty | Algorithm used for the O player | | mctsIterations | number? | MCTS iteration budget (default: 500) |


Board Utilities

Pure functions — none of them mutate anything.

import {
  emptyBoard,
  applyMove,
  checkWin,
  isBoardFull,
  isDraw,
  getAvailableMoves,
  opponent,
} from 'clarityxo-ai';

| Function | Signature | Description | |---|---|---| | emptyBoard | () → Board | Creates a fresh 3×3 board of nulls | | applyMove | (board, move, player) → Board | Returns a new board with the move applied (never mutates) | | checkWin | (board, player) → boolean | Returns true if player has three in a row | | isBoardFull | (board) → boolean | Returns true when no null cell remains | | isDraw | (board) → boolean | Board is full and neither player won | | getAvailableMoves | (board) → Move[] | All {row, col} positions where board[row][col] === null | | opponent | (player) → Player | 'X' → 'O', 'O' → 'X' |


Types

// A 3×3 grid of cells
type Board = [[CellValue, CellValue, CellValue], ...]

// Individual cell — null means empty
type CellValue = 'X' | 'O' | null;

// One of the two players
type Player = 'X' | 'O';

// A board position
interface Move { row: 0 | 1 | 2; col: 0 | 1 | 2; }

// A move with its minimax evaluation
interface ScoredMove extends Move { score: number; depth: number; }

// A detected tactical threat
interface Threat {
  player: Player;
  direction: 'row' | 'col' | 'diag';
  index: number;
  missingCell: Move;
  type: 'win-in-1' | 'fork';
}

// Full position report from GameAnalyzer
interface AnalysisReport {
  evaluation: 'winning' | 'losing' | 'draw' | 'neutral';
  bestMove: Move | null;
  threats: Threat[];
  explanation: string;
}

// Aggregate stats from Simulator.run()
interface SimulationResult {
  xWins: number;
  oWins: number;
  draws: number;
  totalGames: number;
  avgMoves: number;
}

Difficulty Levels

| Level | Algorithm | Character | |---|---|---| | Easy | Random legal move | Picks blindly — great for beginners or as a rollout baseline | | Medium | Minimax depth-2 | Sees one exchange ahead — blocks obvious threats but misses deeper tactics | | Hard | Full minimax + alpha-beta | Perfect play — never loses, always wins when possible | | MCTS | Monte Carlo Tree Search | Probabilistic — strength scales with mctsIterations; interesting mid-game behavior |

import { Difficulty } from 'clarityxo-ai';

Difficulty.Easy    // 'easy'
Difficulty.Medium  // 'medium'
Difficulty.Hard    // 'hard'
Difficulty.MCTS    // 'mcts'

How the Algorithms Work

Minimax with Alpha-Beta Pruning

Minimax is a classic adversarial search algorithm. It builds a complete game tree, assuming both players always play optimally, and returns the move that maximises the current player's outcome.

Negamax formulation — rather than separate max and min branches, negamax flips the sign of the score at each level:

negamax(node):
  if terminal: return score from current player's perspective
  return max over children of −negamax(child)

Alpha-beta pruning cuts branches of the search tree that cannot possibly influence the final decision. In Tic-Tac-Toe this is crucial: it reduces the worst-case node count from 9! ≈ 362,880 to well under 10,000 with good move ordering.

Move ordering (center → corners → edges) is applied before each search level. Because center and corner moves tend to be stronger, the engine finds good cutoff bounds early, making alpha-beta significantly more effective.

Depth-scaled scoring — winning scores are divided by depth (1 / depth), so the engine prefers faster wins and defers losses for as many moves as possible. This makes gameplay feel aggressive rather than passive.

Monte Carlo Tree Search (MCTS)

MCTS trades deterministic correctness for sampling efficiency. It estimates move strength by simulating thousands of random games and tracking win rates.

Each iteration runs four steps:

1. Selection   — walk the tree from root, always choosing the child
                 with the highest UCB1 score:
                 UCB1 = wins/visits + √2 · √(ln(parentVisits) / visits)

2. Expansion   — at the selected node, try a random untried move
                 and create a new child node.

3. Simulation  — from the new node, play random moves until a
                 terminal state is reached.

4. Backprop    — walk back up the tree incrementing visit counts
                 and win counts at every ancestor.

The final move is the child of the root with the most visits (rather than the highest win rate), which reduces variance on low iteration budgets.

UCB1's exploration term (√2 · √(ln(N)/n)) balances exploitation (visiting moves that seem strong) with exploration (trying moves that haven't been sampled enough). C = √2 is the standard theoretical value for games with binary win/loss outcomes.


Project Structure

clarityxo-ai/
├── src/
│   ├── index.ts              # public barrel — re-exports everything
│   ├── types.ts              # Board, Player, Move, Difficulty, and all interfaces
│   ├── utils.ts              # pure board primitives (checkWin, applyMove, …)
│   ├── algorithms/
│   │   ├── random.ts         # random legal move — Easy difficulty + MCTS rollout
│   │   ├── minimax.ts        # negamax + alpha-beta + move ordering
│   │   └── mcts.ts           # Monte Carlo Tree Search with UCB1 selection
│   ├── engine.ts             # AIEngine — unified bestMove / rankedMoves API
│   ├── analyzer.ts           # GameAnalyzer — threat detection + explanation
│   └── simulator.ts          # Simulator — async self-play batch runner
├── tests/
│   ├── utils.test.ts
│   ├── minimax.test.ts
│   ├── mcts.test.ts
│   ├── engine.test.ts
│   ├── analyzer.test.ts
│   └── simulator.test.ts
├── package.json
├── tsconfig.json
└── vitest.config.ts

Development

# Install dependencies
npm install

# Run all tests
npm test

# Type-check without emitting
npm run typecheck

# Build to dist/
npm run build

# Smoke test after build
node --input-type=module <<'EOF'
import { AIEngine, Difficulty, emptyBoard } from './dist/index.js';
const ai = new AIEngine({ difficulty: Difficulty.Hard });
console.log(ai.bestMove(emptyBoard(), 'X')); // { row: 1, col: 1 }
EOF

Test coverage summary

| File | Tests | What's covered | |---|---|---| | utils.test.ts | 13 | Win detection (rows, cols, diagonals), draw detection, move enumeration, immutable applyMove | | minimax.test.ts | 5 | Takes an immediate win, blocks a forced loss, prefers faster wins, Hard vs Hard draws | | mcts.test.ts | 4 | Takes immediate win, blocks opponent win-in-1, null on full board, returns legal move | | engine.test.ts | 8 | All four difficulty levels, center on empty board, rankedMoves ordering | | analyzer.test.ts | 6 | win-in-1 detection, forced-block detection, fork detection, draw evaluation, non-empty explanation | | simulator.test.ts | 4 | Hard vs Hard → 100% draws, Hard vs Easy → X wins >90%, total game counts, avgMoves sanity |


Integration with clarityxo-sdk

clarityxo-ai and clarityxo-sdk are intentionally decoupled. The SDK handles blockchain I/O (reading game state, submitting moves as Stacks transactions); this package handles only the game logic and AI decision-making.

Typical integration pattern:

import { fetchGame, submitMove } from 'clarityxo-sdk';
import { AIEngine, GameAnalyzer, Difficulty } from 'clarityxo-ai';

const game     = await fetchGame(gameId);
const engine   = new AIEngine({ difficulty: Difficulty.Hard });
const analyzer = new GameAnalyzer();

// Analyze the current position
const report = analyzer.analyze(game.board, game.currentPlayer);
console.log(report.explanation);

// Pick and submit the best move
const move = engine.bestMove(game.board, game.currentPlayer);
if (move) await submitMove(gameId, move);

License

MIT © Dominion116