← Back to Writeups
CUSTOMMediumCoding

Bejeweled

XESXOR9/10/20264 min read
#coding#custom#medium#adaptive_waiting#ansi_terminal_emulation#greedy_move_search#grid_parsing#sgr_mouse_click_injection

Bejeweled

Platform: CUSTOM | Category: Coding | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: adaptive_waiting, ansi_terminal_emulation, greedy_move_search, grid_parsing, sgr_mouse_click_injection

Summary

Walkthrough of the CUSTOM Coding Bejeweled. Task: an interactive TCP Bejeweled terminal game using ANSI drawing and SGR mouse input. Solution: emulate the terminal screen, parse the 12x7 grid, greedily choose legal match-3 swaps, and click cell

Description

The stage is yours. Clear the board before the final note fades. socat -,raw,echo=0 tcp:bejeweled.chals.sekai.team:1337

English summary: the service launches a terminal Bejeweled-style game over TCP. The goal is to automate valid mouse input quickly enough to clear the game before the timer expires.

Analysis

Connecting to the service shows an ANSI alternate-screen terminal UI, not a line-oriented protocol. The initial screen requires at least an 80x24 terminal and displays a [ Start ] button. The application enables mouse reporting and accepts SGR mouse events, so the start button can be pressed by sending:

text
ESC[<0;40;13M ESC[<0;40;13m

After starting, the game board is a 12x7 match-3 grid. The visible gem symbols are:

text
♣ ♢ ♡ ◼ ▲ ● ♠

The grid is drawn at stable terminal coordinates. For zero-based column c in [0..6] and row r in [0..11], the cell centers are:

python
x = 19 + 6*c   # terminal column
y = 2 + 2*r    # terminal row

This made it possible to ignore most of the UI and maintain a small virtual terminal screen from ANSI cursor-position writes. The solver parses CSI row;col H updates, ignores styling and mouse-mode escape sequences, and reads gem glyphs near each cell center.

Solution

The automation loop is:

  1. Connect to bejeweled.chals.sekai.team:1337.
  2. Parse incoming ANSI output into a virtual screen buffer.
  3. Send the SGR mouse down/up sequence for the [ Start ] button.
  4. Extract the 12x7 board from fixed cell coordinates.
  5. Enumerate every adjacent horizontal/vertical swap.
  6. For each swap, check whether it creates any horizontal or vertical run of 3+ identical gems.
  7. Pick the best greedy move, preferring more cleared cells and matches lower on the board.
  8. Send two SGR mouse clicks at the selected cell centers.
  9. Wait only until the score or parsed grid changes, then immediately compute the next move.

The timing optimization was important. A first version slept a fixed long delay after every swap and only reached score 9855, level 4 before the 45-second timeout. The successful version waits adaptively for a score or board change, which avoids wasting time and reaches level 8 within 43 seconds.

Core snippets from tasks/sekai2026/Bejeweled/solve.py:

python
GEMS = "♣♢♡◼▲●♠"
R, C = 12, 7
CELL_X = [19 + 6 * i for i in range(C)]
CELL_Y = [2 + 2 * i for i in range(R)]

def click(s, r, c):
    x, y = CELL_X[c], CELL_Y[r]
    s.sendall(f"\x1b[<0;{x};{y}M\x1b[<0;{x};{y}m".encode())

def matches(g):
    m = set()
    for r in range(R):
        c = 0
        while c < C:
            c2 = c + 1
            while c2 < C and g[r][c2] == g[r][c]:
                c2 += 1
            if c2 - c >= 3:
                m.update((r, k) for k in range(c, c2))
            c = c2
    for c in range(C):
        r = 0
        while r < R:
            r2 = r + 1
            while r2 < R and g[r2][c] == g[r][c]:
                r2 += 1
            if r2 - r >= 3:
                m.update((k, c) for k in range(r, r2))
            r = r2
    return m

def best_move(g):
    best = None
    for r in range(R):
        for c in range(C):
            for dr, dc in [(1, 0), (0, 1)]:
                rr, cc = r + dr, c + dc
                if rr >= R or cc >= C or g[r][c] == g[rr][cc]:
                    continue
                h = [row[:] for row in g]
                h[r][c], h[rr][cc] = h[rr][cc], h[r][c]
                mm = matches(h)
                if mm:
                    score = len(mm) * 100 + sum(x for x, y in mm)
                    if not best or score > best[0]:
                        best = (score, (r, c, rr, cc), len(mm))
    return None if best is None else best[1:]

The adaptive wait after each pair of clicks looks like this:

python
old_score = vals.get("score", -1)
old_grid = [row[:] for row in g]
click(s, r, c)
time.sleep(0.005)
click(s, rr, cc)

deadline = time.time() + 0.42
while time.time() < deadline:
    recv_some(s, term, 0.035)
    vals_now = term.score_level_time()
    gg = term.grid()
    if vals_now.get("score", old_score) != old_score or (gg and gg != old_grid):
        break

The successful run printed:

text
*** GAME CLEAR! ***
You reached level 8!
Final score: 17610
Time:        43s
SEKAI{REDACTED}

Key Takeaways / Lessons

  • Mastering adaptive_waiting, ansi_terminal_emulation, greedy_move_search, grid_parsing, sgr_mouse_click_injection was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-06-28 • CUSTOM • Bejeweled • Difficulty: Medium