$ cat writeup.md…
$ cat writeup.md…
sekai2026
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 cells with adaptive waits.
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.
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:
ESC[<0;40;13M ESC[<0;40;13m
After starting, the game board is a 12x7 match-3 grid. The visible gem symbols are:
♣ ♢ ♡ ◼ ▲ ● ♠
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:
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.
The automation loop is:
bejeweled.chals.sekai.team:1337.[ Start ] button.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:
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:
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:
*** GAME CLEAR! *** You reached level 8! Final score: 17610 Time: 43s SEKAI{REDACTED}
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar