Manter as melhores pontuações e incluir empates no corte

RedisBeginner
Pratique Agora

Introdução

No posto de pontuação do Field Day, as tentativas chegam fora de ordem. As três tentativas de Alex marcam 80, 90 e 85 pontos. A melhor é 90: esses são resultados absolutos, não pontos a somar. Outro jogador também se chama Alex, portanto os nomes não podem identificar os competidores. Quando há empate na pontuação do terceiro lugar, todos com essa pontuação se classificam.

Você salvará funções reutilizáveis de registro e classificação com Redis, processará a planilha real de tentativas e publicará os jogadores realmente classificados. Depois, concluirá uma pequena prova cronometrada na qual pontuações menores são melhores. Os dois exercícios usam identidades sintéticas preparadas e pontuações inteiras. Saber navegar pelo terminal e conhecer conexões Redis/GET/SET são pré-requisitos; conjuntos ordenados e os modelos de funções Python fornecidos são explicados antes do uso. O Redis e Terminal estão prontos.

Trabalhe em /home/labex/project/score-desk. Crie main.py e practice.py; atualize apenas score:main:best, score:practice:best e chaves de teste que comecem com score:demo:. Preserve as planilhas CSV originais, os hashes de nomes, a rodada anterior e todas as ferramentas fornecidas. O resultado de 88 pontos de um jogador anterior na rodada principal e o resultado de 72 segundos de um corredor anterior na prática devem ser preservados, embora suas tentativas não estejam nas novas planilhas.

Uma única tentativa deve atualizar o melhor resultado de seu jogador com segurança, mesmo quando outro envio ocorre ao mesmo tempo. A classificação é executada após o encerramento dos envios; suas duas leituras não são um retrato consistente de uma competição em andamento. As pontuações são inteiros de 1 a 1000000. Este exercício não implementa contestação de pontuações, exclusão de um melhor resultado incorreto, detecção de trapaça, cronometragem fracionária nem persistência/failover.

Manter a melhor tentativa absoluta

Nesta etapa, você manterá a maior tentativa absoluta de cada jogador sem permitir que um resultado menor que chegue atrasado a substitua.

Use Terminal, começando no shell normal. O programa fornecido desk lê a planilha e o banco de dados reais. Ele chama suas funções salvas sem alterações; não calcula nem corrige o melhor resultado para você. ./ significa executar um arquivo no diretório atual. Mude o diretório e inspecione a planilha e a lista de jogadores:

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

player_id é uma identidade estável; score é o total absoluto de pontos de uma tentativa. Os IDs p01 e p02 pertencem a pessoas chamadas Alex. O conjunto ordenado principal preparado contém atualmente apenas p90 com pontuação 88. Ele deve permanecer presente. O Redis armazena nomes separadamente em um hash, um mapeamento do ID do jogador para o nome. HGETALL lê esse mapeamento; você não precisa editá-lo.

Um conjunto ordenado armazena cada membro uma vez com uma pontuação numérica. Aqui, o membro é um ID de jogador. O Redis ordena os membros pela pontuação. O comando simples ZADD key score member adiciona um novo membro ou substitui sua pontuação existente. Use uma chave descartável de demonstração para ver por que a última tentativa recebida não é necessariamente a melhor:

./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

A pontuação final é 85, porque o último comando sobrescreveu 90. ZSCORE lê a pontuação de um membro; --raw solicita à CLI um valor simples. ZINCRBY somaria as tentativas, produzindo 255 a partir desses três valores. Essa operação é adequada para incrementos, mas estas tentativas não são incrementos.

ZADD com GT adiciona um novo membro ou altera uma pontuação existente apenas quando o novo valor é maior. A comparação e a atualização ocorrem dentro de um único comando Redis. Uma leitura separada no cliente seguida de uma escrita incondicional pode entrar em condição de corrida com outro chamador. Experimente a condição atômica:

./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

A pontuação permanece 90, inclusive após a tentativa duplicada. Por padrão, a resposta de ZADD conta novos membros, portanto 0 pode significar tanto uma atualização bem-sucedida de um membro existente quanto uma tentativa ignorada. Não é um erro. Leia a pontuação real para distinguir esses casos. GT não impede a inserção de um novo jogador; adicionar XX impediria as primeiras tentativas e seria incorreto aqui.

Salve uma função reutilizável para que cada linha enviada siga a mesma regra. O consumidor fornecido passa um cliente Redis, a chave real, o ID do jogador e a pontuação. O def do Python define a função; os nomes entre parênteses recebem esses valores. O return indentado envia um comando e retorna sua resposta real. Palavras entre aspas são argumentos literais de comandos Redis; nomes sem aspas usam os valores do chamador. Não há pacote Python Redis para instalar: o cliente fornecido envia comandos Redis nativos.

O here-document a seguir grava o arquivo completo. cat > main.py substitui esse arquivo. O marcador PY entre aspas impede o shell de interpretar o conteúdo; o PY de fechamento deve estar sozinho em sua linha. Preserve os quatro espaços antes de return. Cole o bloco inteiro em Terminal:

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

Reproduza a planilha preparada por meio de sua função. Depois, reproduza-a novamente e envie mais uma vez a tentativa menor que chegou atrasada:

./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 permanece em 90; o outro Alex, p02, continua sendo um membro separado em 90. O conjunto completo também contém p05=100, p03=95, p06=90, p04=70 e o jogador anterior p90=88. data mostra IDs e strings de pontuação alternados na ordem crescente do Redis.

ZREVRANK fornece a posição com índice iniciado em zero de um membro em ordem decrescente: a primeira posição é 0. Aqui, p01 tem posição 4, porque pontuações iguais usam a ordem inversa dos bytes dos IDs. Essa posição não desclassifica um jogador empatado na pontuação de classificação. Usaremos pontuações, em vez de uma quantidade fixa de IDs, para classificar.

Se digitar um nome de arquivo ou comando incorretamente, leia o erro, use ls para verificar os arquivos reais e tente novamente com o comando corrigido. Um Score desk error é diferente de um resultado vazio bem-sucedido. Se você aplicou acidentalmente uma regra de pontuação errada, primeiro corrija main.py, inspecione a planilha original e use o comando simples ZADD para restaurar o verdadeiro melhor resultado de cada jogador afetado; por exemplo, ./redis ZADD score:main:best 90 p01. Não edite a planilha nem a lista de nomes, não apague a rodada nem descarte o jogador anterior. Execute a verificação da etapa após inspecionar os resultados reais.

Incluir todos no limite de classificação

Nesta etapa, você transformará um limite de posições em um limite inclusivo de pontuação para que todos os jogadores empatados se classifiquem.

Permaneça em Terminal e no diretório score-desk. Os envios estão encerrados nesta parte. A regra é as três primeiras posições, mais todos os empatados na pontuação da terceira posição. Há duas decisões separadas: encontrar essa pontuação e incluir todos os jogadores que a atingem.

ZRANGE lê um conjunto ordenado. Sem uma opção de pontuação, seus limites são posições inclusivas, com índice iniciado em zero. REV inverte a ordem das pontuações; WITHSCORES inclui a pontuação de cada membro. A terceira posição tem índice 2:

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

O primeiro comando retorna apenas p05, p03 e p06. O segundo retorna p06 e 90: a pontuação de corte é 90. Parar em três jogadores perderia p02 e p01, que se classificam pela regra de empate declarada.

Com BYSCORE, os limites são pontuações, não posições. Na ordem inversa, forneça primeiro o limite superior. +inf significa sem limite superior. Um limite simples de pontuação inclui a igualdade; um limite iniciado por ( excluiria a igualdade e perderia incorretamente todos os empates no corte. Consulte o conjunto completo de classificados:

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

Todos os cinco IDs aparecem, na ordem p05, p03, p06, p02, p01. A ordenação de IDs com pontuação igual é uma convenção de exibição fornecida pelo Redis, não uma regra para eliminar jogadores empatados nem ordenar seus nomes.

Faça isso funcionar para qualquer k positivo. O cliente retorna uma lista Python de strings alternadas: para um único jogador no limite, ela é ["p06", "90"]. O índice 0 da lista é o ID, e o índice 1 é a pontuação. Portanto, boundary[1] usa a pontuação real do Redis. k - 1 converte a posição solicitada em um índice iniciado em zero. A atribuição = armazena essa resposta na variável local boundary.

if not boundary testa se a lista está vazia. Quando k é maior que o número de jogadores, todos se classificam; leia as posições de 0 a -1, em que -1 significa o último membro. Para uma rodada vazia, isso também retorna uma lista legitimamente vazia. Esse tratamento usa o conjunto real do Redis, não uma resposta de exemplo.

Acrescente a próxima função a main.py. Aqui, >> acrescenta em vez de substituir a função de registro existente. O Python usa a indentação adicional sob if para identificar seu retorno condicional:

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

A ação fornecida board invoca qualify e depois associa os nomes da lista real. Ela preserva os membros, valores e a ordem que você retornou. Não filtra novamente, não adiciona um empate ausente nem substitui o corte pelo correto. Experimente solicitações diferentes:

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

A primeira solicitação retorna cinco objetos de jogador. Os dois registros de Alex aparecem com valores player_id diferentes. A segunda retorna apenas p05 em 100. A última retorna todos os sete jogadores, incluindo p90 em 88 e p04 em 70. Nenhuma nova tentativa é enviada ao consultar um placar.

As duas leituras desta função se destinam a uma rodada encerrada. Elas não garantem um único retrato consistente enquanto os envios continuam. O comando condicional único de registro é atômico; essas são afirmações diferentes. Mantenha sua função de registro funcionando e execute a verificação da etapa.

Aplicar as mesmas regras a uma prova cronometrada

Nesta etapa, você criará e usará um método completo de registro e classificação em que valores menores são melhores, em uma rodada de prática independente.

Permaneça em Terminal e no mesmo diretório. Uma corrida curta de prática registra segundos decorridos inteiros positivos, portanto pontuações menores são melhores. Sua planilha independente tem novos IDs; o corredor anterior t90 já tem 72 segundos. Inspecione-a antes de escrever:

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

t01 tem tentativas de 70, 60 e 65 segundos. Seu melhor resultado é 60, não 65. LT altera uma pontuação existente apenas quando o novo valor é menor; ele continua adicionando novos membros. Compare as mesmas três tentativas em uma chave de teste:

./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

O resultado permanece 60. O raciocínio de atualização atômica e a segurança contra duplicatas são os mesmos de GT; apenas a direção do melhor resultado muda.

Para um placar em que valores menores são melhores, use a ordem crescente omitindo REV. Na posição de corte k - 1, inclua todos com uma pontuação menor ou igual a essa pontuação. Um intervalo crescente de pontuações começa com -inf, que significa sem limite inferior. Salve o modelo independente completo em practice.py; ele usa exatamente as mesmas interfaces de função da rodada principal:

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

Envie a planilha de prática, inspecione o limite nativo e o intervalo inclusivo e peça ao consumidor real do placar seu resultado:

./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

O limite é t01 em 60. O placar dos dois primeiros com empates contém t03=50, t01=60, t02=60 e t04=60. Os nomes repetidos Sam pertencem a IDs diferentes. O 65 atrasado não pode substituir 60, e reproduzir a planilha inteira não altera nenhum melhor resultado. O corte grande inclui todos os seis corredores, incluindo t90=72. A classificação principal de cinco jogadores continua disponível.

Para tempos iguais, o Redis ordena os IDs pela ordem crescente dos bytes; o placar principal de maior pontuação usa a ordem decrescente dos bytes por causa de REV. Nenhuma das convenções exclui um participante com pontuação igual. Para estes IDs ASCII preparados, a ordenação declarada é estável.

Agora você tem métodos completos de registro e classificação para ambas as direções. Mantenha os dois arquivos, as duas rodadas, as planilhas originais, as listas de nomes e os resultados da rodada anterior. As verificações exercitam suas funções sem alterações com outros IDs, valores, ordens de chegada e cortes. Execute a verificação final após inspecionar os resultados reais.

Resumo

Você manteve identidades estáveis de jogadores, armazenou as melhores tentativas absolutas com atualizações condicionais atômicas do Redis e separou a direção da pontuação da ordem de exibição. Usou a pontuação real da posição k para incluir todos os empates no limite, incluindo jogadores com o mesmo nome, e manteve resultados anteriores. Suas funções salvas funcionam tanto para eventos de maior pontuação quanto para provas cronometradas em segundos inteiros. As leituras de classificação pressupõem que os envios tenham sido encerrados; persistência, contestações e captura de um retrato consistente em tempo real são trabalhos separados.