⚖ Context note. This is my personal account of a recruitment process with Reef Technologies in June–July 2026. It combines facts from my correspondence, my own interpretation of the experience, and a technical walkthrough of code I wrote. It is not an allegation that the company or its employees committed fraud. The recruitment flow simply raised serious privacy, communication, and process questions for me.
ᯅ I solved a difficult recruitment game. The harder lesson was outside the code.
Most take-home assignments are framed as a one-way evaluation.
⌕ Can the candidate understand requirements?
⚒ Can they design a solution?
⚙ Can they write and test reliable code?
Those are fair questions. But after this experience I would add another:
⌘ Can the candidate evaluate the hiring process, its tools, and the access they are asked to give before they are hired?
I completed a gamified recruitment assignment for a Senior Python Backend Engineer role at Reef Technologies. The assignment itself was genuinely challenging and technically rewarding: three stages, sub-stages, gradually unlocked mechanics, a limited number of turns, resource logistics, production chains, and a remote API.
I finished all three stages, submitted the code, and built a reusable solver rather than a one-off script. Yet I did not progress further because I had not logged the work through Hubstaff, a desktop time-tracking tool that the company required candidates to use.
This article has two threads:
- ⛓ the engineering story: how I built a capability-driven Python solver that could reason about unknown game mechanics;
- ⚡ the candidate story: why I became cautious about an installation request that included desktop monitoring and screenshots, and what I would verify before taking a similar assignment again.
Both lessons matter.
✉ The onboarding request that made me pause
After completing an initial test, I received a congratulatory email and an invitation to the next stage. The message said that the task had to be tracked through the desktop version of Hubstaff. It also explained that the application could periodically take screenshots while I worked.
The request did not feel sufficiently clear and connected in my inbox at the moment I saw it. From my perspective, it looked like a request to install software on my personal computer that could collect work metrics and capture the screen. I treated that as a possible security issue, deleted the message, and later emptied the trash.
Only afterward, while dealing with the task and follow-up correspondence, did I connect the tracking requirement with the recruitment game itself.
⚖ The important nuance
The complete instruction email did contain the Hubstaff requirement. Not following it was my responsibility, and I do not want to rewrite the facts to make that disappear. The company later explained that Hubstaff tracking was an integral requirement because it reflects how their team tracks work time day-to-day. They chose not to move my application forward because no time was recorded there.
At the same time, I believe an installation requirement with screen monitoring deserves a more explicit, security-conscious onboarding flow.
◈ What I would expect before installing monitoring software
- ✉ A separate, clearly titled message: “Required software for the recruitment task.”
- ⌕ A direct explanation of why it is needed before employment.
- ⚲ A description of what data is collected: activity, screenshots, application names, URLs, or anything else.
- ♥ A retention and access policy: who can see the data, and for how long.
- ⛓ A verified source for the installer and a way to confirm that the request is authentic.
- ⏱ A prominent statement that missing this step blocks the next stage.
- ⚖ A safe alternative, if one exists: a separate device, a virtual machine, or an agreed evidence-based workflow.
I am not saying that Hubstaff is inherently unsafe, nor that the process was a scam. My point is narrower and practical: installing any screen-monitoring application on a personal machine is a meaningful privacy and security decision. A candidate is allowed to pause, verify, ask questions, and decline to execute an unclear installation request blindly.
⚡
TRUST != CLICK_FIRST_ASK_LATER
⏱ The apparent contradiction: the platform knew my progress
The task itself ran on the company’s web platform. It showed my progression through three stages and their sub-stages; after each completed task, the corresponding status indicators changed. After I submitted my solution, I was told that an engineer had checked the time I spent in their system — but that this was not the same as Hubstaff tracking.
This is where the process felt counterintuitive to me.
The platform could already establish several useful facts:
- ⌚ I had started the tasks;
- ✦ I had completed the required game stages;
- ⟁ tasks were completed in a visible sequence;
- ⌖ the final solution was submitted.
However, this is also where I can see the company’s side. Platform activity is not a reliable work-time log. It does not include reading specifications, setting up a Python environment, designing architecture, debugging algorithms, writing tests, or stepping away from the keyboard. It cannot accurately prove how long someone worked.
So the core issue was not whether their game platform had any evidence of progress. It did. The issue was that it did not satisfy their separate, explicit requirement for a time tracker.
⏲ My takeaway: a system can prove that you reached an outcome without proving how you spent every minute getting there.
Still, that distinction should be visible from the first line of the task brief — especially when the candidate is asked to run monitoring software during unpaid evaluation work.
⚒ The assignment: a logistics game, not a scripted puzzle
The technical task was much stronger than the administrative experience around it.
At a high level, the game was a turn-based logistics and production problem. A level contained:
- a map and terrain;
- a base with an inventory;
- resource nodes;
- structures already on the map;
- a goal inventory that had to be delivered to a target structure;
- a maximum number of turns.
The available mechanics evolved between stages. Early on, a simple solution might build a road, build a quarry, mine stone, transfer it to the base, and claim the win. That approach collapses as soon as the server introduces new resources, transport types, construction costs, recipes, or production structures.
So I chose a different target:
★ Do not solve one map. Build a solver that can interpret each stage.
The project became a small planning system with four independent layers.
✉ Remote game API
│
▼
◈ CapabilityRegistry
actions · resources · structures
│
▼
⚙ GameState
board · structures · storage · turns · goal
│ │
▼ ▼
⚖ Local validator ⚒ Strategy
replay + invariants plan + search
│ │
└───────⌘─────────┘
│
▼
valid JSON plan → server
The key idea is simple: the strategy must not depend directly on HTTP response shapes, and the server must not be the first place where an invalid plan is discovered.
❖ 1. Actions are real objects, not loose dictionaries
Every game action is a Python object. That gives it behaviour, type identity, string representation for debugging, and a clear mapping to the remote API.
class BuildAction(BaseAction):
def __init__(self, x: int, y: int, structure_type: StructureType | str):
self.x = x
self.y = y
self.type = structure_id(structure_type)
@property
def action_type(self) -> str:
return "BUILD"
def to_api_action(self) -> Action:
return Action(
action="BUILD",
args={"x": self.x, "y": self.y, "type": self.type},
)
def apply(self, game_state: GameState) -> None:
structure = game_state.make_structure(self.type, self.x, self.y)
game_state.base.storage.subtract_in_place(structure.build_cost)
game_state.add_structure(structure)
The same pattern is used for building, mining, transfers, production, and the final win claim:
BuildAction(x, y, structure_type)
ExtractAction(x, y)
TransferAction(path, resource, amount)
ProduceAction(x, y)
ClaimWinAction(x, y)
Why not just create JSON dictionaries in the strategy?
Because the solver has to answer a more important question before serialisation:
⚖ What does this action do to the world state?
If the action can be applied locally, the strategy can observe the consequences of its own choices immediately. A new road changes connectivity. A mine fills a local inventory. A transfer makes material available for construction at the base. Production consumes inputs and creates outputs.
That is the difference between printing a plausible request body and actually planning.
⛓ 2. Model resources as a first-class value object
Resources appear everywhere: construction costs, mine output, transfer amounts, recipes, and win conditions. I used a small Inventory abstraction rather than scattering dict.get(..., 0) across the codebase.
class Inventory:
def __getitem__(self, resource: ResourceKey) -> int:
return self._resources.get(resource_id(resource), 0)
def add(self, resource: ResourceKey, amount: int) -> None:
if amount > 0:
self[resource] = self[resource] + amount
def subtract_in_place(self, other: Inventory) -> None:
for resource, amount in other:
self.remove(resource, amount)
def at_least(self, other: Inventory) -> bool:
return all(self[resource] >= amount for resource, amount in other)
That made core intent readable:
if state.base.storage.at_least(state.goal_resources):
yield ClaimWinAction(state.base.x, state.base.y)
No repeated loops. No silent KeyError. No accidental mutation of the goal. Just a domain-level statement: the base has enough resources to satisfy the goal.
✦
INVENTORYis small code with a large effect on clarity.
⚙ 3. Interpret the stage instead of hardcoding its names
The game exposes stage capabilities: resources, actions, structures, their interfaces, their costs, and eventually their recipes. I converted that server-defined data into a CapabilityRegistry.
@dataclass(frozen=True)
class StructureSpec:
type: str
build_cost: Inventory
interfaces: frozenset[str]
rate: int | None = None
resource_allow: tuple[str, ...] = ()
recipe_inputs: Inventory = field(default_factory=Inventory)
recipe_outputs: Inventory = field(default_factory=Inventory)
@property
def is_extraction(self) -> bool:
return self.has_interface(StructureInterface.Extraction)
@property
def is_production(self) -> bool:
return self.has_interface(StructureInterface.Production)
Now the solver can select structures by their role:
def extractor_specs_for(self, resource: str) -> list[StructureSpec]:
return [
spec for spec in self.structure_specs.values()
if spec.is_buildable
and spec.is_storage
and spec.is_extraction
and (not spec.resource_allow or resource in spec.resource_allow)
]
That means the strategy does not say:
# fragile, level-specific thinking
build("STONE_QUARRY")
It says:
⌕ Find a buildable, storage-capable extractor
that can mine the resource this node contains.
This matters because a game that evolves by stage is a deliberately moving target. A hardcoded first-level script is not an algorithm; it is a screenshot of an algorithm.
𝚒𝚗 𝚘𝚝𝚑𝚎𝚛 𝚠𝚘𝚛𝚍𝚜
𝚃𝚑𝚎 𝚜𝚎𝚛𝚟𝚎𝚛 𝚍𝚎𝚜𝚌𝚛𝚒𝚋𝚎𝚜 𝚠𝚑𝚊𝚝 𝚎𝚡𝚒𝚜𝚝𝚜; 𝚝𝚑𝚎 𝚜𝚘𝚕𝚟𝚎𝚛 𝚍𝚎𝚌𝚒𝚍𝚎𝚜 𝚠𝚑𝚊𝚝 𝚝𝚘 𝚍𝚘 𝚠𝚒𝚝𝚑 𝚒𝚝.
I also added an early compatibility check. If the server advertises an action that the local solver does not implement, the program stops instead of improvising an invalid plan:
solver_actions = supported_action_types()
missing_actions = sorted(registry.actions - solver_actions)
if missing_actions:
raise CapabilityMismatchError(
stage,
f"Stage {stage} exposes unsupported actions: {', '.join(missing_actions)}",
)
That is a small defensive feature, but it prevents a misleading failure mode: “the strategy is bad” when the actual problem is “the game changed and the solver does not know a new action yet.”
⚖ 4. Build a local validator before trusting the network
The server should not be your debugger.
Sending a plan to a remote service is slower than replaying it locally. It also gives weaker feedback, may consume a limited attempt, and forces you to learn game rules through trial and error. I built a validator that replays the entire plan on a copy of the initial state before the client submits anything.
def validate_plan(initial_state: GameState, plan: list[list[BaseAction]]) -> GameState:
state = initial_state.copy()
if len(plan) > state.max_turns:
raise PlanValidationError(
f"plan uses {len(plan)} turns, limit is {state.max_turns}"
)
claimed = False
for turn_index, turn in enumerate(plan):
state.executed_turns = turn_index
state.turn_start()
for action_index, action in enumerate(turn):
_validate_action(state, action, turn_index, action_index)
action.apply(state)
if isinstance(action, ClaimWinAction):
claimed = True
if not claimed:
raise PlanValidationError("plan never claims victory")
return state
⟁ Invariants I validated
- ⌖ A build is inside the board and does not overwrite an existing structure.
- ⚒ The structure exists in the current capability registry and is buildable.
- ◈ The base can afford the construction cost.
- ⛓ A newly built structure is adjacent to the existing network.
- ⚡ An extractor stands on a compatible resource node.
- ⌕ Every transfer step is orthogonally adjacent.
- ⏱ Transfer endpoints are storage structures; intermediate cells are transport-only structures.
- ♥ The source owns the requested positive amount.
- ⚙ A mine or producer is activated no more than once per turn.
- ★ Production has all recipe inputs.
- ⚖ The win claim targets the correct structure and the goal inventory is actually present.
For example, transfer validation is intentionally strict:
if action.amount <= 0:
fail("transfer amount must be positive")
path = [_as_coord(coord) for coord in action.path]
if len(path) < 3:
fail("transfer path must contain at least source, transport, and destination")
for previous, current in zip(path, path[1:]):
if not _adjacent(previous, current):
fail(f"transfer path has non-adjacent step {previous}->{current}")
❖ A validator is not an attempt to clone the entire server. It is a local contract for the mistakes that would otherwise be expensive to discover remotely.
⌖ 5. Search for a build route, then use it as a transfer route
Every expansion starts with a geography problem:
“How can I connect this target cell to the network without crossing forbidden terrain, structures, or incompatible nodes?”
I used breadth-first search for pathfinding. A candidate expansion stores the resource, its extractor type, full route, construction cost, the number of roads needed, terrain penalty, and expected rate.
@dataclass(frozen=True)
class ExpansionCandidate:
resource: str
extractor_type: str
path: tuple[Coord, ...]
cost: Inventory
road_count: int
terrain_penalty: int
rate: int
kind: str = "extraction"
The route has two meanings:
BASE ── road ── road ── EXTRACTOR
▲ │
└──── transfer path ────────┘
First it tells the solver which transport cells need to be built. After construction, it is a valid path for taking materials back to the base.
The strategy builds missing transport cells in order, then builds the extractor at the final coordinate:
for coord in candidate.path[1:-1]:
if self.game_state.get_structure_at(*coord) is None:
yield BuildAction(coord[0], coord[1], self._transport_spec.type)
node = candidate.path[-1]
yield BuildAction(node[0], node[1], candidate.extractor_type)
If the network is complete by that point, the solver mines and transfers in the same turn. This is a small but useful optimisation when the number of turns is tight.
⚡ 6. Start with a greedy strategy; keep a bounded search for when it fails
My normal turn-generation loop is deliberately easy to explain:
① ⛏ Mine from every connected extractor.
② ⛓ Transfer those resources back to the base.
③ ⚙ Feed and activate connected production structures.
④ ⌕ Find the best affordable expansion.
⑤ ⚒ Build its transport path and target structure.
⑥ ★ Claim victory as soon as the goal is satisfied.
This is the essence of the greedy part of the strategy:
if self.game_state.base.storage.at_least(self.game_state.goal_resources):
yield from self._claim()
return
for extractor in self._connected_extractors():
path = self._transfer_path((extractor.x, extractor.y))
if path is None:
continue
yield ExtractAction(extractor.x, extractor.y)
amount = extractor.storage[extractor.extracted_resource]
if amount > 0:
yield TransferAction(path=path, resource=extractor.extracted_resource, amount=amount)
candidate = self._best_affordable_expansion()
Greedy planning is fast, debuggable, and usually good enough. But it has a predictable weakness: a locally cheap expansion can be globally wrong when the game has only a few turns remaining.
So I added a fallback: beam search.
initial state
│
├─ mine / transfer resources available this turn
├─ generate several affordable expansions
├─ simulate each possible next state
├─ score the states
└─ keep the best K branches ◈ beam width
│
▼
next turn
The solver does not enumerate every possible future — that would grow too quickly. Instead, it retains a limited beam of promising states. The score considers progress toward the goal, connected income, production output, remaining turns, recipe-input shortages, construction costs, and terrain penalties.
class GeneralStrategy(BaseStrategy):
ACTION_LOOP_LIMIT = 250
BEAM_WIDTH = 60
BEAM_BUILD_DEPTH = 2
BEAM_CANDIDATE_LIMIT = 12
This makes the algorithm pragmatic rather than academically exhaustive:
∞ Simple cases stay simple. Hard cases get more search only when needed.
For production chains, I also estimate capacity. If the existing factories and connected input sources cannot produce the missing output in the remaining turns, the solver prioritises a new input source or another producer before it is too late.
♻ 7. Tests were part of the solution, not decoration
I used compact synthetic levels to test the planning logic without needing a live server. The tests cover both positive plans and the rule boundaries that should fail.
def test_validator_rejects_invalid_transfer_and_double_mine() -> None:
initial = GameState(...)
with pytest.raises(PlanValidationError, match="insufficient STONE"):
validate_plan(
initial,
[[TransferAction(path=[(2, 0), (1, 0), (0, 0)], resource="STONE", amount=6)]],
)
with pytest.raises(PlanValidationError, match="already mined"):
validate_plan(initial, [[ExtractAction(2, 0), ExtractAction(2, 0)]])
✦ The suite checked
- a single road and a quarry are built in the correct order;
- a multi-road corridor remains connected throughout construction;
- the strategy mines before expanding when the base lacks resources;
- the cheaper resource node is selected first;
- a road cannot be built on a resource node;
- a transfer cannot send more material than the source holds;
- an extractor cannot mine twice in one turn;
- a premature win claim is rejected;
- an invalid plan is never submitted through the CLI.
At the time of writing, the suite passes:
........ [100%]
8 passed in 0.91s
★ Tests do not prove that a solver handles every future game mechanic. They do prove that the rules I had already learned do not silently regress while I add the next mechanic.
♥ What I learned
Technically, I am proud of this solution. The task pushed me to build a domain model, separate server capabilities from decisions, replay plans locally, validate hard constraints, and combine a transparent greedy heuristic with a bounded fallback search.
It reinforced a principle I value in backend work:
⚙ When an external system has rules, create a local model of those rules before you automate decisions against it.
The recruitment experience added a second lesson.
Before I install tracking software, give a service screen access, or commit dozens of hours to a technical task, I will now ask:
- ⚖ Is this tool mandatory, and is that requirement explicitly called out?
- ⌕ What data does it collect and who can access it?
- ⌚ How long is the assignment expected to take?
- ₿ Is the work paid, and if so, under what written conditions?
- ⚲ Is there a way to use an isolated device, account, or virtual machine?
- ✉ What part of the evaluation is blocking: the output, the process, the tracker, or all of the above?
- ⟁ Can the employer provide a verified installation source and privacy documentation?
Those questions are not hostility. They are ordinary digital hygiene.
𝚏𝚒𝚗𝚊𝚕 𝚗𝚘𝚝𝚎
𝚃𝚎𝚌𝚑𝚗𝚒𝚌𝚊𝚕 𝚜𝚔𝚒𝚕𝚕 𝚒𝚜 𝚗𝚘𝚝 𝚓𝚞𝚜𝚝 𝚠𝚛𝚒𝚝𝚒𝚗𝚐 𝚌𝚘𝚍𝚎.
𝙸𝚝 𝚒𝚜 𝚞𝚗𝚍𝚎𝚛𝚜𝚝𝚊𝚗𝚍𝚒𝚗𝚐 𝚛𝚎𝚚𝚞𝚒𝚛𝚎𝚖𝚎𝚗𝚝𝚜, 𝚖𝚘𝚍𝚎𝚕𝚕𝚒𝚗𝚐 𝚌𝚘𝚗𝚜𝚝𝚛𝚊𝚒𝚗𝚝𝚜, 𝚟𝚎𝚛𝚒𝚏𝚢𝚒𝚗𝚐 𝚊𝚜𝚜𝚞𝚖𝚙𝚝𝚒𝚘𝚗𝚜, 𝚊𝚗𝚍 𝚔𝚗𝚘𝚠𝚒𝚗𝚐 𝚠𝚑𝚎𝚗 𝚝𝚘 𝚙𝚊𝚞𝚜 𝚊𝚗𝚍 𝚟𝚎𝚛𝚒𝚏𝚢.
The game was difficult. Solving it was worth the effort. And the experience made me a more careful engineer — not only when I design systems, but also when I decide which systems deserve my trust.
Top comments (2)
I was particularly intrigued by the idea of evaluating the hiring process as a crucial part of the recruitment experience, as you've highlighted with the question "⌘ Can the candidate evaluate the hiring process, its tools, and the access they are asked to give before they are hired?" The fact that the installation of Hubstaff with screen monitoring capabilities raised concerns for you resonates with me, as I've also had experiences where installation requirements seemed unclear or overly intrusive. Your breakdown of what you would expect before installing monitoring software, such as a clear explanation of the need, description of collected data, and retention policy, seems like a reasonable and necessary step in building trust between the candidate and the company. Do you think companies could benefit from having a more open dialogue about their monitoring and tracking requirements during the recruitment process to alleviate such concerns?
You’ve managed to touch every single point of the post with almost algorithmic precision - just seconds after it was published 😄