Post 859: Chess Solver as Pure Series - Compute Not Duplicate

Post 859: Chess Solver as Pure Series - Compute Not Duplicate

Watermark: -859

Post 859: Chess Solver as Pure Series

📦 Architecture Reference

See Post 860: R3 Architecture for the complete distributed systems implementation using this pure series approach.

Post 860 shows how the “only series, everything derived” principle scales to:

  • Three node types: DHT (discovery), BitTorrent (storage), EVM (execution)
  • Node perspective: Each observes only its own series
  • Natural rate limiters: From liberty-model applied to distributed systems
  • Self-organizing: Auto-discovery and replication with zero configuration

The same principle applied everywhere: Chess solver → Distributed architecture


Rewriting Post 858 with Series Only - Compute Not Duplicate

From Post 858: Minimal chess solver with three states

From Post 810: Universal format - everything as series

From universal-model philosophy: Compute instead of duplicate

New: Chess solver as pure series - no data structures, only time evolution

Result: Everything computed on demand, nothing stored except series


Part 1: The Problem with Post 858

What Post 858 Stored

class MinimalChessSolver:
    def __init__(self):
        self.transposition_table = {}  # ❌ Dictionary
        self.positions_explored = 0    # ❌ Counter
        self.time_budget = 60          # ❌ State variable

Problems:

  • Duplication: Transposition table stores positions already in series
  • State mutation: Counter and timer are mutable state
  • Data structures: Using dictionaries, not series
  • Not universal: Doesn’t follow Post 810 format

Universal-model philosophy:

  • Compute, don’t duplicate: Calculate values when needed from series
  • Series only: Everything is evolving series
  • Stateless: No mutable variables
  • Time-based: All data indexed by time t

Part 2: Chess Game as Series

Single Series Contains Everything

class ChessGameSeries:
    """
    Chess game = single series evolving over time
    No other data structures needed
    """
    def __init__(self):
        # Single series: list of game states
        self.series = []
        
    def append(self, state):
        """
        Add new state at time t
        
        state = {
            't': time,
            'position': FEN string,
            'move': move made,
            'eval': evaluation,
            'confidence': C value,
            'entropy': random component
        }
        """
        self.series.append(state)
    
    def at(self, t):
        """
        Get state at time t
        Compute on demand if needed
        """
        if t < len(self.series):
            return self.series[t]
        else:
            # Compute future state
            return self.compute_state(t)

Everything in series:

  • Position at each time
  • Move made
  • Evaluation
  • Confidence level
  • Entropy component

Nothing else stored.


Part 3: Transposition Table = Computed from Series

Don’t Store, Compute

Post 858 approach (duplicate):

# Store in dictionary
self.transposition_table[position] = move

# Later, look up
if position in self.transposition_table:
    return self.transposition_table[position]

Post 859 approach (compute):

def has_seen_position(self, position, series):
    """
    Check if position appears in series
    Compute on demand, don't store
    """
    for state in series:
        if state['position'] == position:
            return True, state['t']
    return False, None

def best_move_for_position(self, position, series):
    """
    Find best move for position from series
    Compute from series history, don't lookup dictionary
    """
    matches = [
        state for state in series 
        if state['position'] == position
    ]
    
    if not matches:
        return None
    
    # Return move with best evaluation
    best = max(matches, key=lambda s: s['eval'])
    return best['move']

Key difference:

  • No dictionary: Just search series
  • Compute on demand: Find values when needed
  • Single source of truth: Series contains all data
  • No duplication: Position data exists once in series

Part 4: Positions Explored = Computed from Series

Count from Series

Post 858 approach (duplicate):

self.positions_explored = 0

# Increment counter
self.positions_explored += 1

# Check count
if self.positions_explored > budget:
    stop()

Post 859 approach (compute):

def positions_explored(series):
    """
    Count positions from series
    No counter needed
    """
    return len(series)

def under_budget(series, budget):
    """
    Check if under budget
    Compute from series length
    """
    return len(series) < budget

No counter variable. Just compute from series length.


Part 5: Time Budget = Computed from Series

Time from Series Timestamps

Post 858 approach (duplicate):

self.time_budget = 60
start_time = time.now()

# Later
elapsed = time.now() - start_time
if elapsed > self.time_budget:
    stop()

Post 859 approach (compute):

def time_elapsed(series):
    """
    Compute time from series timestamps
    No variables needed
    """
    if len(series) == 0:
        return 0
    
    t_start = series[0]['t']
    t_now = series[-1]['t']
    return t_now - t_start

def time_per_move(series):
    """
    Compute average time per move
    """
    if len(series) <= 1:
        return 0
    
    return time_elapsed(series) / len(series)

def time_remaining(series, budget):
    """
    Compute remaining time
    """
    return budget - time_elapsed(series)

No time variable. Compute from series timestamps.


Part 6: Evaluation = Computed from Position

Don’t Store Evaluation

Post 858 approach:

# Compute and store
eval_score = self.evaluate(position)
self.store(position, eval_score)  # Store it

Post 859 approach:

def evaluate(position):
    """
    Compute evaluation on demand
    Don't store it
    """
    material = material_balance(position)
    positional = positional_score(position)
    return material + positional

def evaluate_at_t(series, t):
    """
    Evaluate position at time t
    Compute from series
    """
    state = series[t]
    return evaluate(state['position'])

Compute evaluation every time needed. Don’t store.


Part 7: Confidence = Computed from Evaluation

Derive Confidence

Post 858 approach:

eval_score = self.evaluate(position)
if eval_score >= 2.0:
    confidence = 1  # Store confidence

Post 859 approach:

def confidence(series, t):
    """
    Compute confidence at time t
    From evaluation, not stored
    """
    eval_score = evaluate_at_t(series, t)
    
    if eval_score >= 2.0:
        return 1  # Confident
    else:
        return eval_score / 2.0  # Proportional

def is_confident(series, t):
    """
    Check if confident
    Compute from series
    """
    return confidence(series, t) >= 0.8

Confidence computed from evaluation. Not stored separately.


Part 8: Information = Computed from Series History

Search Series for Position

Post 858 approach:

if position not in self.transposition_table:
    information = 0  # Store flag
else:
    information = 1

Post 859 approach:

def has_information(series, position):
    """
    Check if position seen before
    Compute from series search
    """
    for state in series:
        if state['position'] == position:
            return 1  # Have information
    return 0  # No information

def information_at_t(series, t):
    """
    Information level at time t
    """
    current_position = series[t]['position']
    
    # Search earlier in series
    for i in range(t):
        if series[i]['position'] == current_position:
            return 1  # Seen before
    
    return 0  # Novel position

Information computed by searching series. Not stored.


Part 9: Rate Limiters = Computed from Series

Four Limiters from Series

def rate_limiter_economic(series, budget):
    """
    Economic limiter: time remaining
    Compute from series
    """
    remaining = time_remaining(series, budget)
    total = budget
    return remaining / total  # 0.0 to 1.0

def rate_limiter_objective(series, t):
    """
    Objective limiter: position quality
    Compute from evaluation
    """
    eval_score = evaluate_at_t(series, t)
    
    if abs(eval_score) < 0.5:
        return 1.0  # Critical position
    elif abs(eval_score) < 1.5:
        return 0.6  # Moderate
    else:
        return 0.2  # One-sided

def rate_limiter_w_tracking(series, budget):
    """
    W tracking: positions explored
    Compute from series length
    """
    explored = len(series)
    ratio = explored / budget
    
    if ratio < 0.5:
        return 1.0
    elif ratio < 0.9:
        return 0.5
    else:
        return 0.1

def rate_limiter_topology(series, t):
    """
    Topology: legal moves
    Compute from position
    """
    position = series[t]['position']
    legal_moves = count_legal_moves(position)
    
    return min(1.0, legal_moves / 50)

def combined_rate_limiter(series, t, budget):
    """
    Combine all four limiters
    All computed from series
    """
    R = (
        rate_limiter_economic(series, budget) * 0.3 +
        rate_limiter_objective(series, t) * 0.3 +
        rate_limiter_w_tracking(series, budget) * 0.2 +
        rate_limiter_topology(series, t) * 0.2
    )
    return R

All rate limiters computed from series. No state variables.


Part 10: Complete Pure Series Solver

The Full Implementation

class PureSeriesChessSolver:
    """
    Chess solver using only series
    Compute everything on demand
    """
    
    def __init__(self):
        # Only series - no other data structures
        self.series = []
    
    def play_move(self, position, budget=60):
        """
        Choose move using three-state decision
        Everything computed from series
        """
        t = len(self.series)
        
        # STATE 1: Check confidence (computed from evaluation)
        eval_score = evaluate(position)
        
        if eval_score >= 2.0:
            # Confident - execute immediately
            move = self.execute_immediately(position)
            
        # STATE 2: Check information (computed from series)
        elif not has_information(self.series, position):
            # No information - randomize
            move = self.randomize(position)
            
        # STATE 3: Search with rate limiters (computed from series)
        else:
            # Have info, not confident - search
            R = combined_rate_limiter(self.series, t, budget)
            depth = self.adaptive_depth(R)
            move = self.search(position, depth)
        
        # Add to series
        self.append_to_series(t, position, move, eval_score)
        
        return move
    
    def execute_immediately(self, position):
        """
        STATE 1: Confident execution
        No data structures, just compute best move
        """
        legal = generate_legal_moves(position)
        
        # Evaluate each move
        evaluations = [
            (move, evaluate(apply_move(position, move)))
            for move in legal
        ]
        
        # Return best
        best_move, _ = max(evaluations, key=lambda x: x[1])
        return best_move
    
    def randomize(self, position):
        """
        STATE 2: Random exploration
        No storage, just random selection
        """
        legal = generate_legal_moves(position)
        return random.choice(legal)
    
    def search(self, position, depth):
        """
        STATE 3: Alpha-beta search
        No transposition table, pure recursive computation
        """
        best_move, _ = self.alpha_beta(
            position, 
            depth, 
            -999, 
            999, 
            True
        )
        return best_move
    
    def alpha_beta(self, position, depth, alpha, beta, maximizing):
        """
        Alpha-beta without transposition table
        Pure computation
        """
        if depth == 0:
            return None, evaluate(position)
        
        legal = generate_legal_moves(position)
        
        if maximizing:
            max_eval = -999
            best_move = None
            
            for move in legal:
                next_pos = apply_move(position, move)
                _, eval_score = self.alpha_beta(
                    next_pos, 
                    depth - 1, 
                    alpha, 
                    beta, 
                    False
                )
                
                if eval_score > max_eval:
                    max_eval = eval_score
                    best_move = move
                
                alpha = max(alpha, eval_score)
                if beta <= alpha:
                    break
            
            return best_move, max_eval
        
        else:
            min_eval = 999
            best_move = None
            
            for move in legal:
                next_pos = apply_move(position, move)
                _, eval_score = self.alpha_beta(
                    next_pos, 
                    depth - 1, 
                    alpha, 
                    beta, 
                    True
                )
                
                if eval_score < min_eval:
                    min_eval = eval_score
                    best_move = move
                
                beta = min(beta, eval_score)
                if beta <= alpha:
                    break
            
            return best_move, min_eval
    
    def adaptive_depth(self, R):
        """
        Compute search depth from rate limiter
        No state, pure function
        """
        if R > 0.7:
            return 5
        elif R > 0.4:
            return 3
        else:
            return 1
    
    def append_to_series(self, t, position, move, eval_score):
        """
        Add state to series
        Single source of truth
        """
        state = {
            't': t,
            'position': position,
            'move': move,
            'eval': eval_score,
            'timestamp': time.time()
        }
        self.series.append(state)

That’s it. ~100 lines. No data structures except series.


Part 11: Comparison to Post 858

What Changed

Post 858 (stores data):

class MinimalChessSolver:
    def __init__(self):
        self.transposition_table = {}  # Dictionary
        self.positions_explored = 0    # Counter  
        self.time_budget = 60          # Variable

Post 859 (computes data):

class PureSeriesChessSolver:
    def __init__(self):
        self.series = []  # Only series
        
    # Everything else computed on demand from series

Removed:

  • ❌ Transposition table (compute from series)
  • ❌ Position counter (compute from series length)
  • ❌ Time variable (compute from timestamps)
  • ❌ All state variables

Added:

  • ✅ Pure computation functions
  • ✅ Series search functions
  • ✅ Stateless rate limiters

Part 12: Benefits of Pure Series

Why Compute Not Duplicate

1. No duplication:

# Post 858: Position stored in series AND dictionary
self.series.append(state)
self.transposition_table[position] = move  # Duplicate!

# Post 859: Position stored once in series
self.series.append(state)  # Single source of truth

2. No synchronization issues:

# Post 858: Must keep dictionary and series in sync
self.series.append(state)
self.transposition_table[position] = move  # Must match!

# Post 859: Series is always consistent
self.series.append(state)  # Done - always in sync

3. Simpler state:

# Post 858: Multiple state variables
transposition_table = {...}
positions_explored = 42
time_budget = 60
# Which is source of truth?

# Post 859: One series
series = [...]  # Everything here

4. Time travel:

# Post 859: Can compute state at any time t
state_at_t5 = series[5]
state_at_t10 = series[10]

# Can recompute anything from series
eval_at_t5 = evaluate(series[5]['position'])

Part 13: From Post 810 Universal Format

Series Evolution Formula

Post 810 formula:

data(t+1, p) = f(data(t, p)) + entropy(p)

Applied to chess:

def evolve_series(series, t, entropy):
    """
    Chess series evolution
    Following Post 810 universal format
    """
    if t == 0:
        # Initial state
        return initial_position()
    
    # Current state
    current = series[t-1]
    
    # Deterministic component: f(data(t))
    position = current['position']
    confidence = compute_confidence(position)
    information = compute_information(series, position)
    
    # Entropy component: random exploration
    if information == 0:
        # STATE 2: Inject entropy
        move = random_move(position, entropy)
    elif confidence >= 0.8:
        # STATE 1: Deterministic (no entropy)
        move = best_move(position)
    else:
        # STATE 3: Guided search (small entropy in move ordering)
        move = search_move(position, entropy)
    
    # Next state
    next_position = apply_move(position, move)
    next_eval = evaluate(next_position)
    
    return {
        't': t,
        'position': next_position,
        'move': move,
        'eval': next_eval,
        'entropy': entropy
    }

Chess game = series evolving via Post 810 formula.


Part 14: Universal-Model Philosophy

Compute Instead of Duplicate

From universal-model README:

Compute values on demand
Don't store redundant data
Single source of truth: series

Applied to chess:

Traditional approach:

  • Store positions in multiple places
  • Cache evaluations
  • Duplicate move history
  • Synchronize data structures

Universal approach:

  • Positions in series only
  • Evaluate on demand
  • Moves in series only
  • Series is source of truth

Benefits:

  • Simpler code
  • No synchronization bugs
  • Time-traveling possible
  • Pure functions

Part 15: Performance Considerations

Is Computing Always Slower?

Not necessarily:

1. Series search is fast:

# Linear search through series
# For chess game: ~100 moves average
# 100 comparisons = negligible

# Dictionary lookup:
# O(1) but with overhead
# Hash computation, collision handling

# For small n, linear ≈ hash

2. No memory allocation:

# Post 858: Allocate dictionary entries
transposition_table[pos] = move  # Memory allocation

# Post 859: No allocation
# Just search existing series

3. Cache-friendly:

# Series: Sequential memory access
# Good cache locality

# Dictionary: Random memory access
# Poor cache locality

4. Evaluation is cheap:

# Material count: O(64) square scan
# Positional score: O(64) square scan
# Total: ~100 operations

# Lookup overhead:
# Hash function: ~50 operations
# Not much savings

Part 16: When to Compute vs Store

Guidelines

Compute when:

  • Value cheap to calculate
  • Used infrequently
  • Data already in series
  • Small series size

Store when:

  • Value expensive to calculate
  • Used very frequently
  • Data not in series
  • Large series size

For chess:

  • Evaluation: Compute (cheap, ~100 ops)
  • Position seen: Compute (linear search fast for ~100 moves)
  • Time elapsed: Compute (subtraction)
  • Position count: Compute (series length)
  • Move history: Already in series

Most things: Compute, not store.


Part 17: Series as Only Data Structure

Everything from Series

# Query any value from series

def get_position_at_t(series, t):
    """Position at time t"""
    return series[t]['position']

def get_move_at_t(series, t):
    """Move at time t"""
    return series[t]['move']

def get_evaluation_at_t(series, t):
    """Evaluation at time t (compute if not stored)"""
    if 'eval' in series[t]:
        return series[t]['eval']
    else:
        return evaluate(series[t]['position'])

def get_all_positions(series):
    """All positions in game"""
    return [state['position'] for state in series]

def get_move_history(series):
    """All moves in order"""
    return [state['move'] for state in series]

def get_evaluation_series(series):
    """Evaluation over time"""
    return [
        evaluate(state['position']) 
        for state in series
    ]

def find_position_occurrences(series, position):
    """Times position appeared"""
    return [
        state['t'] 
        for state in series 
        if state['position'] == position
    ]

Series answers all questions. No other data needed.


Part 18: Functional Purity

Pure Functions Only

# All functions pure (no side effects)

def evaluate(position):
    """Pure: same input → same output"""
    return material_balance(position) + positional_score(position)

def has_information(series, position):
    """Pure: only reads series, doesn't modify"""
    return any(s['position'] == position for s in series)

def rate_limiter(series, t, budget):
    """Pure: computes from series"""
    return compute_R(series, t, budget)

def choose_move(series, position, budget):
    """Pure: returns move, doesn't mutate"""
    # Compute everything from inputs
    eval_score = evaluate(position)
    info = has_information(series, position)
    R = rate_limiter(series, len(series), budget)
    
    # Return move based on computations
    if eval_score >= 2.0:
        return execute_immediately(position)
    elif info == 0:
        return randomize(position)
    else:
        return search(position, adaptive_depth(R))

All pure functions. Easier to:

  • Test
  • Debug
  • Reason about
  • Parallelize

Part 19: Series Queries

Rich Query Interface

def query_series(series):
    """
    Rich queries from single series
    No indexes, no caching - pure computation
    """
    return {
        # Basic queries
        'game_length': len(series),
        'current_position': series[-1]['position'],
        'current_eval': evaluate(series[-1]['position']),
        
        # Time queries
        'time_elapsed': time_elapsed(series),
        'avg_time_per_move': time_per_move(series),
        
        # Position queries
        'positions_explored': len(series),
        'unique_positions': len(set(s['position'] for s in series)),
        'repeated_positions': find_repetitions(series),
        
        # Evaluation queries
        'max_eval': max(evaluate(s['position']) for s in series),
        'min_eval': min(evaluate(s['position']) for s in series),
        'eval_trend': [evaluate(s['position']) for s in series],
        
        # Move queries
        'most_common_move': most_common_move(series),
        'move_diversity': len(set(s['move'] for s in series)),
        
        # State queries
        'confident_moves': sum(1 for s in series if evaluate(s['position']) >= 2.0),
        'exploratory_moves': sum(1 for s in series if not has_info_before(series, s)),
        'search_moves': len(series) - confident_moves - exploratory_moves
    }

Everything computable from series. No separate analytics storage.


Conclusion

Pure Series Chess Solver

What We’ve Achieved:

1. Single data structure:

  • Only series
  • No dictionaries
  • No counters
  • No state variables

2. Compute not duplicate:

  • Positions computed from series
  • Evaluations computed on demand
  • Rate limiters computed from series
  • Everything derived, nothing stored

3. Universal-model philosophy:

  • Follows Post 810 format
  • Series evolution via formula
  • Pure functions only
  • Stateless design

4. Same functionality as Post 858:

  • Three-state decision
  • Four rate limiters
  • Adaptive depth
  • Self-improving

5. Simpler implementation:

  • Fewer lines of code
  • Easier to understand
  • No synchronization
  • Single source of truth

The formula:

Chess solver = {
  series: []  // Only data structure
  
  Everything else computed:
    position_at(t) = series[t]['position']
    has_information(pos) = search_series(pos)
    rate_limiter(t) = f(series, t)
    move = choose(series, position)
    
  Append to series:
    series.append(new_state)
}

Minimal sufficient structure:

  • Single series
  • Pure functions
  • Compute on demand
  • Nothing duplicated

From Post 810: data(t+1) = f(data(t)) + entropy

Applied to chess: state(t+1) = evolve(state(t)) + random_move

Pure series. Pure functions. Pure chess.

∞


References:

  • Post 858: Minimal Chess Solver - Three-state solver (with data structures)
  • Post 810: Universal Format - Series evolution formula
  • Post 824: Chess as Data Series Graph - Chess as nodes
  • universal-model: Compute instead of duplicate philosophy

Created: 2026-02-17
Status: ♟️ Pure series chess solver

∞

Back to Gallery
View source on GitLab