I was given a conveyor belt simulation to complete as part of an agency interview. The requirement was to use vanilla PHP, including the logic, the tests and the way I presented the answer. It was a quick exercise, but there was enough in the brief to make me stop and think carefully about how the system should behave.
After I handed it in, I was offered the job straight away. That gave me a lot of confidence as a developer. I’d enjoyed working through the problem, and having that response to the work made me realise how much I enjoy these kinds of challenges.
A small problem with quite specific rules
The simulation has three pairs of workers beside a moving conveyor belt. Each incoming slot has an equal chance of containing component A, component B or nothing. A worker needs one of each component to assemble a finished product, has only two hands, and cannot collect more components while assembling.
Time moves in discrete steps. A finished product should be ready to return to the belt on the fourth subsequent slot after the worker collects the necessary components. Opposite workers cannot both touch the same slot during one step. After 100 steps, the program needs to report the products that have left the belt and the A and B components that have passed through untouched. Those are the rules in the original challenge brief.
The interesting part is the timing. A worker collecting an item, a worker finishing a product and a slot moving down the belt all affect what can happen next. I needed a model where I could follow that sequence rather than just count how many A and B components arrived.
Making time a loop
I represented the belt as an array and gave each worker a small state object. In the first version, the worker had an assigned station, two hand values and an assembly counter. Each belt item held its inventory value and a status flag for whether the slot had been used during that step.
A function called ticker() advances the simulation. On each iteration it moves the belt, records what falls off the end, then processes the workers against the new positions. There is no need to wait for a real second to pass. One loop iteration is one unit of simulated time.
The movement itself uses ordinary PHP array operations. This is the central part, shortened from moveBelt():
<?php
array_unshift($belt, $newObject);
$removed = $belt[array_key_last($belt)];
array_pop($belt);
return (object) [
'belt' => $belt,
'removed' => $removed,
];
array_unshift() puts the new item at the front. I retain the item at the other end before array_pop() removes it. That keeps the belt at a fixed length and gives the reporting code a record of what actually leaves the line.
That last distinction matters. Collecting both components does not mean a product has left the factory. It might still be in a worker’s hands or travelling along the belt when the 100th step finishes.
Keeping track of what each worker can do
The workers are assigned to stations in pairs. Two workers share the first position, the next two share the second, and the final pair share the third. Each worker’s state carries that station index, so the code can look up the slot they are responsible for.
The worker logic checks what is passing, whether the worker already holds that component and whether there is room to collect it. When a component is picked up, its place on the belt becomes empty. Once the worker has both parts, the assembly counter progresses towards a finished product.
I used a status flag on the slot to represent access during a step. This is a sequential PHP simulation, so it isn’t a thread lock. It is a way of modelling the rule that a worker on one side cannot pick up or put down an item at the same time as the worker opposite.
Functions such as createWorker(), moveBelt() and processWorkers() gave the code recognisable responsibilities. I could look at the movement separately from the decisions a worker made, and follow the state from one tick to the next.
Giving the answer some proof
Random input is useful for the full simulation, but it is awkward for checking a specific piece of logic. If the input changes every time, a different result does not tell me whether I’ve broken something.
I made the initialisation accept either a number of steps or a predefined sequence of incoming items. For the demonstration, it generates random components. For the test cases, I can feed it exactly the sequence I want.
One of the original checks uses a single station and a pair of workers:
<?php
$result = init([
'items' => ['a', 'b', '0', '0', '0', '0'],
'workerCount' => 2,
'beltLengthCount' => 1,
]);
echo formatAnswer($result);
That case displays one finished product leaving the belt. Another sequence, a, b, a, b, 0, 0, 0, 0, 0, displays two. The empty slots allow the assembly and return steps to play out, which makes the output easier to inspect.
I also kept a step-by-step log of the workers’ actions. It shows when someone collects a component and when a completed product is returned. Alongside the totals, that gives a reviewer something to follow when checking how the result was reached.
The tests and formatting were plain PHP, as the challenge required. The original checks print expected outcomes alongside the actual results. They are useful demonstration cases, but they don’t automatically fail when a result is wrong. With more time, I’d strengthen that part.
What I’d do with more time
First, I’d turn those examples into explicit pass-or-fail checks and cover more of the rules. Empty input, repeated A components, repeated B components, two workers competing for a slot and a product waiting for space all deserve their own cases. I’d check the state during the run as well as the final totals.
That still doesn’t require a testing framework. For example, this is an additional assertion I could put around the original single-station case:
<?php
$expected = 1;
$actual = $result->sum_manufactured;
if ($actual !== $expected) {
throw new RuntimeException(
"Expected {$expected} product, got {$actual}"
);
}
Second, I’d make the assembly timing more explicit. I’d record the tick when the second component is collected and the tick when the product becomes ready. That is easier to compare with the fourth-subsequent-slot rule than a counter whose meaning depends on where it is incremented in the loop.
I’d separate a worker progressing their assembly from a worker touching the belt. The former can happen while another worker is using the station; the latter must respect a single access decision for that slot during the tick. I’d also make the ready-to-place state explicit, so the worker can wait for an empty slot without overwriting a component or doing two belt actions in one step.
Third, I’d tighten the configuration. I explored making the worker settings more flexible, including the components they collect and the number of hands. For this brief, the defaults should remain A and B, two hands, three pairs and an equal three-way input choice. Making something configurable is only useful if the original rules stay clear and every test supplies valid settings.
I’d use consistent values for empty slots and strict comparisons. PHP’s empty() treats the string "0" as empty, which is worth being careful about when that same value represents a slot in the simulation. Named constants would make those meanings easier to follow than a mixture of numbers, strings and booleans.
Finally, I’d separate the result data from its HTML presentation more thoroughly. Totals and structured events could be checked directly, while formatAnswer() remained responsible for displaying them. At the end of 100 steps, I’d account for components and products still on the belt or held by workers, without quietly running extra ticks to make the totals look complete.
Why I enjoyed it
I liked having a clearly defined problem to work through. There was a set of rules, some state that changed over time and an answer I needed to explain. I could make progress by breaking it down and checking one decision at a time.
The job offer was a welcome result, but the process gave me confidence too. I had taken an unfamiliar brief, built a solution quickly and shown how it arrived at its answer. It reminded me that this part of development, getting into a problem and working out its logic, is something I really enjoy.
The code is preserved in ConveyerChallenge and the more separated Conveyer_Challenge repository. The examples here are shortened from the original solution; the assertion shows an improvement I’d add, rather than a test that was part of the submission.