← Back to list

Mastering Oware: From Human Strategy to Artificial Intelligence

Oware (also known as Awari) is one of the more popular variants of mancala. It is a strategy game played on a board of twelve pits (six on…

Jik Alvin · 2025-11-05 10:58 · 0 claps · 7.2 min read
#ai #game-theory #reinforcement-learning #owaré #a3c
Open on Medium ↗
Wiki topics: AI · AI · General EDU · Education & Learning

Mastering Oware: From Human Strategy to Artificial Intelligence

Oware (also known as Awari) is one of the more popular variants of mancala. It is a strategy game played on a board of twelve pits (six on each side), with four seeds in each pit initially. Players alternate turns sowing and capturing seeds with the goal of collecting more seeds than their opponent. In Cameroon, you will likely know it as Songo. The video below shows a typical game in progress along with the basic rules:

[embed]

Human Strategy and Tactics

As with many board games, Oware is not a game of chance, the strongest players rely on well-planned strategies and execute well-known tactics with clear purpose. The fundamental strategy consists of moving game pieces to obtain favorable positions that enable future captures. Players minimize the number of holes containing fewer than three seeds in their own row (reducing vulnerability to captures) while maximizing such holes in the opponent’s row (increasing capture opportunities). A particularly powerful strategy involves accumulating twelve or more seeds in a single hole — called a kroo — which can complete a full turn around the board and potentially capture up to fifteen seeds in one move. Allowing an opponent to execute such a capture is often an unrecoverable strategic mistake.

Expert players employ several common tactics to gain advantage: multi-hole attacks that threaten two or more opponent holes simultaneously, forcing unavoidable captures; seed hoarding where players accumulate seeds in their territory while distributing minimal seeds to opponent pits, controlling the game’s pace; winning time by forcing opponents to provide seeds that create strategic advantages; and starvation prevention to maintain legal moves and avoid automatic loss. These human strategies form the foundation for heuristic evaluation functions in AI agents — understanding how experts evaluate positions, prioritize moves, and execute tactical sequences provides the knowledge base that can be encoded into algorithmic decision-making, pattern recognition, and opening/endgame databases.

AI Approaches to Mastering Oware

Having established the strategic foundations of human play, we now explore how Artificial Intelligence methods have been applied to model and master this ancient game. Each approach represents a different philosophy of decision-making, from random exploration to deep reinforcement learning.

The Random Agent

The Random Agent serves as a baseline model, selecting a legal move at random without strategic evaluation.

def random_agent(state):
    """
    Random Agent Algorithm
    Selects a random legal move without strategy
    """
    moves = get_legal_moves(state)
    if not moves:
        return None

    move = random.choice(moves)
    new_state = apply_move(state, move)
    return new_state

This is like if you told a child the rules and at at each step just told them to pick any move and play. It is purely stochastic and lacks intelligence, yet useful for benchmarking performance of other algorithms.

The Max Agent

You may remember greed as one of the seven deadly sins, but it is also a powerful motivator in decision-making. The Max Agent takes a greedy approach, choosing the move that yields the maximum immediate reward.

def max_agent(state):
    """
    Immediate Reward Maximization Algorithm
    Chooses move with highest immediate gain
    """
    best_move = None
    best_value = float('-inf')

    for move in get_legal_moves(state):
        temp_state = simulate_move(state, move)
        reward = seeds_captured(temp_state) - seeds_captured(state)

        if reward > best_value:
            best_value = reward
            best_move = move

    return best_move

The motivation for this is to optimize for short-term gain. A shortcoming is that it fails to consider long-term strategic consequences.

Minimax Algorithm

The Minimax algorithm models Oware as a two-player zero-sum game. Each player alternates between maximizing and minimizing the score. It is based on a tree representation of game states.

def minimax(state, depth, maximizing_player):
    """
    Minimax Search Algorithm
    Explores game tree to find optimal move
    """
    if depth == 0 or is_terminal(state):
        return evaluate(state)

    if maximizing_player:
        max_eval = float('-inf')
        for move in get_legal_moves(state):
            eval_score = minimax(
                apply_move(state, move), 
                depth - 1, 
                False
            )
            max_eval = max(max_eval, eval_score)
        return max_eval
    else:
        min_eval = float('inf')
        for move in get_legal_moves(state):
            eval_score = minimax(
                apply_move(state, move), 
                depth - 1, 
                True
            )
            min_eval = min(min_eval, eval_score)
        return min_eval

This approach guarantees optimal results within the explored depth, but suffers from exponential growth in computation.

Alpha-Beta Pruning

Alpha-Beta Pruning improves Minimax by skipping branches that cannot influence the final decision.

def alpha_beta(state, depth, alpha, beta, maximizing_player):
    """
    Alpha-Beta Pruning Algorithm
    Optimizes minimax by pruning irrelevant branches
    """
    if depth == 0 or is_terminal(state):
        return evaluate(state)

    if maximizing_player:
        value = float('-inf')
        for move in get_legal_moves(state):
            value = max(
                value, 
                alpha_beta(
                    apply_move(state, move), 
                    depth - 1, 
                    alpha, 
                    beta, 
                    False
                )
            )
            alpha = max(alpha, value)
            if beta <= alpha:
                break  # Beta cutoff - prune
        return value
    else:
        value = float('inf')
        for move in get_legal_moves(state):
            value = min(
                value, 
                alpha_beta(
                    apply_move(state, move), 
                    depth - 1, 
                    alpha, 
                    beta, 
                    True
                )
            )
            beta = min(beta, value)
            if beta <= alpha:
                break  # Alpha cutoff - prune
        return value

This approach produces the same result as Minimax while drastically reducing computational effort.

Advanced Heuristic Minimax

This version enhances Alpha-Beta Minimax by introducing heuristic knowledge. The evaluation function combines weighted features derived from expert strategies.

The heuristic evaluation is computed as a weighted sum:

Heuristic Descriptions:

H1 — Hoard seeds in one pit: Secure long-term collection

H2 — Keep seeds on player’s side: Prevent being starved

H3 — Maximize available moves: Maintain flexibility

H4 — Maximize seeds in store: Direct scoring advantage

H5 — Move from rightmost pit: Control near-opponent side

H6 — Minimize opponent’s score: Defensive anticipation

H7 — Maximize repeat turns: Enable chained moves

H8 — Points difference: Evaluate current lead

H9 — Closeness to winning: Track victory threshold

H10 — Opponent closeness to winning: Avoid defeat

def weighted_heuristic(state, weights):
    """
    Computes weighted heuristic evaluation
    """
    heuristics = [
        hoard_seeds(state),           # H1
        keep_seeds_own_side(state),   # H2
        count_available_moves(state), # H3
        seeds_in_store(state),        # H4
        rightmost_pit_value(state),   # H5
        opponent_score(state),        # H6
        repeat_turn_potential(state), # H7
        point_difference(state),      # H8
        closeness_to_win(state),      # H9
        opponent_closeness_win(state) # H10
    ]

    return sum(h * w for h, w in zip(heuristics, weights))

def heuristic_minimax(state, depth, alpha, beta, maximizing_player, weights):
    """
    Heuristic-enhanced Minimax with Alpha-Beta Pruning
    """
    if depth == 0 or is_terminal(state):
        return weighted_heuristic(state, weights)

    if maximizing_player:
        value = float('-inf')
        for move in get_legal_moves(state):
            eval_score = heuristic_minimax(
                apply_move(state, move), 
                depth - 1, 
                alpha, 
                beta, 
                False,
                weights
            )
            value = max(value, eval_score)
            alpha = max(alpha, value)
            if beta <= alpha:
                break
        return value
    else:
        value = float('inf')
        for move in get_legal_moves(state):
            eval_score = heuristic_minimax(
                apply_move(state, move), 
                depth - 1, 
                alpha, 
                beta, 
                True,
                weights
            )
            value = min(value, eval_score)
            beta = min(beta, value)
            if beta <= alpha:
                break
        return value

This strategy draws directly from the strategic principles employed by human experts, allowing the AI to evaluate positions with greater nuance. We described a few of these strategies when we discussed how a human would play the game.

Monte Carlo Tree Search (MCTS)

MCTS uses random playouts to statistically estimate the value of moves rather than explicit evaluation.

import math

class MCTSNode:
    def __init__(self, state, parent=None):
        self.state = state
        self.parent = parent
        self.children = []
        self.wins = 0
        self.visits = 0
        self.untried_moves = get_legal_moves(state)

    def ucb_score(self, c=1.414):
        """Upper Confidence Bound for Trees"""
        if self.visits == 0:
            return float('inf')
        return (self.wins / self.visits) + 
               c * math.sqrt(math.log(self.parent.visits) / self.visits)

def mcts(root_state, iterations):
    """
    Monte Carlo Tree Search Algorithm
    """
    root = MCTSNode(root_state)

    for i in range(iterations):
        # Selection
        node = root
        while not node.untried_moves and node.children:
            node = max(node.children, key=lambda n: n.ucb_score())

        # Expansion
        if node.untried_moves:
            move = random.choice(node.untried_moves)
            node.untried_moves.remove(move)
            child_state = apply_move(node.state, move)
            child = MCTSNode(child_state, parent=node)
            node.children.append(child)
            node = child

        # Simulation
        state = node.state
        while not is_terminal(state):
            move = random.choice(get_legal_moves(state))
            state = apply_move(state, move)

        result = get_winner(state)

        # Backpropagation
        while node:
            node.visits += 1
            node.wins += result
            node = node.parent

    return max(root.children, key=lambda n: n.visits).state

The process balances exploration and exploitation using the UCB formula:

Where:

  • $w_i$ = wins for node $i$

  • $n_i$ = visits to node $i$

  • $N$ = total parent visits

  • $c$ = exploration parameter

Excels in complex environments, dynamically adapting to different game states.

Asynchronous Advantage Actor-Critic (A3C) Agent

The A3C Agent employs reinforcement learning, combining multiple asynchronous agents that learn policy and value functions concurrently.

import torch
import torch.nn as nn
import torch.optim as optim

class A3CNetwork(nn.Module):
    def __init__(self, state_size, action_size):
        super(A3CNetwork, self).__init__()
        self.fc1 = nn.Linear(state_size, 128)
        self.fc2 = nn.Linear(128, 64)

        # Actor (policy) head
        self.policy = nn.Linear(64, action_size)

        # Critic (value) head
        self.value = nn.Linear(64, 1)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = torch.relu(self.fc2(x))

        policy = torch.softmax(self.policy(x), dim=-1)
        value = self.value(x)

        return policy, value

def a3c_agent(global_network, optimizer, gamma=0.99, max_steps=100):
    """
    A3C Reinforcement Learning Algorithm
    """
    local_network = A3CNetwork(state_size, action_size)
    state = get_initial_state()

    states, actions, rewards = [], [], []

    for t in range(max_steps):
        # Get action from policy
        state_tensor = torch.FloatTensor(state)
        policy, value = local_network(state_tensor)

        action = torch.multinomial(policy, 1).item()
        next_state, reward = step(state, action)

        states.append(state)
        actions.append(action)
        rewards.append(reward)

        if is_terminal(next_state):
            break

        state = next_state

    # Calculate returns
    R = 0 if is_terminal(next_state) else 
        local_network(torch.FloatTensor(next_state))[1].item()

    returns = []
    for r in reversed(rewards):
        R = r + gamma * R
        returns.insert(0, R)

    # Update global network
    optimizer.zero_grad()

    actor_loss = 0
    critic_loss = 0

    for state, action, R in zip(states, actions, returns):
        state_tensor = torch.FloatTensor(state)
        policy, value = local_network(state_tensor)

        advantage = R - value.item()

        # Actor loss
        actor_loss -= torch.log(policy[action]) * advantage

        # Critic loss
        critic_loss += (R - value) ** 2

    total_loss = actor_loss + 0.5 * critic_loss
    total_loss.backward()

    # Update global network parameters
    optimizer.step()

Learns from experience, achieving strong generalization without predefined heuristics.

Solving the Game

In 2002, John W. Romein and Henri E. Bal solved the game of Awari (Oware’s close variant). Using a parallel search algorithm across billions of positions, they discovered that optimal play from both sides leads to a draw.

Their database contained 204 billion entries (178 GB), one of the largest computed for any game at the time. This remarkable achievement demonstrated that:

(P1) Perfect play results in a draw

(P2) The game tree complexity is manageable with modern computing

(P3) Oware joins Checkers (draughts) as strongly solved games

Conclusion

We have reached the end, if you made it this far you probably enjoyed reading about it as much as I did. Oware is a very nice game with strategic elements that have captivated a lot of players. We have seen that even traditional African games are interesting enough to be studied by AI researchers, and that they can provide fertile ground for exploring concepts in game theory, reinforcement learning, and algorithm design.

References

(R1) Romein & Bal (2002): “Solving the Game of Awari using Parallel Retrograde Analysis”

(R2) Trevon J. Hunter: “The Exploration and Analysis of Mancala from an AI Perspective”

(R3) Joan Sala’s Oware Strategy Guide: https://auale.joansala.com/en/strategy/


메타데이터
post_id
e8cf5c8eb7b0
slug
mastering-oware-from-human-strategy-to-artificial-intelligence-e8cf5c8eb7b0
url
https://medium.com/@pamelafugua/mastering-oware-from-human-strategy-to-artificial-intelligence-e8cf5c8eb7b0
canonical_url
https://medium.com/@pamelafugua/mastering-oware-from-human-strategy-to-artificial-intelligence-e8cf5c8eb7b0
author_url
https://medium.com/@pamelafugua
status
ok
fetched_at
2026-06-23 17:05:31