A higher-or-lower number game can give misleading hints or attempt counts if the secret and round state change at the wrong time. I'll use a small Vue implementation to show how to keep those updates consistent, then explain why midpoint guesses put a worst-case bound on the number of attempts.
The player chooses a range, submits a number, and gets a high, low, or correct result. There is no computer player performing binary search. The useful connection is that the feedback gives a human player exactly the information needed to shrink a search interval.
Pick One Secret for the Whole Round
The implementation offers upper bounds of 10, 50, 100, and 1,000, always starting at 1. The target generator is deliberately ordinary:
function randomTarget(max) {
return Math.floor(Math.random() * max) + 1;
}
Multiplying a random value in the interval from zero up to, but not including, one produces a value below max. Taking the floor gives integers from zero through max - 1; adding one moves the range to the numbers the UI promises.
The target must be generated once per round, not once per submission. Otherwise a helpful-looking “too low” message would describe a secret that might no longer exist when the next guess arrives. That destroys the premise of interval narrowing.
The round reset is also a state transition, not just another random assignment:
function newGame() {
if (typeof window === "undefined") return;
target.value = randomTarget(maxNum.value);
currentGuess.value = null;
guesses.value = [];
won.value = false;
feedback.value = "";
feedbackType.value = "";
}
I like reading this function as a checklist of everything belonging to one round. The secret changes, input clears, history disappears, the win lock resets, and the previous hint goes away together.
Changing the selected range calls this same reset. Keeping two partially different reset implementations would make it easy to carry an old guess history into a new range or leave the submission button disabled after a previous win.
Record the Attempt Before Announcing the Win
The submission function first checks whether the round is already won, rejects an empty or nonnumeric input, and reports numbers outside the selected bounds. Only then does it compare the guess with the secret.
In the successful branch, the guess is appended before the attempt count is used:
if (g === target.value) {
guesses.value.push({ value: g, dir: "correct" });
won.value = true;
feedback.value = t("guessNumber.correct", { n: guesses.value.length });
feedbackType.value = "correct";
if (bestScore.value === null || guesses.value.length < bestScore.value) {
bestScore.value = guesses.value.length;
}
}
The history is held in a Vue ref, so both the comparison and the stored score read guesses.value.length. More importantly, the successful submission is included before either read. Counting first and appending afterward would underreport every win by one attempt.
Each history item stores both the submitted value and its direction. The template derives its arrow and appearance from that record. I would rather keep that compact structured history than try to reconstruct previous results from translated feedback strings.
The best score is a separate ref, so starting a new round does not erase it. It is not stored in localStorage, and it is not partitioned by range. A one-guess win in the smallest range can therefore remain the best score when the player switches to 1–1,000. Its lifetime is that of the component instance: refreshing the page or unmounting and recreating the component loses the record. It is a convenience, not a normalized measure of skill.
Higher-or-Lower Feedback Defines a Search Interval
Suppose the secret is somewhere from 1 through 100. Guessing 50 and hearing “too low” eliminates everything from 1 through 50, including the number just tried. The remaining candidates are 51 through 100.
Guessing near the middle of the remaining interval keeps the larger of the two possible remaining groups small. Repeating that strategy is binary search performed by the player. It is the same idea used to search a sorted collection, except the game supplies a comparison instead of exposing an array.
For an inclusive interval [low, high], I would choose Math.floor((low + high) / 2). After a “too low” result for g, the next lower bound is g + 1; after “too high,” the next upper bound is g - 1. Excluding the failed guess matters: keeping it as a candidate can make the search repeat a value instead of progressing, especially when only two candidates remain.
For a range containing 100 distinct targets, seven guesses suffice in the worst case with an appropriate midpoint strategy. More generally, a comparison tree with at most k guesses can accommodate up to 2^k - 1 target values: each comparison can finish at its own node or continue into one of two smaller intervals.
The smallest sufficient bound for N targets is therefore Math.ceil(Math.log2(N + 1)). Six guesses cover at most 63 targets; seven cover 127. That explains the seven-guess guarantee for 100 targets without assuming that every guess has only two outcomes: equality ends the search at the current node.
That bound does not mean every game takes seven attempts. The secret might be the first midpoint, and random guesses can occasionally get lucky. Nor does it mean the component calculates a recommended next guess; it only provides feedback and history.
The component also keeps the input bounds at 1 and the selected maximum, not the narrowed interval. Repeating an earlier guess, or guessing something earlier feedback already ruled out, still counts as an attempt. The guarantee depends on the player's strategy; the interface does not enforce it.
This distinction makes the game a useful teaching example. The state machine supplies trustworthy observations, while the search strategy is a separate layer of reasoning. You can explain interval halving without pretending the application contains a solver that is absent from its source.
Browser Games Still Have Initialization and Input Boundaries
The component begins with a deterministic target of 50 and calls newGame() from onMounted. Randomness is deferred until the browser has mounted the page, rather than producing different secrets during server rendering and the browser's first render.
For a casual guessing game, Math.random() is adequate. It is not a cryptographic secret generator, and a target stored in client-side state is not protected from someone inspecting the application. This is not an architecture for competitive play or prize distribution.
There is also a narrower validation gap: the submission logic checks numeric conversion and bounds but does not check Number.isInteger(g). A fractional guess inside the range can be counted as an attempt even though the generated target is always an integer. For example, 50.5 against a secret of 51 produces “too low” and enters the history. The number input's default step can mark that value invalid, but the click handler and Enter handler call submitGuess directly, without a validating form submission. An integer guard before recording history would close that gap; it is not part of the current implementation.
I turned this small state-machine example into a free browser game: Guess the Number Game.
Top comments (0)