Where it came from
My friend Robie showed me the Dotsies typeface sometime in undergrad. He has a passing interest in linguistics and I guess some of it rubbed off. Dotsies is an alternative alphabet where each letter is encoded as a vertical column of five dots — present or absent — which means every letter is just a 5-bit pattern.
The thing that hooked me is that 5 bits gives you 32 possible values, and the English alphabet only has 26 letters. You have enough room for the full grapheme set plus punctuation, all packed into a bitset. As soon as I thought about it that way, the cipher angle was obvious — if every letter is a number, you can do arithmetic on text. Shift bits, swap columns, rotate rows. Standard substitution ciphers but operating directly on the binary representation of the alphabet rather than a lookup table.
So I built a C++ framework to play with that: encoding text into Dotsies patterns, applying transformations, and then trying to work backwards from ciphertext using frequency analysis.
Architecture
The framework is split into a handful of focused components:
| Component | Purpose | Key Features |
|---|---|---|
letter.h/cpp |
5-bit pattern representation | Bit manipulation, circular shifts, pattern analysis |
ciphertext.h/cpp |
Text container and transformations | Row/column operations, batch processing, bounds checking |
table.h/cpp |
Character mapping system | Letter-to-pattern mapping, custom alphabets |
parser.h/cpp |
Text parsing and conversion | String-to-ciphertext conversion, validation |
decrypter.h/cpp |
Cryptographic analysis | Frequency analysis, pattern matching, optimization algorithms |
There are multiple build targets depending on what you need — the full ncurses interactive version, a text-only build for systems without ncurses, a standalone decrypter demo, and some matrix operation examples. The Makefile handles all of it:
# Compilation flags
CFLAGS = -g -Wall -O3 -march=native -std=c++11
# Feature flags
-DNO_NCURSES # Disable ncurses for text-only builds
-DNDEBUG # Disable debug assertions for release builds
Usage
Basic letter operations
// Create a letter with specific bit pattern
letter l(std::bitset<5>("10101"));
// Perform circular shift
l.circle_shift(true, 2); // Shift right by 2 positions
// Check bit values
bool bit3 = l.get(3);
l.set(2, true);
// Print pattern
l.printb(); // Outputs: "# # #"
Text encryption
// Create ciphertext from string
table t; // Default alphabet mapping
parser ps(t);
ciphertext ct = ciphertext::from_string("hello world", t);
// Apply transformations
ct.row_shift(1, true, 2); // Shift row 1 right by 2
ct.column_swap(0, 4); // Swap columns 0 and 4
ct.column_shift(true, 1); // Rotate all columns right by 1
Decryption analysis
// Initialize decrypter with dictionary
decrypter dec(ps, "dictionary.txt");
// Analyze encrypted text
dec.analyze(encrypted_ct);
// Get best decryption attempt
std::string result = dec.best_guess(encrypted_ct);
// Advanced decryption with optimization
auto result = dec.advanced_decrypt(encrypted_ct, 50);
Sample output
=== DotsiesCipherGen Decrypter Demo ===
1. Encrypt a custom message
2. Use a sample message
3. Decrypt an existing ciphertext
4. Exit
Ciphertext information:
Length: 43 letters
Bit patterns:
Letter 0: 10101
Letter 1: 11010
Letter 2: 01101
...
===== Results Comparison =====
Original: the quick brown fox jumps over the lazy dog
Decrypted: the quick brown fox jumps over the lazy dog
Character match: 43/43 (100.00%)
Decryption SUCCESS!
Technical notes
The decryption side was the most interesting problem. Since the cipher space is small (5 bits per letter), brute force is tractable for short messages, but for anything longer you need a smarter approach. The decrypter uses English letter frequency matching as a starting point, then n-gram analysis to score candidate decryptions, with simulated annealing to escape local optima. It's not novel cryptanalysis — these are standard techniques — but applying them to a bit-manipulation cipher rather than a traditional substitution cipher was a fun constraint to work within.
Algorithm complexity at a glance:
- Letter operations — O(1) for bit manipulations
- Text transformations — O(n) where n is text length
- Decryption — O(kⁿ) naive, brought down significantly with frequency analysis + annealing
The build uses C++11 throughout — std::bitset for the patterns, std::unique_ptr where ownership is clear, move semantics to avoid unnecessary copies on the larger ciphertext objects. Bounds checking is gated behind NDEBUG so debug builds are safe and release builds are fast.