Keep Best Scores and Include Cutoff Ties

RedisBeginner
Practice Now

Introduction

At the Field Day score desk, attempts arrive out of order. Alex's three attempts score 80, 90 and 85 points. The best is 90: these are absolute results, not points to add together. A second player is also named Alex, so names cannot identify competitors. When the third-place score is tied, everyone at that score qualifies.

You will save reusable Redis recording and qualification functions, process the actual attempt sheet and publish the real qualifying players. Then you will complete a small timed trial in which lower scores are better. Both exercises use prepared synthetic identities and integer scores. Basic terminal navigation and Redis connections/GET/SET are prerequisites; sorted sets and the supplied Python function templates are explained before use. Redis and Terminal are ready.

Work in /home/labex/project/score-desk. Create main.py and practice.py; update only score:main:best, score:practice:best and scratch keys beginning score:demo:. Preserve original CSV sheets, roster hashes, the previous round and all supplied tools. An earlier player's 88-point main result and an earlier runner's 72-second practice result must survive, even though their attempts are absent from the new sheets.

A single attempt must update its player's best safely even when another submission overlaps. Qualification runs after submissions close; its two reads are not a snapshot of a changing competition. Scores are integers from 1 to 1000000. This exercise does not implement score appeals, deletion of an erroneous best, cheating detection, fractional timing or persistence/failover.

Keep the Best Absolute Attempt

In this step, you will keep each player’s highest absolute attempt without letting a late lower result replace it.

Use Terminal, starting at the normal shell. The supplied desk program reads the real sheet and database. It calls your saved functions unchanged; it does not calculate or repair a correct best for you. ./ means run a file in the current directory. Change directory and inspect the sheet and the roster:

cd /home/labex/project/score-desk
./desk attempts main
./redis HGETALL score:main:names
./desk data main

player_id is a stable identity; score is one attempt's absolute point total. IDs p01 and p02 both belong to people named Alex. The prepared main sorted set currently contains only p90 with score 88. It must remain present. Redis stores names separately in a hash, a mapping from player ID to name. HGETALL reads that mapping; you do not need to edit it.

A sorted set stores each member once with a numeric score. Here the member is a player ID. Redis orders members by score. Plain ZADD key score member adds a new member or replaces its existing score. Use a disposable demo key to see why the last arrival is not necessarily the best:

./redis ZADD score:demo:last 80 p01
./redis ZADD score:demo:last 90 p01
./redis ZADD score:demo:last 85 p01
./redis --raw ZSCORE score:demo:last p01

The final score is 85, because the last command overwrote 90. ZSCORE reads one member's score; --raw asks the CLI for a plain value. ZINCRBY would add attempts instead, producing 255 from these three values. That operation is suitable for increments, but these attempts are not increments.

ZADD with GT adds a new member or changes an existing score only when the new value is greater. The comparison and update happen within one Redis command. A separate client-side read followed by an unconditional write can race with another caller. Try the atomic condition:

./redis ZADD score:demo:best GT 80 p01
./redis ZADD score:demo:best GT 90 p01
./redis ZADD score:demo:best GT 85 p01
./redis ZADD score:demo:best GT 90 p01
./redis --raw ZSCORE score:demo:best p01

The score stays 90, including after the duplicate attempt. By default, the ZADD reply counts new members, so 0 can mean either a successful update of an existing member or an ignored attempt. It is not an error. Read the actual score to distinguish those cases. GT does not prevent inserting a new player; adding XX would prevent first attempts and would be wrong here.

Save a reusable function so every submitted row follows the same rule. The supplied consumer passes a Redis client, the actual key, player ID and score. Python def defines the function; the names in parentheses receive those values. The indented return sends a command and returns its actual reply. Quoted words are literal Redis command arguments; unquoted names use the caller's values. There is no Python Redis package to install: the provided client sends native Redis commands.

The following here-document writes the complete file. cat > main.py replaces that file. The quoted PY marker keeps the shell from interpreting the body; the closing PY must be alone on its line. Preserve the four spaces before return. Paste the whole block into Terminal:

cat > main.py <<'PY'
def record(client, key, player, score):
    return client.command("ZADD", key, "GT", score, player)
PY

Replay the prepared sheet through your function. Then replay it again and submit the late lower attempt once more:

./desk replay main
./desk data main
./desk replay main
./desk submit main p01 85
./redis --raw ZSCORE score:main:best p01
./redis ZREVRANK score:main:best p01

p01 stays at 90; the other Alex, p02, remains a separate member at 90. The full set also contains p05=100, p03=95, p06=90, p04=70 and the earlier p90=88. data shows alternating IDs and score strings in ascending Redis order.

ZREVRANK gives a member's zero-based position in descending order: the first position is 0. Here p01 has position 4, because equal scores use reverse bytewise ID order. That position does not disqualify a player tied at the qualifying score. We will use scores, rather than a fixed number of IDs, for qualification.

If you mistype a filename or command, read the error, use ls to check the actual files and retry the corrected command. A Score desk error is different from a successful empty result. If you accidentally applied a wrong scoring rule, first correct main.py, inspect the original sheet and use ordinary ZADD to restore each affected player's true best; for example ./redis ZADD score:main:best 90 p01. Do not edit the sheet or roster, erase the round or discard the earlier player. Run the step check after inspecting the actual results.

Include Everyone at the Cutoff

In this step, you will turn a position cutoff into an inclusive score boundary so every tied player qualifies.

Stay in Terminal and the score-desk directory. Submissions are closed for this part. The rule is the top three positions, plus everyone tied at the third position's score. There are two separate decisions: find that score, then include all players meeting it.

ZRANGE reads a sorted set. Without a score option its bounds are inclusive, zero-based positions. REV reverses score order; WITHSCORES includes each member's score. The third position has index 2:

./redis --raw ZRANGE score:main:best 0 2 REV WITHSCORES
./redis --raw ZRANGE score:main:best 2 2 REV WITHSCORES

The first command returns only p05, p03 and p06. The second returns p06 and 90: the cutoff score is 90. Stopping at three players would lose p02 and p01, who both qualify under the stated tie rule.

With BYSCORE, the bounds are scores, not positions. In reverse order, give the upper bound first. +inf means no upper limit. A plain score bound includes equality; a bound starting with ( would exclude equality and wrongly lose all cutoff ties. Read the complete qualifying set:

./redis --raw ZRANGE score:main:best +inf 90 BYSCORE REV WITHSCORES

All five IDs appear, ordered p05, p03, p06, p02, p01. Equal-score ID ordering is a display convention supplied by Redis, not a rule for eliminating tied players or sorting their names.

Make this work for any positive k. The client returns a Python list of alternating strings: for a single boundary player it is ["p06", "90"]. List index 0 is the ID and index 1 is the score. boundary[1] therefore uses the actual score from Redis. k - 1 converts the requested position to a zero-based index. Assignment = stores that reply in the local variable boundary.

if not boundary tests whether the list is empty. When k is larger than the number of players, everyone qualifies; read positions 0 through -1, where -1 means the last member. For an empty round this also returns an honest empty list. This fallback uses the actual Redis set, not a sample answer.

Append the next function to main.py. Here >> appends instead of replacing the existing recording function. Python uses the extra indentation beneath if to identify its conditional return:

cat >> main.py <<'PY'

def qualify(client, key, k):
    boundary = client.command("ZRANGE", key, k - 1, k - 1, "REV", "WITHSCORES")
    if not boundary:
        return client.command("ZRANGE", key, 0, -1, "REV", "WITHSCORES")
    return client.command("ZRANGE", key, "+inf", boundary[1], "BYSCORE", "REV", "WITHSCORES")
PY

The supplied board action invokes qualify, then attaches names from the real roster. It preserves your returned members, values and order. It does not re-filter, add a missing tie or substitute a correct cutoff. Exercise different requests:

./desk board main 3
./desk board main 1
./desk board main 100

The first request returns five player objects. Both Alex records appear with different player_id values. The second returns only p05 at 100. The last returns all seven players, including p90 at 88 and p04 at 70. No new attempts are submitted by reading a board.

The two reads in this function are intended for a closed round. They do not guarantee one consistent snapshot while submissions continue. The single conditional recording command is atomic; those are different claims. Keep your recording function working and run the step check.

Apply the Same Rules to a Timed Trial

In this step, you will build and use a complete lower-is-better recording and qualification method on an independent practice round.

Stay in Terminal and the same directory. A short practice race records positive integer elapsed seconds, so lower scores are better. Its independent sheet has new IDs; the earlier runner t90 already has 72 seconds. Inspect it before writing:

./desk attempts practice
./redis HGETALL score:practice:names
./desk data practice

t01 has attempts of 70, 60 and 65 seconds. Its best is 60, not 65. LT changes an existing score only when the new value is lower; it still adds new members. Compare the same three attempts on a scratch key:

./redis ZADD score:demo:time LT 70 t01
./redis ZADD score:demo:time LT 60 t01
./redis ZADD score:demo:time LT 65 t01
./redis --raw ZSCORE score:demo:time t01

The result stays 60. The atomic-update reasoning and duplicate safety are the same as for GT; only the better direction changes.

For a lower-is-better board, use ascending order by omitting REV. At cutoff position k - 1, include everyone with a score less than or equal to that score. A forward score range begins with -inf, meaning no lower limit. Save the full independent template in practice.py; it uses exactly the same function interfaces as the main round:

cat > practice.py <<'PY'
def record(client, key, player, score):
    return client.command("ZADD", key, "LT", score, player)

def qualify(client, key, k):
    boundary = client.command("ZRANGE", key, k - 1, k - 1, "WITHSCORES")
    if not boundary:
        return client.command("ZRANGE", key, 0, -1, "WITHSCORES")
    return client.command("ZRANGE", key, "-inf", boundary[1], "BYSCORE", "WITHSCORES")
PY

Submit the practice sheet, inspect the native boundary and inclusive range, then ask the real board consumer for its result:

./desk replay practice
./redis --raw ZRANGE score:practice:best 1 1 WITHSCORES
./redis --raw ZRANGE score:practice:best -inf 60 BYSCORE WITHSCORES
./desk board practice 2
./desk replay practice
./desk submit practice t01 65
./desk board practice 2
./desk board practice 100
./desk board main 3

The boundary is t01 at 60. The top-two-with-ties board contains t03=50, t01=60, t02=60 and t04=60. Duplicate names Sam belong to different IDs. The late 65 cannot replace 60, and replaying the whole sheet changes no best. The large cutoff includes all six runners, including t90=72. The main five-player qualification remains available.

For equal times, Redis orders IDs bytewise ascending; the high-score main board uses bytewise descending order because of REV. Neither convention excludes an equal-score participant. For these prepared ASCII IDs the stated ordering is stable.

You now have complete recording and qualification methods for both directions. Keep both files, both rounds, original sheets, rosters and earlier-round results. Checks exercise your unchanged functions on other IDs, values, arrival orders and cutoffs. Run the final check after inspecting the real results.

Summary

You kept stable player identities, stored absolute best attempts with atomic conditional Redis updates, and separated score direction from display order. You used the actual kth score to include every cutoff tie, including same-name players, and retained earlier results. Your saved functions work for both high-score events and integer-second timed trials. The qualification reads assume submissions have closed; persistence, appeals and live snapshotting are separate work.