A solver for the Gothic 1 Remake lockpicking minigame, plus optional playback that types the solution into the game for you.
The solver treats the puzzle as a shortest-path problem on an unweighted graph and uses breadth-first search, so the sequence it returns is provably the minimum number of moves for that lock.
A lock has N plates (usually 4 to 7, we denote the plates alphabetically - a, b, c, ...). Each plate sits at one of 7 positions, numbered 1 ( the plate is fully right, hence the pin is in the left most hole) … 7 (the plate is fully left, hence the pin is in the right most hole). You win when every plate is at position 4 (middle position).
The catch: plates are coupled. Moving one plate also moves others, according to
rules specific to each lock — e.g. "moving plate a right also moves c and d right,
and b left". The coupling is mirrored: if moving a right pushes b left, then moving
a left pushes b right. That's why only one direction per plate has to be described.
| Key | Action |
|---|---|
w |
move cursor down the plate list (a → b → c → …) |
s |
move cursor up the plate list (… → c → b → a) |
a |
move the selected plate left (position +1) |
d |
move the selected plate right (position −1) |
The cursor always starts on plate a when a lock opens. There is no wrap-around at the
ends of the list.
Requires uv. From the repo root:
uv sync
That creates .venv/ and installs pydirectinput (sending keys the game will actually
read) and keyboard (the global start/abort hotkey).
Open lock_breaker.py and edit two module-level values.
start — the plate positions currently on screen, left to right:
start = (2, 3, 5, 7, 6, 1)plate_couplings — one vector per plate, each describing what happens when you move
that plate right. Entry i of a vector is the effect on plate i:
plate_couplings = [
(1, 1, 1, 1, 0, 0), # moving plate a right: a, b, c, d all move right
(-1, 1, -1, 0, 0, 0), # moving plate b right: b right; a and c move left
(0, 0, 1, 0, 0, 0), # moving plate c right: only c moves
...
]You only describe the "right" direction — the solver derives "left" by negating each
vector. plate_couplings must have one vector per plate, and every vector must be the
same length as start.
A mistyped vector is the most common failure. It produces a plausible-looking solution that simply doesn't work in-game. Double-check the couplings against the screen before running.
Solve only — prints the move list:
uv run lock_breaker.py
Output is the minimal sequence of plate moves:
['c-left', 'c-left', 'e-right', 'a-left', ...]
Solve and play it into the game:
uv run lock_player.py
- It prints the solution and waits.
- Alt-tab into Gothic. Make sure the lockpicking screen is focused and the cursor is
on plate
a. - Press F8 to play the sequence.
- Press ESC at any time to abort.
Playback tunables live at the top of lock_player.py: HOTKEY,
ABORT_KEY, KEY_DELAY, KEY_HOLD, COUNTDOWN.
- On Windows the
keyboardlibrary's global hotkey may need the terminal to run as administrator to register while the game has focus. - Move counts can be large — 68 moves has been observed for a lock with awkward coupling. That is genuinely the minimum for that lock, not a bug; BFS proves no shorter sequence exists.
- Playback uses
pydirectinputrather thanpyautoguiorkeyboard, because games read DirectInput/raw input and typically ignore window-message-based synthetic input.