From 759d8b776250daccae71b32470ee7286a71b24c6 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sat, 5 Aug 2023 09:56:55 -0400 Subject: [PATCH 1/7] Testa E PUB e cria+testa E NPU --- .../me/chester/minitruco/server/ComandoE.java | 19 ++++-- .../minitruco/server/ComandoETest.java | 64 +++++++++++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 server/src/test/java/me/chester/minitruco/server/ComandoETest.java diff --git a/server/src/main/java/me/chester/minitruco/server/ComandoE.java b/server/src/main/java/me/chester/minitruco/server/ComandoE.java index a7d24207..28b2ad0e 100644 --- a/server/src/main/java/me/chester/minitruco/server/ComandoE.java +++ b/server/src/main/java/me/chester/minitruco/server/ComandoE.java @@ -6,11 +6,15 @@ /** * Entra numa sala. *

- * Parâmetros: - * - "R ___" para entrar em uma sala (ou criar uma) com as regras especificadas - * (cada _ é T ou F para baralho limpo, manilha velha e modo mineiro) - *

- * Obs.: Se o jogador não tiver nome, um nome implícito será dado + * Recebe o tipo de sala e o modo de jogo. Tipos: + *

+ * TODO: NPU, PRI, PRI-nnnn + * Modos: "P" para truco paulista, "M" para mineiro, etc; vide classe `Modo` */ public class ComandoE extends Comando { @@ -29,7 +33,10 @@ public void executa(String[] args, JogadorConectado j) { if ("PUB".equals(args[1])) { Sala s = Sala.colocaEmSalaPublica(j, args[2]); s.mandaInfoParaTodos(); - // TODO: implementar comandos para sala privada + } else if ("NPU".equals(args[1])) { + Sala s = new Sala(true, args[2]); + s.adiciona(j); + s.mandaInfoParaTodos(); } else { j.println("X AI"); } diff --git a/server/src/test/java/me/chester/minitruco/server/ComandoETest.java b/server/src/test/java/me/chester/minitruco/server/ComandoETest.java new file mode 100644 index 00000000..9e1541a5 --- /dev/null +++ b/server/src/test/java/me/chester/minitruco/server/ComandoETest.java @@ -0,0 +1,64 @@ +package me.chester.minitruco.server; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ComandoETest { + + private static JogadorConectado j1, j2, j3; + + @BeforeEach + void setUp() { + Sala.limpaSalas(); + j1 = spy(new JogadorConectado(null)); + j2 = spy(new JogadorConectado(null)); + j3 = spy(new JogadorConectado(null)); + j1.setNome("j1"); + j2.setNome("j2"); + j3.setNome("j3"); + doNothing().when(j1).println(any()); + doNothing().when(j2).println(any()); + doNothing().when(j3).println(any()); + } + + @Test + void testProcuraSalaPublicaFazMatchDeRegra() { + Comando.interpreta("E PUB P", j1); + verify(j1).println("I j1|bot|bot|bot P 1 PUB"); + + Comando.interpreta("E PUB P", j2); + verify(j1).println("I j1|j2|bot|bot P 1 PUB"); + verify(j2).println("I j1|j2|bot|bot P 2 PUB"); + + Comando.interpreta("E PUB M", j3); + verify(j3).println("I j3|bot|bot|bot M 1 PUB"); + + verify(j1, times(2)).println(any()); + verify(j2, times(1)).println(any()); + } + + @Test + void testCriaSalaPublicaIgnoraSalaExistente() { + Comando.interpreta("E PUB P", j1); + Comando.interpreta("E NPU P", j2); + Comando.interpreta("E NPU P", j3); + + verify(j1).println("I j1|bot|bot|bot P 1 PUB"); + verify(j2).println("I j2|bot|bot|bot P 1 PUB"); + verify(j3).println("I j3|bot|bot|bot P 1 PUB"); + + verify(j1, times(1)).println(any()); + verify(j2, times(1)).println(any()); + verify(j3, times(1)).println(any()); + } + + // TODO: X NO faz sentido? Seria melhor dar um nome default + // TODO: X JE faz sentido? Seria melhor jogador sair da sala atual e entrar na nova + // TODO: X AI faz sentido? Seria melhor ignorar comandos e argumentos inválidos (e logar?) +} From 1ba3e6af611095d1e684c7357629c644539a1d93 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sat, 5 Aug 2023 09:57:34 -0400 Subject: [PATCH 2/7] =?UTF-8?q?Espa=C3=A7o=20faltante=20e=20coment=C3=A1ri?= =?UTF-8?q?o=20desatualizado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/me/chester/minitruco/android/SalaActivity.java | 2 +- server/src/main/java/me/chester/minitruco/server/Sala.java | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/me/chester/minitruco/android/SalaActivity.java b/app/src/main/java/me/chester/minitruco/android/SalaActivity.java index 4e78a0df..9d69a9fa 100644 --- a/app/src/main/java/me/chester/minitruco/android/SalaActivity.java +++ b/app/src/main/java/me/chester/minitruco/android/SalaActivity.java @@ -121,7 +121,7 @@ protected void exibeMesaForaDoJogo(String notificacaoI) { // parte inferior da tela (textViewJogador1), sucedido por // "(você)"; sucede a pessoa que é gerente com "(gerente)" int p = (posJogador - 1) % 4; - textViewJogador1.setText(nomes[p] + (p == 0 ? " (você/gerente)" : "(você)")); + textViewJogador1.setText(nomes[p] + (p == 0 ? " (você/gerente)" : " (você)")); p = (p + 1) % 4; textViewJogador2.setText(nomes[p] + (p == 0 ? " (gerente)" : "")); p = (p + 1) % 4; diff --git a/server/src/main/java/me/chester/minitruco/server/Sala.java b/server/src/main/java/me/chester/minitruco/server/Sala.java index e49328a7..d2282680 100644 --- a/server/src/main/java/me/chester/minitruco/server/Sala.java +++ b/server/src/main/java/me/chester/minitruco/server/Sala.java @@ -23,7 +23,7 @@ public class Sala { /** - * Salas criadas por usuários (a chave é o código da sala) + * Salas privadas (a chave é o código da sala) */ private static final Map salasPrivadas = new HashMap<>(); @@ -58,7 +58,7 @@ public static void limpaSalas() { } /** - * Código usado para os amigos acharem a sala; null se for uma sala pública + * Código usado para os amigos acharem a sala privada; null se for uma sala pública */ String codigo; @@ -76,7 +76,7 @@ public static void limpaSalas() { private PartidaLocal partida = null; /** - * Cria uma sala . + * Cria uma nova sala, pública ou privada */ public Sala(boolean publica, String modo) { if (publica) { @@ -92,7 +92,6 @@ public Sala(boolean publica, String modo) { /** * Coloca o jogador em uma sala pública que tenha aquele modo de partida * criando uma caso estejam todas lotadas - * */ public static synchronized Sala colocaEmSalaPublica(JogadorConectado j, String modo) { Sala sala = salasPublicasDisponiveis.stream().filter(s -> From c00c6f109d7fb1d24c8ff8ace1c4da6367af0374 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sat, 5 Aug 2023 11:52:36 -0400 Subject: [PATCH 3/7] Introduz Hamcrest matchers (para testes mais limpinhos com regex) --- app/build.gradle | 1 + core/build.gradle | 1 + .../src/test/java/me/chester/minitruco/core/JogadorTest.java | 5 +++-- server/build.gradle | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3232fb0f..1f23be05 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,4 +45,5 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0-M1' testImplementation 'org.mockito:mockito-core:5.3.1' testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' + testImplementation 'org.hamcrest:hamcrest:2.2' } diff --git a/core/build.gradle b/core/build.gradle index 57f28aa5..b794e59d 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -12,6 +12,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0-M1' testImplementation 'org.mockito:mockito-core:5.3.1' testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' + testImplementation 'org.hamcrest:hamcrest:2.2' } test { diff --git a/core/src/test/java/me/chester/minitruco/core/JogadorTest.java b/core/src/test/java/me/chester/minitruco/core/JogadorTest.java index 22125839..1b25948e 100644 --- a/core/src/test/java/me/chester/minitruco/core/JogadorTest.java +++ b/core/src/test/java/me/chester/minitruco/core/JogadorTest.java @@ -1,7 +1,8 @@ package me.chester.minitruco.core; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.matchesPattern; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -38,7 +39,7 @@ private static String sanitizaNome(String nome) { void assertNomeDefault(String nome) { String regex = "^sem_nome_\\d{1,3}$"; - assertTrue(nome.matches(regex), nome + " não deu match em " + regex); + assertThat(nome, matchesPattern(regex)); } @Test diff --git a/server/build.gradle b/server/build.gradle index 953a6e5f..e4ac3596 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -11,6 +11,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0-M1' testImplementation 'org.mockito:mockito-core:5.3.1' testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' + testImplementation 'org.hamcrest:hamcrest:2.2' } java { sourceCompatibility = JavaVersion.VERSION_19 From 150aa08bb545d67e8e75b1d3931bf0bcf8dd6b33 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sat, 5 Aug 2023 11:53:46 -0400 Subject: [PATCH 4/7] =?UTF-8?q?Implementa=20b=C3=A1sico=20da=20cria=C3=A7?= =?UTF-8?q?=C3=A3o=20e=20entrada=20em=20sala=20privada=20(E=20PRI=20modo?= =?UTF-8?q?=20e=20E=20PRI-xxxxx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../me/chester/minitruco/server/ComandoE.java | 21 ++++++++++++------- .../me/chester/minitruco/server/Sala.java | 17 +++++++++------ .../minitruco/server/ComandoETest.java | 21 +++++++++++++++++++ 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/me/chester/minitruco/server/ComandoE.java b/server/src/main/java/me/chester/minitruco/server/ComandoE.java index 28b2ad0e..2dd7fc3f 100644 --- a/server/src/main/java/me/chester/minitruco/server/ComandoE.java +++ b/server/src/main/java/me/chester/minitruco/server/ComandoE.java @@ -4,17 +4,14 @@ /* Copyright © 2005-2023 Carlos Duarte do Nascimento "Chester" */ /** - * Entra numa sala. - *

- * Recebe o tipo de sala e o modo de jogo. Tipos: + * Entra numa sala. Sintaxe: *

    - *
  • PUB - procura uma sala pública, se não achar, cria
  • - *
  • NPU - cria uma nova sala pública
  • - *
  • PRI - cria uma nova sala privada
  • + *
  • PUB modo - procura uma sala pública compatível, se não achar, cria
  • + *
  • NPU modo - cria nova sala pública
  • + *
  • PRI modo - cria nova sala privada
  • *
  • PRI-nnnnn - entra numa sala privada existente
  • *
- * TODO: NPU, PRI, PRI-nnnn - * Modos: "P" para truco paulista, "M" para mineiro, etc; vide classe `Modo` + * modos: "P" para truco paulista, "M" para mineiro, etc; vide classe `Modo` */ public class ComandoE extends Comando { @@ -37,6 +34,14 @@ public void executa(String[] args, JogadorConectado j) { Sala s = new Sala(true, args[2]); s.adiciona(j); s.mandaInfoParaTodos(); + } else if ("PRI".equals(args[1])) { + Sala s = new Sala(false, args[2]); + s.adiciona(j); + s.mandaInfoParaTodos(); + } else if (args[1].startsWith("PRI-")) { + Sala s = Sala.colocaEmSalaPrivada(j, args[1].substring(4)); + // TODO tratar null + s.mandaInfoParaTodos(); } else { j.println("X AI"); } diff --git a/server/src/main/java/me/chester/minitruco/server/Sala.java b/server/src/main/java/me/chester/minitruco/server/Sala.java index d2282680..0bd1c641 100644 --- a/server/src/main/java/me/chester/minitruco/server/Sala.java +++ b/server/src/main/java/me/chester/minitruco/server/Sala.java @@ -82,7 +82,7 @@ public Sala(boolean publica, String modo) { if (publica) { salasPublicasDisponiveis.add(this); } else { - String codigo = UUID.randomUUID().toString().substring(0, 5); + String codigo = UUID.randomUUID().toString().toUpperCase().substring(0, 5); this.codigo = codigo; salasPrivadas.put(codigo, this); } @@ -92,6 +92,8 @@ public Sala(boolean publica, String modo) { /** * Coloca o jogador em uma sala pública que tenha aquele modo de partida * criando uma caso estejam todas lotadas + * + * @return sala em que foi colocado */ public static synchronized Sala colocaEmSalaPublica(JogadorConectado j, String modo) { Sala sala = salasPublicasDisponiveis.stream().filter(s -> @@ -105,13 +107,16 @@ public static synchronized Sala colocaEmSalaPublica(JogadorConectado j, String m } /** - * Coloca o jogador em uma sala privada pré-existente - * @param codigo o código recebido de quem criou a sala - * @return false caso a sala não tenha sido encontrada ou esteja lotada + * Coloca o jogador em uma sala privada pré-existente (através do código) + * + * @return sala em que foi colocado, ou null se não existir/estiver lotada */ - public static synchronized boolean colocaEmSalaPrivada(JogadorConectado j, String codigo) { + public static synchronized Sala colocaEmSalaPrivada(JogadorConectado j, String codigo) { Sala sala = salasPrivadas.get(codigo); - return (sala != null && sala.adiciona(j)); + if (sala != null && sala.adiciona(j)) { + return sala; + } + return null; } /** diff --git a/server/src/test/java/me/chester/minitruco/server/ComandoETest.java b/server/src/test/java/me/chester/minitruco/server/ComandoETest.java index 9e1541a5..59cac467 100644 --- a/server/src/test/java/me/chester/minitruco/server/ComandoETest.java +++ b/server/src/test/java/me/chester/minitruco/server/ComandoETest.java @@ -1,5 +1,7 @@ package me.chester.minitruco.server; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.matchesPattern; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.spy; @@ -8,6 +10,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; class ComandoETest { @@ -58,6 +61,24 @@ void testCriaSalaPublicaIgnoraSalaExistente() { verify(j3, times(1)).println(any()); } + @Test + void testSalaPrivada() { + ArgumentCaptor notificacaoCaptor = ArgumentCaptor.forClass(String.class); + Comando.interpreta("E PRI P", j1); + verify(j1).println(notificacaoCaptor.capture()); + + String notificacao = notificacaoCaptor.getValue(); + assertThat(notificacao, matchesPattern("I j1\\|bot\\|bot\\|bot P 1 PRI-[0-9A-Z]+")); + String salaComPrefixo = notificacao.split(" ")[4]; + + Comando.interpreta("E " + salaComPrefixo, j2); + verify(j1).println("I j1|j2|bot|bot P 1 " + salaComPrefixo); + verify(j2).println("I j1|j2|bot|bot P 2 " + salaComPrefixo); + + verify(j1, times(2)).println(any()); + verify(j2, times(1)).println(any()); + } + // TODO: X NO faz sentido? Seria melhor dar um nome default // TODO: X JE faz sentido? Seria melhor jogador sair da sala atual e entrar na nova // TODO: X AI faz sentido? Seria melhor ignorar comandos e argumentos inválidos (e logar?) From 22bcae1747428a90fad1c9baa306e64d79602ec6 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sat, 5 Aug 2023 13:52:30 -0400 Subject: [PATCH 5/7] Garante que todo jogador conectado tenha nome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (a app sempre vai atribuir, mas evita um mol de checagens por aí e deixa o log mais limpinho) --- .../main/java/me/chester/minitruco/server/ComandoN.java | 1 + .../me/chester/minitruco/server/JogadorConectado.java | 1 + .../java/me/chester/minitruco/server/ServerLogger.java | 2 +- .../me/chester/minitruco/server/JogadorConectadoTest.java | 8 ++++++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/me/chester/minitruco/server/ComandoN.java b/server/src/main/java/me/chester/minitruco/server/ComandoN.java index 12779ac4..b3cafdae 100644 --- a/server/src/main/java/me/chester/minitruco/server/ComandoN.java +++ b/server/src/main/java/me/chester/minitruco/server/ComandoN.java @@ -26,6 +26,7 @@ public void executa(String[] args, JogadorConectado j) { String nome = Jogador.sanitizaNome( (comando + " ").substring(2) ); + ServerLogger.evento("Nome mudou de " + j.getNome() + " para " + nome); j.setNome(nome); j.println("N " + nome); } diff --git a/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java b/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java index 0923cf8f..6f48189e 100644 --- a/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java +++ b/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java @@ -52,6 +52,7 @@ public class JogadorConectado extends Jogador implements Runnable { */ public JogadorConectado(Socket cliente) { this.cliente = cliente; + this.setNome(Jogador.sanitizaNome("")); // atribui um nome padrão } @FunctionalInterface diff --git a/server/src/main/java/me/chester/minitruco/server/ServerLogger.java b/server/src/main/java/me/chester/minitruco/server/ServerLogger.java index 024de7a5..fadcfdd8 100644 --- a/server/src/main/java/me/chester/minitruco/server/ServerLogger.java +++ b/server/src/main/java/me/chester/minitruco/server/ServerLogger.java @@ -49,7 +49,7 @@ public static synchronized void evento(Jogador j, String mensagem) { System.out.print("[sem_sala] "); } if (j != null) { - System.out.print(!j.getNome().equals("unnamed") ? j.getNome() : "[sem_nome]"); + System.out.print(j.getNome()); if (j instanceof JogadorConectado) { System.out.print('@'); System.out.print(((JogadorConectado) j).getIp()); diff --git a/server/src/test/java/me/chester/minitruco/server/JogadorConectadoTest.java b/server/src/test/java/me/chester/minitruco/server/JogadorConectadoTest.java index d4408707..5a25aa42 100644 --- a/server/src/test/java/me/chester/minitruco/server/JogadorConectadoTest.java +++ b/server/src/test/java/me/chester/minitruco/server/JogadorConectadoTest.java @@ -1,5 +1,7 @@ package me.chester.minitruco.server; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.text.MatchesPattern.matchesPattern; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @@ -43,4 +45,10 @@ void testOnFinish() throws InterruptedException { assertTrue(chamouOnFinished[0]); } + @Test + void testNovoJogadorTemNomePadrao() { + JogadorConectado j = new JogadorConectado(mockSocket); + assertThat(j.getNome(), matchesPattern("^sem_nome_\\d{1,3}$")); + } + } From 104a657eba1129c4ac4494d38c7ab605c5ba9e07 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sat, 5 Aug 2023 13:55:20 -0400 Subject: [PATCH 6/7] =?UTF-8?q?Helper=20para=20n=C3=A3o=20lidar=20com=20ex?= =?UTF-8?q?ce=C3=A7=C3=B5es=20fora=20da=20classe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/me/chester/minitruco/core/Modo.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/src/main/java/me/chester/minitruco/core/Modo.java b/core/src/main/java/me/chester/minitruco/core/Modo.java index 81aa5650..f8ba2249 100644 --- a/core/src/main/java/me/chester/minitruco/core/Modo.java +++ b/core/src/main/java/me/chester/minitruco/core/Modo.java @@ -10,6 +10,12 @@ */ public interface Modo { + /** + * @return instância de Modo correspondente ao modoStr + * @param modoStr String de 1 caractere indicando o modo desejado. Ex.: + * "M" para mineiro, "P" para paulista, etc. + * @throws IllegalArgumentException se o modo for inválido + */ static Modo fromString(String modoStr) { switch (modoStr) { case "M": @@ -25,6 +31,15 @@ static Modo fromString(String modoStr) { } } + static boolean isModoValido(String modoStr) { + try { + fromString(modoStr); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + int pontuacaoParaMaoDeX(); int valorInicialDaMao(); From f8579dc54441934e1372935d0574c20857c05463 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sat, 5 Aug 2023 14:04:36 -0400 Subject: [PATCH 7/7] Checa todos os argumentos e simplifica sintaxe do ComandoE, documentando. --- docs/documentacao-para-desenvolvimento.md | 16 ++++-- .../me/chester/minitruco/server/ComandoE.java | 55 ++++++++++--------- .../me/chester/minitruco/server/Sala.java | 15 +++++ .../minitruco/server/ComandoETest.java | 50 +++++++++++++++-- 4 files changed, 100 insertions(+), 36 deletions(-) diff --git a/docs/documentacao-para-desenvolvimento.md b/docs/documentacao-para-desenvolvimento.md index 361e0b44..374801af 100644 --- a/docs/documentacao-para-desenvolvimento.md +++ b/docs/documentacao-para-desenvolvimento.md @@ -281,7 +281,11 @@ Quando o miniTruco foi criado (2005), poucas pessoas possuíam celulares e plano Idealmente isso seria feito serializando as chamadas e objetos com um protocolo binário (se fosse hoje em dia, algo como [Protobuf](https://protobuf.dev/)). Mas eu também queria que fosse possível interagir diretamente com o servidor via terminal (para testes, depuração e também por diversão), então acabei criando uma "linguagem" que define comandos e notificações em texto simples. -O protocolo consiste em _comandos_ enviados pelo cliente (ex.: `J 3c` para "`J`ogar o `3` de `c`opas") e _notificações_ enviadas pelo servidor (ex.: `V 2 F` para "`v`ez do jogador na posição `2`, que não pode (`F`alse) jogar fechada). Os clientes devem processar as notificações assincronamente, e podem enviar comandos a qualquer momento, desde que faça sentido (ex.: o comando `J` só funciona se um jogo estiver em andamento e for a vez do jogador). +O protocolo consiste em _comandos_ enviados pelo cliente (ex.: `J 3c` para "`J`ogar o `3` de `c`opas") e _notificações_ enviadas pelo servidor (ex.: `V 2 F` para "`v`ez do jogador na posição `2`, que não pode (`F`alse) jogar fechada). + +Os clientes devem processar as notificações assincronamente, e podem enviar comandos a qualquer momento, desde que faça sentido (ex.: o comando `J` só funciona se um jogo estiver em andamento e for a vez do jogador). + +Comandos com erros de sintaxe ou argumentos inválidos são ignorados. ### Testando (jogando) via nc/telnet @@ -310,7 +314,8 @@ TODO colocar um exemplo de jogo aqui (GIF ou whatnot) - ``: Quatro sequências de caracteres, separadas por `|`. Exemplo: `john|bot|ringo|george`. - ``: `P` para truco paulista, `M` para truco mineiro, `V` para manilha velha ou `L` baralho limpo. - ``: Posição de um jogador na sala/partida, de 1 a 4. É constante durante a partida, mas pode mudar fora dela (o servidor manda uma notificação `I` sempre que a formação da sala mudar). -- `` : Informa o tipo de sala em que estamos conectados. Pode ser `BLT` (bluetooth), `PUB` (pública) ou `PRI-nnnnn` (privada, com o código nnnnn). +- `` : Informa o tipo de sala em que estamos conectados. Pode ser `BLT` (bluetooth), `PUB` (pública) ou `PRI-código` (privada, com o código especificado). +- `` : String de números e letras maiúsculas que identifica uma sala privada. Exemplo: `A9327J`. - ``: Uma das duas equipes (duplas). Pode ser 1 (equpe dos jogadores 1 e 3) ou 2 (jogadores 2 e 4). - ``: número aleatório grande que permite que todos os clientes mostrem a mesma frase (o "balãozinho") para um evento. Por exemplo, se o jogador 1 pediu truco (paulista) e o número sorteado foi 12345678, todos irão receber `T 1 3 12345678`; se o cliente tem 8 frases possíveis para truco, ele calcula 12345678 % 8 = 6 e exibe a frase de índice 6. Dessa forma, todos os clientes mostram a mesma frase (se estiverem com a mesma versão do [strings.xml](../app/src/main/res/values/strings.xml)) e o servidor não tem que saber quantas frases tem cada tipo de mensagem. @@ -321,6 +326,9 @@ TODO colocar um exemplo de jogo aqui (GIF ou whatnot) - `B `: Informa o número do build do cliente, para que o servidor verifique compatibilidade - `N `: Define o nome do jogador (é sanitizado; se for 100% inválido recebe um default) - `E PUB `: Entra em uma sala pública (criando, se não tiver nenhuma com vaga) com o modo especificado +- `E NPU `: Cria uma nova sala pública e entra nela +- `E PRI `: Cria uma nova sala privada e entra nela +- `E PRI-`: Entra em uma sala privada com o código #### Dentro da sala (fora de jogo) - `S`: Sai da sala (encerrando a partida, se houver uma em andamento) @@ -347,10 +355,6 @@ TODO colocar um exemplo de jogo aqui (GIF ou whatnot) - `W x.y`: `Versão do servidor (outras infos podem vir no futuro) - `X CI`: `Comando inválido -- `X AI`: `Argumento inválido -- `X FS`: `Você não está numa sala -- `X NO`: `É preciso atribuir um nome para entrar na sala -- `X JE sala`: Você já está na sala de código `sala` - `N nome`: Seu nome foi definido como `nome` - `I `: Informações da sala (vide detalhes em "convenções") - `P `: Início da partida diff --git a/server/src/main/java/me/chester/minitruco/server/ComandoE.java b/server/src/main/java/me/chester/minitruco/server/ComandoE.java index 2dd7fc3f..a9bbd8eb 100644 --- a/server/src/main/java/me/chester/minitruco/server/ComandoE.java +++ b/server/src/main/java/me/chester/minitruco/server/ComandoE.java @@ -3,6 +3,8 @@ /* SPDX-License-Identifier: BSD-3-Clause */ /* Copyright © 2005-2023 Carlos Duarte do Nascimento "Chester" */ +import me.chester.minitruco.core.Modo; + /** * Entra numa sala. Sintaxe: *
    @@ -17,37 +19,38 @@ public class ComandoE extends Comando { @Override public void executa(String[] args, JogadorConectado j) { - - try { - if (j.getNome().equals("unnamed")) { - j.println("X NO"); + String subcomando, modo = null; + if (args.length < 2 || args.length > 3) { + return; + } + subcomando = args[1]; + if (args.length == 3) { + modo = args[2]; + if (!Modo.isModoValido(modo)) { return; } - if (j.getSala() != null) { - // TODO: mostrar o código se for sala pública? - j.println("X JE " + j.getSala().codigo); + } + if (j.getSala() != null) { + (new ComandoS()).executa(new String[]{"S"}, j); + } + try { + Sala s; + if ("PUB".equals(subcomando)) { + s = Sala.colocaEmSalaPublica(j, modo); + } else if ("NPU".equals(subcomando)) { + s = Sala.colocaEmNovaSala(j,true, modo); + } else if ("PRI".equals(subcomando)) { + s = Sala.colocaEmNovaSala(j,false, modo); + } else if (subcomando.startsWith("PRI-")) { + s = Sala.colocaEmSalaPrivada(j, subcomando.substring(4)); } else { - if ("PUB".equals(args[1])) { - Sala s = Sala.colocaEmSalaPublica(j, args[2]); - s.mandaInfoParaTodos(); - } else if ("NPU".equals(args[1])) { - Sala s = new Sala(true, args[2]); - s.adiciona(j); - s.mandaInfoParaTodos(); - } else if ("PRI".equals(args[1])) { - Sala s = new Sala(false, args[2]); - s.adiciona(j); - s.mandaInfoParaTodos(); - } else if (args[1].startsWith("PRI-")) { - Sala s = Sala.colocaEmSalaPrivada(j, args[1].substring(4)); - // TODO tratar null - s.mandaInfoParaTodos(); - } else { - j.println("X AI"); - } + return; + } + if (s != null) { + s.mandaInfoParaTodos(); } } catch (NumberFormatException | IndexOutOfBoundsException e) { - j.println("X AI"); + // Simplesmente ignoramos qualquer erro causado pelo comando } } diff --git a/server/src/main/java/me/chester/minitruco/server/Sala.java b/server/src/main/java/me/chester/minitruco/server/Sala.java index 0bd1c641..6e05ed16 100644 --- a/server/src/main/java/me/chester/minitruco/server/Sala.java +++ b/server/src/main/java/me/chester/minitruco/server/Sala.java @@ -119,6 +119,21 @@ public static synchronized Sala colocaEmSalaPrivada(JogadorConectado j, String c return null; } + /** + * Cria uma nova sala e coloca o jogador nela + * + * @param j Jogador a ser colocado na sala (vai ser sempre o gerente) + * @param publica true se a sala for pública, false se for privada + * @param modo "P" para paulista, "M" para mineiro, etc. + * @return nova sala em que foi colocado + */ + public static synchronized Sala colocaEmNovaSala(JogadorConectado j, boolean publica, String modo) { + Sala sala = new Sala(publica, modo); + sala.adiciona(j); + + return sala; + } + /** * Adiciona um jogador na primeira posição disponível da sala, * garantindo os links bidirecionais e, se necessário, diff --git a/server/src/test/java/me/chester/minitruco/server/ComandoETest.java b/server/src/test/java/me/chester/minitruco/server/ComandoETest.java index 59cac467..b7d5aa37 100644 --- a/server/src/test/java/me/chester/minitruco/server/ComandoETest.java +++ b/server/src/test/java/me/chester/minitruco/server/ComandoETest.java @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.matchesPattern; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -14,7 +15,7 @@ class ComandoETest { - private static JogadorConectado j1, j2, j3; + private static JogadorConectado j1, j2, j3, jAnon; @BeforeEach void setUp() { @@ -22,12 +23,14 @@ void setUp() { j1 = spy(new JogadorConectado(null)); j2 = spy(new JogadorConectado(null)); j3 = spy(new JogadorConectado(null)); + jAnon = spy(new JogadorConectado(null)); j1.setNome("j1"); j2.setNome("j2"); j3.setNome("j3"); doNothing().when(j1).println(any()); doNothing().when(j2).println(any()); doNothing().when(j3).println(any()); + doNothing().when(jAnon).println(any()); } @Test @@ -79,7 +82,46 @@ void testSalaPrivada() { verify(j2, times(1)).println(any()); } - // TODO: X NO faz sentido? Seria melhor dar um nome default - // TODO: X JE faz sentido? Seria melhor jogador sair da sala atual e entrar na nova - // TODO: X AI faz sentido? Seria melhor ignorar comandos e argumentos inválidos (e logar?) + @Test + void testArgumentosESalasInvalidoaSaoIgnoradas() { + Comando.interpreta("E", j1); + Comando.interpreta("E ", j1); + Comando.interpreta("E XYZ", j1); + Comando.interpreta("E XYZ A", j1); + Comando.interpreta("E PUB !", j1); + Comando.interpreta("E PRI-SALA404", j1); + verify(j1, never()).println(any()); + } + + @Test + void testTiraDaSalaAtualSeJaEstiverEmUma() { + Comando.interpreta("E PUB M", j1); + verify(j1).println("I j1|bot|bot|bot M 1 PUB"); + Comando.interpreta("E PUB M", j2); + verify(j1).println("I j1|j2|bot|bot M 1 PUB"); + verify(j2).println("I j1|j2|bot|bot M 2 PUB"); + Comando.interpreta("E PUB M", j3); + verify(j1).println("I j1|j2|j3|bot M 1 PUB"); + verify(j2).println("I j1|j2|j3|bot M 2 PUB"); + verify(j3).println("I j1|j2|j3|bot M 3 PUB"); + + Comando.interpreta("E PUB P", j1); + verify(j1).println("S"); + verify(j1).println("I j1|bot|bot|bot P 1 PUB"); + verify(j2).println("I j2|j3|bot|bot M 1 PUB"); + verify(j3).println("I j2|j3|bot|bot M 2 PUB"); + + verify(j1, times(5)).println(any()); + verify(j2, times(3)).println(any()); + verify(j3, times(2)).println(any()); + } + + @Test + void testUsaNomeDefaultSeNaoForInformado() { + ArgumentCaptor notificacaoCaptor = ArgumentCaptor.forClass(String.class); + Comando.interpreta("E PUB P", jAnon); + verify(jAnon).println(notificacaoCaptor.capture()); + String notificacao = notificacaoCaptor.getValue(); + assertThat(notificacao, matchesPattern("I sem_nome_.+\\|bot\\|bot\\|bot P 1 PUB")); + } }