Jogo multiplayer de adivinhar o número — SPD Aula 03
Projeto desenvolvido para a disciplina de Sistemas Paralelos e Distribuídos (SPD) da ULBRA Palmas. Implementa comunicação em rede via sockets TCP, criptografia simétrica de sessão e um jogo multiplayer em tempo real.
numrush/
├── server.py ← Servidor TCP puro (terminal) — requisito principal da atividade
├── client.py ← Cliente TCP puro (terminal) — requisito principal da atividade
├── app.py ← Flask + SocketIO (versão web completa)
├── requirements.txt ← Dependências Python
├── Dockerfile ← Imagem Docker da versão web
├── docker-compose.yml ← Orquestração do container
├── .gitignore
├── README.md
└── templates/
├── base.html ← Layout base, CSS, DM widget, presença em tempo real
├── index.html ← Landing page
├── login.html ← Login / Cadastro
├── lobby.html ← Lobby + salas + chat global
├── game.html ← Arena NUMRUSH + 3 chats
└── battlezone.html ← Arena BATTLEZONE
pip install -r requirements.txtO projeto pode ser executado de duas formas independentes:
Implementação direta dos requisitos da atividade usando sockets TCP da biblioteca padrão do Python, sem nenhuma dependência externa além da cryptography.
# Terminal 1 — iniciar o servidor
python server.py
# Terminal 2, 3, 4... — iniciar um cliente por janela
python client.pyApós conectar, cada cliente recebe uma chave Fernet exclusiva e pode usar os comandos:
/users → lista usuários online
/dm <user> <msg> → mensagem privada para usuário específico
/all <msg> → broadcast para todos conectados
/data → envia dicionário de exemplo (demonstra envio de objetos Python)
/startgame → inicia o jogo "Adivinhe o número"
/guess <n> → envia tentativa durante o jogo
/gamestatus → placar do jogo atual
/quit → desconectar
Versão completa com interface visual, múltiplas salas, chat em 3 canais, DMs e dois jogos.
python app.py
# Acesse: http://localhost:5000Para testar com múltiplos jogadores, abra em janelas/abas diferentes (use nomes diferentes no login).
O Docker empacota o projeto junto com todas as dependências numa "caixinha" isolada chamada container, que roda igual em qualquer máquina (Windows, Mac, Linux ou servidor na nuvem) sem precisar instalar Python ou configurar nada manualmente.
Instalar o Docker Desktop. Após instalar, abra o Docker Desktop e aguarde o ícone da baleia 🐳 aparecer na barra de tarefas antes de prosseguir.
docker compose up --build
# Acesse: http://localhost:5000Na primeira vez demora cerca de 1 minuto (baixa a imagem do Python e instala as dependências). Nas próximas execuções é muito mais rápido.
docker compose up --build -d# Para e mantém o container
Ctrl + C
# Para e remove o container
docker compose downDescubra seu IP local:
ipconfig # Windows
ifconfig # Mac / LinuxOutros dispositivos na mesma rede acessam por:
http://SEU_IP_LOCAL:5000
Se não conectar, libere a porta no firewall do Windows (terminal como administrador):
netsh advfirewall firewall add rule name="NumRush" dir=in action=allow protocol=TCP localport=5000O Ngrok cria um túnel público temporário — gera um link acessível de qualquer dispositivo, em qualquer rede, sem precisar mexer no roteador.
# Instala o Ngrok
winget install ngrok
# Configura o token (obtido em ngrok.com após criar conta gratuita)
ngrok config add-authtoken SEU_TOKEN
# Com o Docker já rodando, em outro terminal:
ngrok http 5000O Ngrok vai exibir um link tipo https://a1b2c3.ngrok-free.app que qualquer pessoa consegue acessar enquanto o terminal estiver aberto.
O projeto implementa o modelo Cliente/Servidor sobre o protocolo TCP, onde:
- O servidor fica sempre rodando, aguardando conexões
- Os clientes se conectam ao servidor e se comunicam através dele
- Toda comunicação passa pelo servidor — os clientes nunca se falam diretamente
Cliente A ──┐
├──► Servidor ◄──► distribui para os demais
Cliente B ──┘
Dicionários Python são convertidos para JSON, opcionalmente criptografados, e enviados com um cabeçalho de 4 bytes indicando o tamanho da mensagem — técnica chamada de framing, que garante que mensagens grandes ou múltiplas mensagens seguidas não se misturem:
[ 4 bytes: tamanho ] [ N bytes: dados JSON (criptografados) ]
# Envio
raw = json.dumps({"tipo": "chat", "msg": "olá"}).encode()
raw = fernet.encrypt(raw) # criptografa
conn.sendall(struct.pack(">I", len(raw)) + raw) # envia com tamanho prefixado
# Recebimento
tamanho = struct.unpack(">I", conn.recv(4))[0] # lê os 4 bytes do tamanho
raw = conn.recv(tamanho) # lê exatamente N bytes
dados = json.loads(fernet.decrypt(raw)) # decifra e converteCada cliente que se conecta passa por um handshake inicial antes de trocar qualquer dado:
Cliente Servidor
│ │
│──── { type: "hello", │
│ username: "joao" } ─────►│ sem criptografia
│ │
│◄─── { type: "key", │
│ key: "ABC123..." } ──────│ sem criptografia — único envio em claro
│ │
│══ todas as mensagens seguintes são criptografadas com a chave recebida ══
A partir daí, toda mensagem que sai do cliente já vai criptografada, e o servidor só consegue ler porque tem a mesma chave.
O servidor mantém um dicionário em memória com todos os clientes conectados:
clients = {
"joao": { "conn": <socket>, "fernet": <chave>, "addr": ("192.168.1.2", 54321) },
"maria": { "conn": <socket>, "fernet": <chave>, "addr": ("192.168.1.3", 54322) },
}Cada cliente é identificado pelo seu username, enviado no primeiro contato. O servidor usa esse mapeamento para saber para qual socket encaminhar cada mensagem.
Para atender vários clientes ao mesmo tempo sem um bloquear o outro, o servidor cria uma thread separada para cada conexão:
conn, addr = server.accept() # aceita nova conexão
t = threading.Thread(
target=handle_client, # cada cliente roda em sua própria thread
args=(conn, addr),
daemon=True
)
t.start()Isso significa que o servidor consegue conversar com o cliente A enquanto ao mesmo tempo processa uma mensagem do cliente B.
| Requisito | Arquivo | Como está implementado |
|---|---|---|
| Servidor TCP escutando conexões | server.py |
socket.bind((HOST, 9999)) + socket.listen(10) — aguarda conexões na porta 9999 |
| Cliente TCP conectando ao servidor | client.py |
socket.connect((HOST, PORT)) — se conecta ao servidor |
| Enviar objetos Python (dicionários) pela rede | server.py / client.py |
send_msg() converte dict → JSON → bytes; recv_msg() faz o caminho inverso |
| Servidor processa dados e retorna resposta | server.py |
Comando /data envia um dicionário, servidor transforma os valores e devolve data_response |
| Servidor identifica cada cliente | server.py |
clients: dict[str, dict] — cada conexão é mapeada pelo username enviado no handshake |
| Chave única por cliente | server.py |
generate_key() chama Fernet.generate_key() a cada nova conexão |
| Criptografia síncrona (simétrica) | server.py / client.py |
Fernet (AES-128-CBC + HMAC-SHA256) — mesma chave cifra e decifra |
| Cliente envia dados criptografados | client.py |
Após receber a chave, todo send_msg(conn, data, fernet_global) usa criptografia |
| Desafio | Arquivo | Como está implementado |
|---|---|---|
| Sistema de mensagens entre múltiplos clientes | server.py |
Função broadcast() percorre todos os clientes e envia para cada um |
| Mensagem para usuário específico | server.py / client.py |
/dm <user> <msg> — send_to(username, data) busca o socket pelo nome e entrega direto |
| Mensagem para todos conectados | server.py / client.py |
/all <msg> — chama broadcast() que envia para todos simultaneamente |
| Jogo multiplayer com sockets | server.py / client.py |
"Adivinhe o número" — servidor sorteia número secreto, clientes enviam /guess, servidor responde maior/menor |
A versão web (app.py) reimplementa todos os requisitos com Flask-SocketIO e adiciona:
- Interface gráfica completa no navegador
- Sistema de salas com múltiplas partidas simultâneas
- Dois jogos: NUMRUSH e BATTLEZONE
- 3 canais de chat (global, sala, partida)
- Chat privado (DM) com histórico e badge de não-lidos
- Indicador de presença online em tempo real
- Criptografia XOR+Base64 nas mensagens de chat
- Suporte a Docker para fácil implantação
1. Cliente conecta → envia { type: "hello", username: "joao" }
2. Servidor gera chave Fernet exclusiva → envia em texto puro
3. Cliente armazena a chave
4. Toda mensagem seguinte é criptografada com fernet.encrypt()
5. Servidor decifra com fernet.decrypt() usando a mesma chave
O Fernet é um padrão de criptografia simétrica que combina AES-128-CBC (cifragem) com HMAC-SHA256 (autenticação). A mesma chave que cifra também decifra — por isso é chamado de simétrico ou síncrono.
Cliente digita: "olá"
Vai pela rede: { msg: "Dz8=", encrypted: true }
Servidor recebe: decifra → "olá"
O XOR é o algoritmo simétrico mais simples possível: aplica a mesma operação para cifrar e decifrar. O Base64 converte o resultado binário em texto para trafegar no JSON.
| Evento | Payload | Descrição |
|---|---|---|
create_room |
{name, max, game_type} |
Criar nova sala |
join_game_room |
{room_id} |
Entrar em sala existente |
leave_game_room |
{room_id} |
Sair da sala |
start_game |
{room_id} |
Host inicia a partida |
guess |
{room_id, value} |
Enviar chute (1–100) |
restart_game |
{room_id} |
Host reinicia após fim |
room_chat |
{room_id, chat_type, msg} |
Mensagem de sala ou partida |
global_chat |
{msg} |
Mensagem global (cifrada) |
send_dm |
{to, msg} |
Mensagem privada para outro jogador |
open_dm |
{with_user} |
Abrir conversa e buscar histórico |
get_dm_inbox |
— | Listar todas as conversas |
get_lobby |
— | Solicitar estado atual do lobby |
| Evento | Payload | Descrição |
|---|---|---|
session_key |
{key, algorithm} |
Chave de criptografia da sessão |
lobby_update |
{rooms, online} |
Lista de salas + usuários online |
online_update |
{online} |
Lista de online (presença em tempo real) |
global_history |
[msgs] |
Histórico do chat global |
global_message |
{from, msg, time} |
Nova mensagem global |
room_created |
{room_id, game_type} |
Sala criada com sucesso |
join_ok |
{room_id, game_type, room} |
Entrada na sala confirmada |
player_joined |
{username, players} |
Jogador entrou na sala |
player_left |
{username, players} |
Jogador saiu da sala |
new_host |
{host} |
Novo host definido |
game_started |
{players} |
Partida iniciada |
guess_result |
{guesser, guess, hint, attempts, scoreboard} |
Resultado do chute |
game_over |
{winner, number, reason, attempts, time} |
Fim de jogo |
game_reset |
{players, host} |
Partida reiniciada |
room_message_sala |
{from, msg, time} |
Mensagem do chat de sala |
room_message_partida |
{from, msg, time} |
Mensagem do chat de partida |
dm_message |
{from, to, msg, time} |
Mensagem privada recebida |
dm_opened |
{with_user, messages, online} |
Histórico de DM carregado |
dm_inbox |
{convos} |
Lista de conversas do inbox |
err |
{msg} |
Erro (sala cheia, não autorizado, etc.) |