Bitboard
Ternary Bitboard
Conversion
Game Development
Computer Science

Bitboard to titboard ternary bitboard conversion

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A normal bitboard can only store two states per square: bit set or bit clear. A ternary board representation needs three states, such as empty, black, and white, so the first thing to understand is that a single bitboard does not contain enough information by itself. You usually convert from two related bitboards into a per-square ternary representation.

Start With a Clear State Mapping

Suppose each square may be in one of three states:

  • '0 for empty'
  • '1 for player A'
  • '2 for player B'

A common binary representation is two 64-bit integers:

  • one bitboard for player A occupancy
  • one bitboard for player B occupancy

That is enough information to derive a ternary value per square. What you cannot do is convert a single occupancy bitboard into a ternary board unless some second source tells you which occupied squares belong to which state.

Convert Two Bitboards Into Ternary Cells

The simplest conversion is to build an array of 64 trits, one per board square.

cpp
1#include <array>
2#include <cstdint>
3#include <iostream>
4#include <stdexcept>
5
6std::array<uint8_t, 64> to_trits(uint64_t black, uint64_t white) {
7    if ((black & white) != 0) {
8        throw std::invalid_argument("overlapping bitboards are invalid");
9    }
10
11    std::array<uint8_t, 64> trits{};
12
13    for (int square = 0; square < 64; ++square) {
14        uint64_t mask = 1ULL << square;
15        if (black & mask) {
16            trits[square] = 1;
17        } else if (white & mask) {
18            trits[square] = 2;
19        } else {
20            trits[square] = 0;
21        }
22    }
23
24    return trits;
25}
26
27int main() {
28    uint64_t black = 0b0001;
29    uint64_t white = 0b0100;
30
31    auto trits = to_trits(black, white);
32    std::cout << int(trits[0]) << " " << int(trits[1]) << " " << int(trits[2]) << "\n";
33}

This representation is easy to debug and easy to feed into evaluation code. It is not the most compact possible format, but it makes the conversion logic obvious.

Pack Ternary Values Only If You Really Need To

If the goal is compression, you can pack trits into bytes or larger integers. Five trits fit inside one byte because 3^5 is 243, which is less than 256. That gives a denser representation than storing one full byte per square.

cpp
1#include <array>
2#include <cstdint>
3#include <vector>
4
5std::vector<uint8_t> pack_trits(const std::array<uint8_t, 64>& trits) {
6    std::vector<uint8_t> packed;
7
8    for (int i = 0; i < 64; i += 5) {
9        uint8_t value = 0;
10        uint8_t factor = 1;
11
12        for (int j = 0; j < 5 && i + j < 64; ++j) {
13            value += trits[i + j] * factor;
14            factor *= 3;
15        }
16
17        packed.push_back(value);
18    }
19
20    return packed;
21}

That second step is optional. Many engines keep the original bitboards for move generation and use ternary conversion only for serialization, hashing, or machine-learning features.

Decide Why You Want a Ternary Form

The right representation depends on the operation:

  • bitboards are excellent for fast bitwise move generation
  • ternary cells are easier for generic board inspection or export
  • packed trits are useful when compact storage matters

If your engine spends most of its time generating attacks or legal moves, converting everything into ternary form can slow down the hot path. In that case, derive ternary values only when another subsystem truly needs them.

Validate Before Converting

The biggest correctness rule is that the source bitboards must not overlap. If the same square is set in both bitboards, the ternary result is undefined because one square cannot simultaneously be in two non-empty states.

It is also worth agreeing on square ordering up front. Some code uses least-significant bit as the first square, while other code maps bits by rank and file differently. Two correct-looking conversion routines can disagree completely if they assume different square numbering.

Common Pitfalls

  • Trying to derive three states from one bitboard with no extra metadata.
  • Forgetting to reject overlapping source bitboards.
  • Mixing up square numbering conventions during conversion.
  • Packing trits too early and making debugging harder than necessary.
  • Replacing fast bitboard logic with ternary arrays in performance-critical code.

Summary

  • A single binary bitboard is not enough to represent a three-state board.
  • The usual conversion starts from two occupancy bitboards and maps each square to 0, 1, or 2.
  • An array of trits is the simplest and safest ternary representation.
  • Packed ternary storage is possible, but it is a separate optimization step.
  • Keep bitboards for fast engine logic and convert only where ternary data is actually useful.

Course illustration
Course illustration

All Rights Reserved.