From 5f17e872064b35099e2860e0b64bcf03a0bc7870 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 07:56:12 -0400 Subject: [PATCH 01/14] Tira o foco do input de nome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (evitando que o teclado virtual apareça de cara em aparelhos antigos, que são justamente os mais lentos nesse sentido) --- .../minitruco/android/TituloActivity.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/me/chester/minitruco/android/TituloActivity.java b/app/src/main/java/me/chester/minitruco/android/TituloActivity.java index 152a4870..04f6e7b9 100644 --- a/app/src/main/java/me/chester/minitruco/android/TituloActivity.java +++ b/app/src/main/java/me/chester/minitruco/android/TituloActivity.java @@ -14,6 +14,7 @@ import android.os.Bundle; import android.provider.Settings; import android.view.View; +import android.view.WindowManager; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; @@ -182,7 +183,7 @@ private void pedeNome(Consumer callback) { editNomeJogador.setText(nome); runOnUiThread(() -> { - new AlertDialog.Builder(this) + AlertDialog dialogNome = new AlertDialog.Builder(this) .setIcon(R.mipmap.ic_launcher) .setTitle("Nome") .setMessage("Qual nome você gostaria de usar?") @@ -195,7 +196,20 @@ private void pedeNome(Consumer callback) { callback.accept(nomeFinal); }) .setNegativeButton("Cancela", null) - .show(); + .create(); + + // Evita mostrar o teclado de cara em alguns Androids mais antigos + // (não funciona em todos, mas evita a "dança" do diálogo em alguns) + dialogNome.setOnShowListener(d -> { + Button btnOk = dialogNome.getButton(AlertDialog.BUTTON_POSITIVE); + btnOk.setFocusable(true); + btnOk.setFocusableInTouchMode(true); + btnOk.requestFocus(); + + }); + dialogNome.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + + dialogNome.show(); }); } From 5d18585f4bb24d437a2bfa958b06ca245297610e Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 08:18:13 -0400 Subject: [PATCH 02/14] =?UTF-8?q?Verifica=20se=20est=C3=A1=20de=20fato=20c?= =?UTF-8?q?onectado=20na=20internet=20antes=20de=20pedir=20o=20nome?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minitruco/android/TituloActivity.java | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/me/chester/minitruco/android/TituloActivity.java b/app/src/main/java/me/chester/minitruco/android/TituloActivity.java index 04f6e7b9..56422719 100644 --- a/app/src/main/java/me/chester/minitruco/android/TituloActivity.java +++ b/app/src/main/java/me/chester/minitruco/android/TituloActivity.java @@ -1,5 +1,6 @@ package me.chester.minitruco.android; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.provider.Settings.Global.DEVICE_NAME; import static android.text.InputType.TYPE_CLASS_TEXT; @@ -10,6 +11,7 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageManager; +import android.net.ConnectivityManager; import android.os.Build; import android.os.Bundle; import android.provider.Settings; @@ -153,14 +155,34 @@ private void configuraBotoesMultiplayer() { } if (temInternet) { btnInternet.setOnClickListener(v -> { - pedeNome((nome) -> { - startActivity(new Intent(getBaseContext(), - ClienteInternetActivity.class)); - }); + if (conectadoNaInternet()) { + pedeNome((nome) -> { + startActivity(new Intent(getBaseContext(), + ClienteInternetActivity.class)); + }); + } else { + mostraAlertBox("Sem conexão", + "Não foi possível conectar à Internet. Verifique sua conexão e tente novamente."); + } }); } } + private boolean conectadoNaInternet() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); + return cm.getNetworkCapabilities(cm.getActiveNetwork()).hasCapability(NET_CAPABILITY_INTERNET); + } else { + // Android < 6, vamos assumir que tem internet + // (se não conectar só vai vir uma mensagem feia mesmo) + return true; + } + } catch (Exception e) { + return false; + } + } + private void pedeNome(Consumer callback) { // Se já temos um nome guardado, é ele String nome = preferences.getString("nome_multiplayer", null); From 560826ea38d711987c79b5d175cf491c8a297240 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 10:24:19 -0400 Subject: [PATCH 03/14] Muda nome default para sem_nome_nnn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nnn é um número entre 1 e 999 com isso evitamos de "Jogador(a)" virar "Jogadora" se re-sanitizado acrescentei um teste de idempotência pra sacramentar isso --- .../me/chester/minitruco/core/Jogador.java | 2 +- .../chester/minitruco/core/JogadorTest.java | 39 +++++++++++++++---- .../minitruco/server/ComandoNTest.java | 14 +++++-- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/me/chester/minitruco/core/Jogador.java b/core/src/main/java/me/chester/minitruco/core/Jogador.java index cbc92d54..4d8a211d 100644 --- a/core/src/main/java/me/chester/minitruco/core/Jogador.java +++ b/core/src/main/java/me/chester/minitruco/core/Jogador.java @@ -43,7 +43,7 @@ public static String sanitizaNome(String nome) { .replaceAll(" +","_") .replaceAll("^(.{0,25}).*$", "$1") .replaceAll("_$","") - .replaceAll("^[-_ ]*$", "Jogador(a)"); + .replaceAll("^[-_ ]*$", "sem_nome_"+(1 + random.nextInt(999))); } /** 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 cafd19f8..4c34580e 100644 --- a/core/src/test/java/me/chester/minitruco/core/JogadorTest.java +++ b/core/src/test/java/me/chester/minitruco/core/JogadorTest.java @@ -1,6 +1,7 @@ package me.chester.minitruco.core; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -35,16 +36,38 @@ private static String sanitizaNome(String nome) { assertEquals("crashy_on_iOS_Power_h_0", sanitizaNome("crashy on iOS: Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗🏳0🌈️జ్ఞ‌ా\uDB40\uDC00 ")); } + void assertNomeDefault(String nome) { + String regex = "^sem_nome_\\d{1,4}$"; + assertTrue(nome.matches(regex), nome + " não deu match em " + regex); + } + @Test void sanitizaNomeUsaDefaultSeNãoTiverCaracteresVálidos() { - assertEquals("Jogador(a)", sanitizaNome(null)); - assertEquals("Jogador(a)", sanitizaNome("")); - assertEquals("Jogador(a)", sanitizaNome("_")); - assertEquals("Jogador(a)", sanitizaNome("-")); - assertEquals("Jogador(a)", sanitizaNome("___--__")); - assertEquals("Jogador(a)", sanitizaNome("-------")); - assertEquals("Jogador(a)", sanitizaNome("💩")); - assertEquals("Jogador(a)", sanitizaNome("誰かの名前を日本語で")); + assertNomeDefault(sanitizaNome(null)); + assertNomeDefault(sanitizaNome("")); + assertNomeDefault(sanitizaNome("_")); + assertNomeDefault(sanitizaNome("-")); + assertNomeDefault(sanitizaNome("___--__")); + assertNomeDefault(sanitizaNome("-------")); + assertNomeDefault(sanitizaNome("💩")); + assertNomeDefault(sanitizaNome("誰かの名前を日本語で")); + } + + @Test + void sanitizaNomeÉIdempotente() { + String[] nomes = new String[]{ + "nome", + "nome_com_underscore", + "nome-com-hífen", + "nome com espaços", + "nome com espaços e _ e - e 💩 e 123", + null, + "", + "sem_nome_123"}; + for (String nome : nomes) { + String sanitizado = sanitizaNome(nome); + assertEquals(sanitizado, sanitizaNome(sanitizado)); + } } @Test diff --git a/server/src/test/java/me/chester/minitruco/server/ComandoNTest.java b/server/src/test/java/me/chester/minitruco/server/ComandoNTest.java index cc2bc698..f84febe6 100644 --- a/server/src/test/java/me/chester/minitruco/server/ComandoNTest.java +++ b/server/src/test/java/me/chester/minitruco/server/ComandoNTest.java @@ -1,6 +1,7 @@ package me.chester.minitruco.server; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.matches; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -20,14 +21,19 @@ static void setUp() { void verificaResposta(String linha, String resposta) { Mockito.reset(jogadorConectado); Comando.interpreta(linha, jogadorConectado); - verify(jogadorConectado).println(eq(resposta)); + if (resposta == "") { + String regex = "^N sem_nome_\\d{1,4}$"; + verify(jogadorConectado).println(matches(regex)); + } else { + verify(jogadorConectado).println(eq(resposta)); + } } @Test void testNomesInvalidosViramNomeDefault() { - verificaResposta("N", "N Jogador(a)"); - verificaResposta("N ", "N Jogador(a)"); - verificaResposta("N !@#$", "N Jogador(a)"); + verificaResposta("N", ""); + verificaResposta("N ", ""); + verificaResposta("N !@#$", ""); } @Test From 319257f7db673cc84a4edb1911a70019c80dcae7 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 12:07:12 -0400 Subject: [PATCH 04/14] =?UTF-8?q?Atualiza=20documenta=C3=A7=C3=A3o=20com?= =?UTF-8?q?=20hierarquias=20de=20salas,=20cliente=20internet=20e=20outros?= =?UTF-8?q?=20lances?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/desenvolvimento.md | 121 +++++++++++++++++++++++++++++++++------- 1 file changed, 100 insertions(+), 21 deletions(-) diff --git a/docs/desenvolvimento.md b/docs/desenvolvimento.md index 14c6d938..34798702 100644 --- a/docs/desenvolvimento.md +++ b/docs/desenvolvimento.md @@ -14,13 +14,20 @@ - [Arquitetura de Classes](#arquitetura-de-classes) - [Partidas e Jogadores](#partidas-e-jogadores) - [Jogo simples (single player)](#jogo-simples-single-player) + - [Jogo Multiplayer](#jogo-multiplayer) + - [Diferenças conceituais em relação ao single-player](#diferenças-conceituais-em-relação-ao-single-player) + - [Implementação](#implementação) - [Jogo via Bluetooth](#jogo-via-bluetooth) - [Jogo via Internet](#jogo-via-internet) - [Protocolo de comunicação multiplayer](#protocolo-de-comunicação-multiplayer) + - [Jogando via nc/telnet](#jogando-via-nctelnet) - [Convenções](#convenções) - [Comandos](#comandos) - - [Fora do jogo](#fora-do-jogo) + - [Fora da sala](#fora-da-sala) + - [Dentro da sala (fora de jogo)](#dentro-da-sala-fora-de-jogo) + - [Dentro da sala (fora de jogo e gerente)](#dentro-da-sala-fora-de-jogo-e-gerente) - [Durante o jogo](#durante-o-jogo) + - [A qualquer momento](#a-qualquer-momento) - [Notificações](#notificações) - [Estratégia dos bots](#estratégia-dos-bots) - [Assets gráficos](#assets-gráficos) @@ -114,7 +121,8 @@ O [vocabulário típico do truco](https://www.jogosdorei.com.br/blog/girias-do-t - **Aumento**: quando um jogador pede para aumentar o valor da rodada ("truco", que aumenta para 3 ou 4 pontos, "seis", "oito"/"nove" ou "doze", conforme o modo de jogo). - **Mão de X**: é a mão de 11 do truco paulista, ou mão de 10 do truco mineiro (quando apenas uma das duplas tem essa pontuação e pode optar por jogar ou não). - **Baralho**: visualmente, é o bitmap desenhado quando a carta está fechada (valor virado para baixo). Três dessas cartas são desenhadas no canto superior direito para simbolizar o baralho todo. Não confundir com a classe [`Baralho`](../core/src/main/java/me/chester/minitruco/core/Baralho.java), que faz parte do core e é quem sorteia as [`Carta`](../core/src/main/java/me/chester/minitruco/core/Carta.java)s. - +- **Posição**: visualmente, o jogo define a posição de um jogador como um número de 1 a 4. A posição 1 está na parte inferior da tela, a 2 na direita, a 3 acima e a 4 à esquerda. As posições 1 e 3 formam uma dupla, e as posições 2 e 4 formam a outra. +- ## Arquitetura de Classes ### Partidas e Jogadores @@ -153,47 +161,111 @@ Neste modo (que é o padrão do jogo, iniciado ao tocar o botão "Jogar) as trê - `JogadorHumano` faz a ponte entre a partida e a UI do Android. Ele recebe as notificações da partida e traduz em elementos visuais (de `TrucoActivity` e `MesaView`). Quando o usuário interage com estes elementos, ela envia os comandos correspondentes à partida. - `JogadorBot` faz a ponte entre a partida e uma `Estrategia`. Da mesma forma que `JogadorHumano`, ela recebe as notificações da partida, mas se concentra basicamente em eventos que precisam de uma resposta (ex.: é a vez daquele bot), chamando métodos de `Estrategia` e, de acordo com a resposta, enviando comandos à partida. -Vale observar que a UI só reage quando a partida notifica `JogadorHumano`. Por exemplo, se ele pede truco, o balão só aparece quando a partida manda a notificação dizendo "jogador X pediu truco". Isso também vale para eventos dos outros jogadores: quando um bot joga uma carta, a animação aparece quando `JogadorHumano` recebe a notfiifição de "jogador Y jogou a carta Ij". +Vale observar que a UI só reage quando a partida notifica `JogadorHumano`. Por exemplo, se ele pede truco, o balão só aparece quando a partida manda a notificação dizendo "jogador X pediu truco". Isso também vale para eventos dos outros jogadores: quando um bot joga uma carta, a animação aparece quando `JogadorHumano` recebe a notfifição de "jogador Y jogou a carta Ij". -Essa separação radical simplifica os jogadores (`JogadorHumano` não precisa entender as regras do jogo, `JogadorBot` só se preocupa em jogar), evita trapaças (`PartidaLocal` é a única autoridade) e permite total reuso no multiplayer, como veremos a seguir. +Neste modo, o `JogadorHumano` sempre estará na posição 1 da `PartidaLocal`, e os bots nas posições 2, 3 e 4; estas posições são exibidas como descrito em [Terminologia](#terminologia): a 1 na parte de baixo da tela, a 2 na direita, a 3 acima e a 4 à esquerda. -### Jogo via Bluetooth +Essa separação radical de classes simplifica os jogadores (`JogadorHumano` não precisa entender as regras do jogo, `JogadorBot` só se preocupa em jogar), evita trapaças (`PartidaLocal` é a única autoridade) e permite total reuso no multiplayer, como veremos a seguir. + +### Jogo Multiplayer + +#### Diferenças conceituais em relação ao single-player + +É importante observar que o jogo multiplayer introduz algumas complexidades para entender a motivação da arquitetura de classes mais complexa: + +O jogo multiplayer pode acontecer via Bluetooth ou pela internet. Do ponto de vista do usuário, os jogadores entram em uma "sala de jogo". Um deles (o "gerente") pode mudar os outros de lugar e é quem pode iniciar uma nova partida (tanto a partir da sala de jogo, quanto no botão "Nova Partida" que aparece quando uma se encerra). + +Todos os aparelhos enxergam a mesa do seu ponto de vista, ou seja, se temos os jogadores A, B, C e D, com B à direita de A, C à direita de B e D à direita de C, o jogador A vê a mesa assim: + +``` + C + +D B + + A +``` + +mas o jogador B vê assim: + +``` + D + +A C -Para jogar via Bluetooth, um aparelho seleciona a opção "Criar Jogo", que abre uma `ServidorBluetoothActivity`. Esta aguarda por conexões de outros aparelhos, e quando um se conecta, ela cria um `JogadorBluetooth`. + B +``` -`JogadorBluetooth` recebe notificações da `PartidaLocal` da mesma forma que `JogadorHumano`, mas em vez de traduzir para a UI, ela traduz em comandos textuais1, que são enviados ao outro aparelho via Bluetooth. Da mesma forma, ela recebe notificações textuais do outro aparelho e traduz em comandos para a partida. +#### Implementação +Para que todos os aparelhos e bots "enxerguem" a partida como se fosse local, foi criada a classe `PartidaRemota`. Ela atua como um _proxy_ da `PartidaLocal`, que recebe as notificações desta em formato texto e transforma em chamadas de métdodos para o `Jogador` apropriado (em particular o `JogadorHumano`, que vai reproduzir esses eventos na UI). Ela também faz o inverso: converte métodos de comando que o `JogadorHumano` chama em comandos textuais para que possam ser encaminhados para o outro lado. + +Existem, portanto, quatro formatos de jogo: single-player (que sempre roda a partida local), Bluetooth rodando a partida local ("Criar Jogo" na UI), Bluetooth rodando a partida remotamente ("Procurar jogo" na UI) e internet (que sempre roda a partida remota). Cada um desses formatos é representado por uma classe descendente de `SalaActivity`, que cria a `Partida` apropriada (local ou remota), com os `Jogador`es apropriados (vide abaixo), gerenciando a conexão com o(s) outro(s) aparelho(s) ou com o servidor de internet: + +```mermaid +classDiagram + SalaActivity <|-- TituloActivity + SalaActivity <|-- BluetoothActivity + BluetoothActivity <|-- ServidorBluetoothActivity + BluetoothActivity <|-- ClienteBluetoothActivity + SalaActivity <|-- InternetActivity + + SalaActivity : +criaNovaPartida(...) + + <> SalaActivity + <> BluetoothActivity +``` + +Pode parecer estranho que uma `Activity` faça isso, mas a UI depende muito de qual dos modos acima estamos utilizando, então há pouca vantagem em separar (talvez eu faça isso no futuro para melhorar a testabilidade). + +Todas as salas exibem os jogadores remotos e, quando aplicável, as opções do gerente, com exceção da `TituloActivity` (a "sala" do single-player, que não tem nada disso e só precisa que a partida seja criada). + +Como a criação da partida depende de a `TrucoActivity` estar rodando (e ela não sabe qual o modo de jogo atual), o [`CriadorDePartida`](../app/src/main/java/me/chester/minitruco/android/SalaActivity.java) mantém uma referência à `SalaActivity` atual, e delega para ela a criação da partida. + +### Jogo via Bluetooth + +O jogo via Bluetooth começa quando um aparelho eleciona a opção "Criar Jogo", que abre uma `ServidorBluetoothActivity`. Esta aguarda por conexões de outros aparelhos, e sempre que um deles se conecta, ela cria um `JogadorBluetooth` e repassa a conexão para ele. Os comandos textuais recebidos nesta conexão são traduzidos para comandos na `PartidaLocal`, e as notificações desta são traduzidas para comandos textuais e enviadas ao outro aparelho: ```mermaid classDiagram -direction LR PartidaLocal -- "1" JogadorHumano PartidaLocal -- "3" JogadorBluetooth JogadorBluetooth -- ServidorBluetoothActivity - note for ServidorBluetoothActivity "conversa com cliente\nvia Bluetooth" + note for ServidorBluetoothActivity "conversa com cliente\nvia socket Bluetooth" ``` -De forma análoga, o aparelho que seleciona a opção "Procurar Jogo" abre uma `ClienteBluetoothActivity`, que se conecta no aparelho servidor. Aqui quem faz a tradução de notificações e comandos para o protocolo textual é `PartidaRemota`: +Já o aparelho que seleciona a opção "Procurar Jogo" abre uma `ClienteBluetoothActivity`, que se conecta no aparelho servidor. Aqui quem faz a tradução de notificações e comandos para o protocolo textual é `PartidaRemota`, conforme descrito na seção anterior. + +Os outros jogadores (que podem ser clientes ou servidores) são representados por `JogadorDummy` (eles não precisam fazer nada, já que `JogadorHumano` é quem reproduz suas ações na UI, como acontece no single-player em relação aos `JogadorCPU`). Ficamos assim: ```mermaid classDiagram - PartidaRemota -- ClienteBluetoothActivity PartidaRemota -- "1" JogadorHumano - note for ClienteBluetoothActivity "conversa com servidor\nvia Bluetooth" + PartidaRemota -- "3" JogadorDummy + PartidaRemota -- ClienteBluetoothActivity + note for ClienteBluetoothActivity "conversa com servidor\nvia socket Bluetooth" ``` -Parece complicado, mas a grande vantagem é que nem `PartidaLocal` (no servidor), nem `JogadorHumano` (no cliente) precisam saber que estão conversando via Bluetooth, graças aos _proxies_ `JogadorBluetooth` e `PartidaRemota`. Isso permite que o mesmo código seja usado para jogar localmente ou via Bluetooth, e também permite que o jogo seja jogado via Bluetooth ou internet. +Parece complicado, mas a grande vantagem é que nem `PartidaLocal` (no servidor), nem `JogadorHumano` (no cliente) precisam saber que estão conversando via Bluetooth, graças aos _proxies_ `JogadorBluetooth` e `PartidaRemota`. *1 para detalhes sobre estes comandos e notificações textuais, veja a seção [Protocolo de comunicação multiplayer](#protocolo-de-comunicação-multiplayer).* - ### Jogo via Internet -TODO (jogo via internet ainda está em desenvolvimento) +A `ClienteInternetActivity` vai se comportar de forma parecida com a `ClienteBluetoothActivity`, isto é, usando uma `PartidaRemota` como _proxy_ da `PartidaLocal` no servidor, um `JogadorHumano` para interfacear com a UI e três `JogadorDummy` para representar os outros jogadores (bots ou outros clientes na internet). -### Sockets e Activities no jogo multiplayer +A diferença é que a conexão é feita via internet, e não via Bluetooth (para a `PartidaLocal` é tudo um [`socket`](https://docs.oracle.com/javase/8/docs/api/java/net/Socket.html)). -Todas as `ActivityMultiplayer` mencionadas acima precisam manter o(s) socket(s) nos quais elas estão conectadas, e fazer a ponte entre os `Jogador`es e estes. Para gerenciar isso, a classe [`CriadorDePartida`](../app/src/main/java/me/chester/minitruco/android/CriadorDePartida.java) mantém uma referência à `ActivityMultiplayer` em uso no momento e consolida a criação de novas partidas. +```mermaid +classDiagram + PartidaRemota -- "1" JogadorHumano + PartidaRemota -- "3" JogadorDummy + PartidaRemota -- ClienteInternetActivity + note for ClienteInternetActivity "conversa com servidor\nvia socket internet" +``` + +O servidor internet é um módulo completamente separado, que não roda em Android. + +TODO: documentar o servidor internet ## Protocolo de comunicação multiplayer @@ -234,14 +306,21 @@ TODO colocar um exemplo de jogo aqui (GIF ou whatnot) ### Comandos -#### Fora do jogo +#### Fora da sala - `W`: recupera número de versão (e de repente outras infos no futuro) - `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 -- `S`: - Sai da sala (encerrando a partida, se houver uma em andamento) -- `Q`: - Inicia a partida (se o jogador for o gerente e não houver uma em andamento) + +#### Dentro da sala (fora de jogo) +- `S`: Sai da sala (encerrando a partida, se houver uma em andamento) + +#### Dentro da sala (fora de jogo e gerente) + +- `R R`: Reconfigura a sala rotacionando os não-gerentes (troca de parceiro) +- `R I`: Reconfigura a sala invertendo os adversários +- `Q`: ("quero jogar") - inicia a partida #### Durante o jogo @@ -251,7 +330,7 @@ TODO colocar um exemplo de jogo aqui (GIF ou whatnot) - `C`: Corre (recusa aumento de aposta) - `H _`: decide se aceita ou recusa jogar em mão de 11 (_ = T para aceita e F para recusa) -#### Outros +#### A qualquer momento - `K `: responde a uma notificação de keepalive do servidor para evitar que a conexão caia por inatividade (apenas internet) From 335b1ca57e3cec94eb52ea254f6537623e8f3c9d Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 15:47:26 -0400 Subject: [PATCH 05/14] Coloca nome do jogador no toString para facilitar debug de testes e logs --- core/src/main/java/me/chester/minitruco/core/Jogador.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/java/me/chester/minitruco/core/Jogador.java b/core/src/main/java/me/chester/minitruco/core/Jogador.java index 4d8a211d..eb0e3ec5 100644 --- a/core/src/main/java/me/chester/minitruco/core/Jogador.java +++ b/core/src/main/java/me/chester/minitruco/core/Jogador.java @@ -64,6 +64,11 @@ public String getNome() { return nome; } + @Override + public String toString() { + return super.toString()+"["+nome+"]"; + } + public void setNome(String nome) { this.nome = nome; } From 23a3f571b5db70a0b5bf44f1c426bce5245f5e15 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 15:51:11 -0400 Subject: [PATCH 06/14] =?UTF-8?q?Move=20timestamp=20para=20JogadorConectad?= =?UTF-8?q?o,=20simplificando=20v=C3=A1rios=20m=C3=A9todos;=20em=20particu?= =?UTF-8?q?lar=20corrige=20e=20testa=20trocaParceiro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (também introduz um sleep de 1ms pra garantir que o timestamp do gerente é único) --- .../minitruco/server/JogadorConectado.java | 6 + .../me/chester/minitruco/server/Sala.java | 148 ++++++-------- .../me/chester/minitruco/server/SalaTest.java | 189 +++++++++++++++--- 3 files changed, 232 insertions(+), 111 deletions(-) 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 194e8345..d524c849 100644 --- a/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java +++ b/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java @@ -8,6 +8,7 @@ import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; +import java.util.Date; import me.chester.minitruco.core.Carta; import me.chester.minitruco.core.Jogador; @@ -31,6 +32,11 @@ public class JogadorConectado extends Jogador implements Runnable { */ public boolean querJogar = false; + /** + * Timestamp da última vez que o jogador entrou na sala + */ + public Date timestampSala; + private Sala sala; /** 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 76c7b0fe..60241c62 100644 --- a/server/src/main/java/me/chester/minitruco/server/Sala.java +++ b/server/src/main/java/me/chester/minitruco/server/Sala.java @@ -3,13 +3,12 @@ /* SPDX-License-Identifier: BSD-3-Clause */ /* Copyright © 2005-2023 Carlos Duarte do Nascimento "Chester" */ +import static java.lang.Thread.sleep; import static me.chester.minitruco.core.JogadorBot.APELIDO_BOT; -import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -67,11 +66,7 @@ public static void limpaSalas() { * Jogadores presentes na sala */ private Jogador[] jogadores = new Jogador[4]; - /** - * Timestamp de entrada de cada jogador (usada para determinar o gerente) - */ - private Date[] timestamps = new Date[4]; /** * Partida que está rodando nessa sala (se houver) */ @@ -133,9 +128,15 @@ public boolean adiciona(JogadorConectado j) { // Procura um lugarzinho na sala. Se achar, adiciona for (int i = 0; i <= 3; i++) { if (jogadores[i] == null) { + // Garante timestamps diferentes (Date tem resolução de 1ms) + try { + sleep(1); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } // Link sala->jogador jogadores[i] = j; - timestamps[i] = new Date(); + j.timestampSala = new Date(); // Link jogador->sala j.setSala(this); atualizaColecoesDeSalas(); @@ -153,20 +154,17 @@ public boolean adiciona(JogadorConectado j) { * remotos */ public Jogador getGerente() { - int posGerente = -1; + JogadorConectado g = null; for (int i = 0; i <= 3; i++) { - if (jogadores[i] instanceof JogadorConectado) { - if (posGerente == -1 - || timestamps[i].before(timestamps[posGerente])) { - posGerente = i; - } + if (!(jogadores[i] instanceof JogadorConectado)) { + continue; + } + JogadorConectado j = (JogadorConectado) jogadores[i]; + if (g == null || j.timestampSala.before(g.timestampSala)) { + g = j; } } - if (posGerente != -1) { - return jogadores[posGerente]; - } else { - return null; - } + return g; } /** @@ -202,7 +200,6 @@ public boolean remove(JogadorConectado j) { } // Desfaz link sala->jogador jogadores[i] = null; - timestamps[i] = null; // Desfaz link jogador->sala j.setSala(null); j.querJogar = false; @@ -378,75 +375,58 @@ public synchronized void liberaJogo() { this.partida = null; } +// +// public void inverteAdversariosDoGerente() { +// +// // Acha o gerente +// Jogador gerente = getGerente(); +// int posGerente = 0; +// for (int i = 0; i <= 3; i++) { +// if (!gerente.equals(jogadores[i])) { +// posGerente = i; +// } +// } +// // Acha as posições dos adversários +// int posAdv1 = posGerente + 1; +// int posAdv2 = posGerente + 3; +// if (posAdv1 > 4) +// posAdv1 -= 4; +// if (posAdv2 > 4) +// posAdv2 -= 4; +// +// // Troca jogadores e timestamps +// posAdv1--; +// posAdv2--; +// +// Jogador tempJogador = jogadores[posAdv1]; +// jogadores[posAdv1] = jogadores[posAdv2]; +// jogadores[posAdv2] = tempJogador; +// +// Date tempTimestamp = timestamps[posAdv1]; +// timestamps[posAdv1] = timestamps[posAdv2]; +// timestamps[posAdv2] = tempTimestamp; +// +// } + /** - * Troca o parceiro do gerente da sala (fazendo um rodízio de todo mundo - * menos o gerente) + * Rotaciona os outros jogadores, trocando o adversário a cada chamada. + *

+ * Não faz nada se o solicitante não for o gerente. + * + * @param solicitante Jogador que solicitou a rotação */ - public void trocaParceiroDoGerente() { - - Jogador gerente = getGerente(); - - // Cria uma lista das posições a trocar, duplicando a primeira no final - List posicoes = new ArrayList<>(); - int posGerente = 0; - for (int i = 1; i <= 4; i++) { - if (!gerente.equals(this.getJogador(i))) { - posicoes.add(i); - } else { - posGerente = i; - } - } - posicoes.add(posicoes.get(0)); - - // Cria novos arrays de jogadores/timestamps, rotacionando as posições - // com base na lista acima (jogando o próximo da lista no atual) - Jogador[] novosJogadores = new Jogador[4]; - Date[] novosTimestamps = new Date[4]; - for (int i = 0; i <= 2; i++) { - novosJogadores[posicoes.get(i) - 1] = getJogador(posicoes - .get(i + 1)); - novosTimestamps[posicoes.get(i) - 1] = timestamps[posicoes - .get(i + 1) - 1]; + public void trocaParceiro(JogadorConectado solicitante) { + if (solicitante != getGerente()) { + return; } - // Complementa a lista com o gerente e troca a lista atual por essa - novosJogadores[posGerente - 1] = gerente; - novosTimestamps[posGerente - 1] = timestamps[posGerente - 1]; - jogadores = novosJogadores; - timestamps = novosTimestamps; + int i1 = getPosicao(getGerente()); + int i2 = (i1 + 1) % 4; + int i3 = (i2 + 1) % 4; + Jogador temp = jogadores[i2]; + jogadores[i2] = jogadores[i3]; + jogadores[i3] = jogadores[i1]; + jogadores[i1] = temp; } - - public void inverteAdversariosDoGerente() { - - // Acha o gerente - Jogador gerente = getGerente(); - int posGerente = 0; - for (int i = 0; i <= 3; i++) { - if (!gerente.equals(jogadores[i])) { - posGerente = i; - } - } - // Acha as posições dos adversários - int posAdv1 = posGerente + 1; - int posAdv2 = posGerente + 3; - if (posAdv1 > 4) - posAdv1 -= 4; - if (posAdv2 > 4) - posAdv2 -= 4; - - // Troca jogadores e timestamps - posAdv1--; - posAdv2--; - - Jogador tempJogador = jogadores[posAdv1]; - jogadores[posAdv1] = jogadores[posAdv2]; - jogadores[posAdv2] = tempJogador; - - Date tempTimestamp = timestamps[posAdv1]; - timestamps[posAdv1] = timestamps[posAdv2]; - timestamps[posAdv2] = tempTimestamp; - - } - } diff --git a/server/src/test/java/me/chester/minitruco/server/SalaTest.java b/server/src/test/java/me/chester/minitruco/server/SalaTest.java index 6aa31352..62f65a8f 100644 --- a/server/src/test/java/me/chester/minitruco/server/SalaTest.java +++ b/server/src/test/java/me/chester/minitruco/server/SalaTest.java @@ -1,5 +1,6 @@ package me.chester.minitruco.server; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -10,21 +11,54 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import static java.lang.Thread.sleep; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.net.Socket; +import me.chester.minitruco.core.Jogador; import me.chester.minitruco.core.Partida; class SalaTest { JogadorConectado j1, j2, j3, j4, j5, j6, j7, j8, j9, j10; + JogadorConectado jj1, jj2, jj3, jj4; + + private void assertPosicoes(Sala s, + Jogador p1, Jogador p2, Jogador p3, Jogador p4) { + Jogador[] esperados = new Jogador[] {p1, p2, p3, p4}; + Jogador[] reais = new Jogador[] { + s.getJogador(1), + s.getJogador(2), + s.getJogador(3), + s.getJogador(4) + }; + assertArrayEquals(esperados, reais); + } + + private Sala criaSalaComGerenteNaPosicao1() { + Sala sala = new Sala(true, "P"); + sala.adiciona(jj1); + sala.adiciona(jj2); + sala.adiciona(jj3); + sala.adiciona(jj4); + assertPosicoes(sala, jj1, jj2, jj3, jj4); + assertEquals(jj1, sala.getGerente()); + return sala; + } + + private Sala criaSalaComGerenteNaPosicao2() { + Sala sala = criaSalaComGerenteNaPosicao1(); + sala.remove(jj1); + sala.adiciona(jj1); + assertPosicoes(sala, jj1, jj2, jj3, jj4); + assertEquals(jj2, sala.getGerente()); + return sala; + } @BeforeEach - void setUp() { + void setUp() throws InterruptedException { Sala.limpaSalas(); j1 = spy(new JogadorConectado(mock(Socket.class))); @@ -37,12 +71,62 @@ void setUp() { j8 = new JogadorConectado(mock(Socket.class)); j9 = new JogadorConectado(mock(Socket.class)); j10 = new JogadorConectado(mock(Socket.class)); + j1.setNome("j1"); + j2.setNome("j2"); + j3.setNome("j3"); + j4.setNome("j4"); // Mocks para jogadores que podem iniciar/encerrar partida doNothing().when(j1).println(any()); -// doNothing().when(j2).println(any()); -// doNothing().when(j3).println(any()); -// doNothing().when(j4).println(any()); + + jj1 = new JogadorConectado(null); + jj2 = new JogadorConectado(null); + jj3 = new JogadorConectado(null); + jj4 = new JogadorConectado(null); + jj1.setNome("jj1"); + jj2.setNome("jj2"); + jj3.setNome("jj3"); + jj4.setNome("jj4"); + } + + // Isso é importante para não haver ambiguidade sobre + // quem é o gerente da sala + @Test + void testAdicionaUsaTimestampsSequenciais() { + Sala s = new Sala(true, "P"); + s.adiciona(j1); + s.adiciona(j2); + s.adiciona(j3); + s.adiciona(j4); + assertTrue(j1.timestampSala.before(j2.timestampSala)); + assertTrue(j2.timestampSala.before(j3.timestampSala)); + assertTrue(j3.timestampSala.before(j4.timestampSala)); + } + + @Test + void testGerenteÉSempreOUsuarioMaisAntigoNaSala() { + Sala s = new Sala(true, "P"); + assertNull(s.getGerente()); + s.adiciona(j1); + assertEquals(j1, s.getGerente()); + s.adiciona(j2); + assertEquals(j1, s.getGerente()); + s.adiciona(j3); + assertEquals(j1, s.getGerente()); + s.adiciona(j4); + assertEquals(j1, s.getGerente()); + s.remove(j1); + assertEquals(j2, s.getGerente()); + s.remove(j3); + assertEquals(j2, s.getGerente()); + s.adiciona(j3); + assertEquals(j2, s.getGerente()); + s.remove(j2); + assertEquals(j4, s.getGerente()); + s.remove(j4); + assertEquals(j3, s.getGerente()); + s.remove(j3); + assertNull(s.getGerente()); } @Test @@ -122,28 +206,6 @@ void testGetInfo() { assertEquals("I bot|paul|george|bot $POSICAO P TTFT 2", s.getInfo()); } - @Test - void testGerente() throws InterruptedException { - // Os sleeps garantem timestamps diferentes para fins de teste - // (precisão de java.util.Date é de 1ms); IRL, se houver um - // empate (bem improvável), o que a classe decidir está bom. - Sala s = new Sala(true, "P"); - s.adiciona(j1); sleep(1); - assertEquals(j1, s.getGerente()); - s.adiciona(j2); sleep(1); - s.adiciona(j3); sleep(1); - assertEquals(j1, s.getGerente()); - s.remove(j1); sleep(1); - assertEquals(j2, s.getGerente()); - s.adiciona(j1); sleep(1); - assertEquals(j2, s.getGerente()); - s.remove(j2); sleep(1); - assertEquals(j3, s.getGerente()); - s.remove(j1); sleep(1); - s.remove(j3); sleep(1); - assertNull(s.getGerente()); - } - @Test void testIniciaPartida() { Sala s = new Sala(true, "P"); @@ -166,4 +228,77 @@ void testIniciaPartida() { s.iniciaPartida(j1); assertEquals(p, s.getPartida()); } + + private Sala criaSalaCheiaComJ2Gerente() { + Sala s = new Sala(true, "P"); + s.adiciona(j1); + s.adiciona(j2); + s.adiciona(j3); + s.adiciona(j4); + s.remove(j1); + s.adiciona(j1); + assertPosicoes(s, j1, j2, j3, j4); + assertEquals(j2, s.getGerente()); + + return s; + } + + private Sala criaSalaCheiaComJ3Gerente() { + Sala s = criaSalaCheiaComJ2Gerente(); + s.remove(j2); + s.adiciona(j2); + assertPosicoes(s, j1, j2, j3, j4); + assertEquals(j3, s.getGerente()); + + return s; + } + + @Test + void testTrocaParceiroIgnoraNaoGerente() { + Sala s = criaSalaComGerenteNaPosicao2(); + assertPosicoes(s, jj1, jj2, jj3, jj4); + s.trocaParceiro(jj1); + assertPosicoes(s, jj1, jj2, jj3, jj4); + } + + @Test + void testTrocaParceiroSimples() { + Sala s = criaSalaComGerenteNaPosicao1(); + assertPosicoes(s, jj1, jj2, jj3, jj4); + s.trocaParceiro(jj1); + assertPosicoes(s, jj1, jj3, jj4, jj2); + } + + @Test + void testTrocaParceiroNaoAlteraQuemÉOGerente() { + Sala s = criaSalaComGerenteNaPosicao2(); + assertEquals(jj2, s.getGerente()); + s.trocaParceiro(jj2); + assertEquals(jj2, s.getGerente()); + } + + @Test + void testTrocaParceiroComGerenteNaPosicao2() { + Sala s = criaSalaComGerenteNaPosicao2(); + assertPosicoes(s, jj1, jj2, jj3, jj4); + s.trocaParceiro(jj2); + assertPosicoes(s, jj3, jj2, jj4, jj1); + s.trocaParceiro(jj2); + assertPosicoes(s, jj4, jj2, jj1, jj3); + s.trocaParceiro(jj2); + assertPosicoes(s, jj1, jj2, jj3, jj4); + } + + @Test + void testTrocaParceiroEmSalaComVaga() { + Sala s = criaSalaComGerenteNaPosicao2(); + s.remove(jj3); + assertPosicoes(s, jj1, jj2, null, jj4); + s.trocaParceiro(jj2); + assertPosicoes(s, null, jj2, jj4, jj1); + s.trocaParceiro(jj2); + assertPosicoes(s, jj4, jj2, jj1, null); + s.trocaParceiro(jj2); + assertPosicoes(s, jj1, jj2, null, jj4); + } } From 2cabbc047a0ac286a1b1c0d7a72e0154ce4d7ada Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 15:53:08 -0400 Subject: [PATCH 07/14] =?UTF-8?q?Remove=20c=C3=B3digo=20morto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../me/chester/minitruco/server/SalaTest.java | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/server/src/test/java/me/chester/minitruco/server/SalaTest.java b/server/src/test/java/me/chester/minitruco/server/SalaTest.java index 62f65a8f..29089207 100644 --- a/server/src/test/java/me/chester/minitruco/server/SalaTest.java +++ b/server/src/test/java/me/chester/minitruco/server/SalaTest.java @@ -58,7 +58,7 @@ private Sala criaSalaComGerenteNaPosicao2() { } @BeforeEach - void setUp() throws InterruptedException { + void setUp() { Sala.limpaSalas(); j1 = spy(new JogadorConectado(mock(Socket.class))); @@ -229,30 +229,6 @@ void testIniciaPartida() { assertEquals(p, s.getPartida()); } - private Sala criaSalaCheiaComJ2Gerente() { - Sala s = new Sala(true, "P"); - s.adiciona(j1); - s.adiciona(j2); - s.adiciona(j3); - s.adiciona(j4); - s.remove(j1); - s.adiciona(j1); - assertPosicoes(s, j1, j2, j3, j4); - assertEquals(j2, s.getGerente()); - - return s; - } - - private Sala criaSalaCheiaComJ3Gerente() { - Sala s = criaSalaCheiaComJ2Gerente(); - s.remove(j2); - s.adiciona(j2); - assertPosicoes(s, j1, j2, j3, j4); - assertEquals(j3, s.getGerente()); - - return s; - } - @Test void testTrocaParceiroIgnoraNaoGerente() { Sala s = criaSalaComGerenteNaPosicao2(); From 547b141f3e16d8efca1e5cd598dd1eb37e2c2c46 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 16:01:40 -0400 Subject: [PATCH 08/14] Implementa inverteAdversarios --- .../me/chester/minitruco/server/Sala.java | 53 +++++++------------ .../me/chester/minitruco/server/SalaTest.java | 38 ++++++++++--- 2 files changed, 52 insertions(+), 39 deletions(-) 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 60241c62..46421752 100644 --- a/server/src/main/java/me/chester/minitruco/server/Sala.java +++ b/server/src/main/java/me/chester/minitruco/server/Sala.java @@ -375,39 +375,6 @@ public synchronized void liberaJogo() { this.partida = null; } -// -// public void inverteAdversariosDoGerente() { -// -// // Acha o gerente -// Jogador gerente = getGerente(); -// int posGerente = 0; -// for (int i = 0; i <= 3; i++) { -// if (!gerente.equals(jogadores[i])) { -// posGerente = i; -// } -// } -// // Acha as posições dos adversários -// int posAdv1 = posGerente + 1; -// int posAdv2 = posGerente + 3; -// if (posAdv1 > 4) -// posAdv1 -= 4; -// if (posAdv2 > 4) -// posAdv2 -= 4; -// -// // Troca jogadores e timestamps -// posAdv1--; -// posAdv2--; -// -// Jogador tempJogador = jogadores[posAdv1]; -// jogadores[posAdv1] = jogadores[posAdv2]; -// jogadores[posAdv2] = tempJogador; -// -// Date tempTimestamp = timestamps[posAdv1]; -// timestamps[posAdv1] = timestamps[posAdv2]; -// timestamps[posAdv2] = tempTimestamp; -// -// } - /** * Rotaciona os outros jogadores, trocando o adversário a cada chamada. *

@@ -429,4 +396,24 @@ public void trocaParceiro(JogadorConectado solicitante) { jogadores[i3] = jogadores[i1]; jogadores[i1] = temp; } + + /** + * Inverte a dupla adversária. + *

+ * Não faz nada se o solicitante não for o gerente. + * + * @param solicitante Jogador que solicitou a inversão + */ + public void inverteAdversarios(JogadorConectado solicitante) { + if (solicitante != getGerente()) { + return; + } + + int i1 = getPosicao(getGerente()); + int i2 = (i1 + 2) % 4; + + Jogador temp = jogadores[i1]; + jogadores[i1] = jogadores[i2]; + jogadores[i2] = temp; + } } diff --git a/server/src/test/java/me/chester/minitruco/server/SalaTest.java b/server/src/test/java/me/chester/minitruco/server/SalaTest.java index 29089207..2d45e398 100644 --- a/server/src/test/java/me/chester/minitruco/server/SalaTest.java +++ b/server/src/test/java/me/chester/minitruco/server/SalaTest.java @@ -230,19 +230,19 @@ void testIniciaPartida() { } @Test - void testTrocaParceiroIgnoraNaoGerente() { - Sala s = criaSalaComGerenteNaPosicao2(); + void testTrocaParceiroSimples() { + Sala s = criaSalaComGerenteNaPosicao1(); assertPosicoes(s, jj1, jj2, jj3, jj4); s.trocaParceiro(jj1); - assertPosicoes(s, jj1, jj2, jj3, jj4); + assertPosicoes(s, jj1, jj3, jj4, jj2); } @Test - void testTrocaParceiroSimples() { - Sala s = criaSalaComGerenteNaPosicao1(); + void testTrocaParceiroIgnoraNaoGerente() { + Sala s = criaSalaComGerenteNaPosicao2(); assertPosicoes(s, jj1, jj2, jj3, jj4); s.trocaParceiro(jj1); - assertPosicoes(s, jj1, jj3, jj4, jj2); + assertPosicoes(s, jj1, jj2, jj3, jj4); } @Test @@ -277,4 +277,30 @@ void testTrocaParceiroEmSalaComVaga() { s.trocaParceiro(jj2); assertPosicoes(s, jj1, jj2, null, jj4); } + + @Test + void testInverteAdversariosSimples() { + Sala s = criaSalaComGerenteNaPosicao1(); + assertPosicoes(s, jj1, jj2, jj3, jj4); + s.inverteAdversarios(jj1); + assertPosicoes(s, jj1, jj4, jj3, jj2); + } + + @Test + void testInverteAdversariosIgnoraNaoGerente() { + Sala s = criaSalaComGerenteNaPosicao2(); + assertPosicoes(s, jj1, jj2, jj3, jj4); + s.inverteAdversarios(jj1); + assertPosicoes(s, jj1, jj2, jj3, jj4); + } + + @Test + void testInverteAdversariosComGerenteNaPosicao2() { + Sala s = criaSalaComGerenteNaPosicao2(); + assertPosicoes(s, jj1, jj2, jj3, jj4); + s.inverteAdversarios(jj2); + assertPosicoes(s, jj3, jj2, jj1, jj4); + s.inverteAdversarios(jj2); + assertPosicoes(s, jj1, jj2, jj3, jj4); + } } From f642a4f62694dd2f1d161569832fa3ad3e4fbb85 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 17:40:42 -0400 Subject: [PATCH 09/14] retorna booleano no trocaParceiro/inverteAdversarios (para o comando decidir se vai notificar os membros da sala) --- .../java/me/chester/minitruco/server/Sala.java | 14 ++++++++++---- .../me/chester/minitruco/server/SalaTest.java | 16 ++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) 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 46421752..23695f45 100644 --- a/server/src/main/java/me/chester/minitruco/server/Sala.java +++ b/server/src/main/java/me/chester/minitruco/server/Sala.java @@ -381,10 +381,11 @@ public synchronized void liberaJogo() { * Não faz nada se o solicitante não for o gerente. * * @param solicitante Jogador que solicitou a rotação + * @return true se rotacionou, false se não (porque o solicitante não é o gerente) */ - public void trocaParceiro(JogadorConectado solicitante) { + public boolean trocaParceiro(JogadorConectado solicitante) { if (solicitante != getGerente()) { - return; + return false; } int i1 = getPosicao(getGerente()); @@ -395,6 +396,8 @@ public void trocaParceiro(JogadorConectado solicitante) { jogadores[i2] = jogadores[i3]; jogadores[i3] = jogadores[i1]; jogadores[i1] = temp; + + return true; } /** @@ -403,10 +406,11 @@ public void trocaParceiro(JogadorConectado solicitante) { * Não faz nada se o solicitante não for o gerente. * * @param solicitante Jogador que solicitou a inversão + * @return true se inverteu, false se não (porque o solicitante não é o gerente) */ - public void inverteAdversarios(JogadorConectado solicitante) { + public boolean inverteAdversarios(JogadorConectado solicitante) { if (solicitante != getGerente()) { - return; + return false; } int i1 = getPosicao(getGerente()); @@ -415,5 +419,7 @@ public void inverteAdversarios(JogadorConectado solicitante) { Jogador temp = jogadores[i1]; jogadores[i1] = jogadores[i2]; jogadores[i2] = temp; + + return true; } } diff --git a/server/src/test/java/me/chester/minitruco/server/SalaTest.java b/server/src/test/java/me/chester/minitruco/server/SalaTest.java index 2d45e398..234221c5 100644 --- a/server/src/test/java/me/chester/minitruco/server/SalaTest.java +++ b/server/src/test/java/me/chester/minitruco/server/SalaTest.java @@ -230,18 +230,18 @@ void testIniciaPartida() { } @Test - void testTrocaParceiroSimples() { + void testTrocaParceiroSimplesRetornaTrueETroca() { Sala s = criaSalaComGerenteNaPosicao1(); assertPosicoes(s, jj1, jj2, jj3, jj4); - s.trocaParceiro(jj1); + assertTrue(s.trocaParceiro(jj1)); assertPosicoes(s, jj1, jj3, jj4, jj2); } @Test - void testTrocaParceiroIgnoraNaoGerente() { + void testTrocaParceiroRetornaFalsoEIgnoraSeNaoForGerente() { Sala s = criaSalaComGerenteNaPosicao2(); assertPosicoes(s, jj1, jj2, jj3, jj4); - s.trocaParceiro(jj1); + assertFalse(s.trocaParceiro(jj1)); assertPosicoes(s, jj1, jj2, jj3, jj4); } @@ -279,18 +279,18 @@ void testTrocaParceiroEmSalaComVaga() { } @Test - void testInverteAdversariosSimples() { + void testInverteAdversariosSimplesRetornaTrueEInverte() { Sala s = criaSalaComGerenteNaPosicao1(); assertPosicoes(s, jj1, jj2, jj3, jj4); - s.inverteAdversarios(jj1); + assertTrue(s.inverteAdversarios(jj1)); assertPosicoes(s, jj1, jj4, jj3, jj2); } @Test - void testInverteAdversariosIgnoraNaoGerente() { + void testInverteAdversariosIgnoraNaoGerenteERetornaFalse() { Sala s = criaSalaComGerenteNaPosicao2(); assertPosicoes(s, jj1, jj2, jj3, jj4); - s.inverteAdversarios(jj1); + assertFalse(s.inverteAdversarios(jj1)); assertPosicoes(s, jj1, jj2, jj3, jj4); } From c927d08dbe6091dbabdd8f6902173e6f6aaf08e3 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 17:47:53 -0400 Subject: [PATCH 10/14] =?UTF-8?q?Implementa=20comandos=20R=20T=20e=20R=20I?= =?UTF-8?q?=20no=20servidor=20(troca=20parceiro=20e=20inverte=20advers?= =?UTF-8?q?=C3=A1rios)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/desenvolvimento.md | 2 +- .../me/chester/minitruco/server/ComandoR.java | 30 ++++++++ .../minitruco/server/ComandoRTest.java | 69 +++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/me/chester/minitruco/server/ComandoR.java create mode 100644 server/src/test/java/me/chester/minitruco/server/ComandoRTest.java diff --git a/docs/desenvolvimento.md b/docs/desenvolvimento.md index 34798702..8c84e3af 100644 --- a/docs/desenvolvimento.md +++ b/docs/desenvolvimento.md @@ -318,7 +318,7 @@ TODO colocar um exemplo de jogo aqui (GIF ou whatnot) #### Dentro da sala (fora de jogo e gerente) -- `R R`: Reconfigura a sala rotacionando os não-gerentes (troca de parceiro) +- `R T`: Reconfigura a sala rotacionando os não-gerentes (troca de parceiro) - `R I`: Reconfigura a sala invertendo os adversários - `Q`: ("quero jogar") - inicia a partida diff --git a/server/src/main/java/me/chester/minitruco/server/ComandoR.java b/server/src/main/java/me/chester/minitruco/server/ComandoR.java new file mode 100644 index 00000000..4ecbe271 --- /dev/null +++ b/server/src/main/java/me/chester/minitruco/server/ComandoR.java @@ -0,0 +1,30 @@ +package me.chester.minitruco.server; + +/* SPDX-License-Identifier: BSD-3-Clause */ +/* Copyright © 2005-2023 Carlos Duarte do Nascimento "Chester" */ + +/** + * Informa ao servidor que o jogador abandonou a partida. + */ +public class ComandoR extends Comando { + + @Override + public void executa(String[] args, JogadorConectado j) { + Sala sala = j.getSala(); + if (sala == null || args.length != 2) { + return; + } + switch (args[1]) { + case "T": + if (sala.trocaParceiro(j)) { + sala.mandaInfoParaTodos(); + } + break; + case "I": + if (sala.inverteAdversarios(j)) { + sala.mandaInfoParaTodos(); + } + break; + } + } +} diff --git a/server/src/test/java/me/chester/minitruco/server/ComandoRTest.java b/server/src/test/java/me/chester/minitruco/server/ComandoRTest.java new file mode 100644 index 00000000..2378eed2 --- /dev/null +++ b/server/src/test/java/me/chester/minitruco/server/ComandoRTest.java @@ -0,0 +1,69 @@ +package me.chester.minitruco.server; + +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.verify; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class ComandoRTest { + + private static Sala sala; + private static JogadorConectado j1, j2, j3; + + @BeforeEach + void setUp() { + sala = new Sala(true, "P"); + j1 = spy(new JogadorConectado(null)); + j2 = spy(new JogadorConectado(null)); + j3 = spy(new JogadorConectado(null)); + j1.setNome("j1"); + j2.setNome("j2"); + j3.setNome("j3"); + sala.adiciona(j1); + sala.adiciona(j2); + sala.adiciona(j3); + Mockito.reset(j1,j2, j3); + doNothing().when(j1).println(any()); + doNothing().when(j2).println(any()); + doNothing().when(j3).println(any()); + } + + @Test + void testTrocaParceirosDoGerente() { + Comando.interpreta("R T", j1); + verify(j1).println("I j1|j3|bot|j2 1 P FFTF 1"); + verify(j2).println("I j1|j3|bot|j2 4 P FFTF 1"); + verify(j3).println("I j1|j3|bot|j2 2 P FFTF 1"); + } + + @Test + void testInverteAdversariosDoGerente() { + Comando.interpreta("R I", j1); + verify(j1).println("I j1|bot|j3|j2 1 P FTFF 1"); + verify(j2).println("I j1|bot|j3|j2 4 P FTFF 1"); + verify(j3).println("I j1|bot|j3|j2 3 P FTFF 1"); + } + + @Test + void testIgnoraOutrosUsuarios() { + Comando.interpreta("R T", j2); + Comando.interpreta("R I", j2); + verify(j1, never()).println(any()); + } + + @Test + void testIgnoraArgumentosInvalidos() { + Comando.interpreta("R", j1); + Comando.interpreta("R ", j1); + Comando.interpreta("R X", j1); + Comando.interpreta("R R R", j1); + Comando.interpreta("R WTF", j1); + verify(j1, never()).println(any()); + } + +} From 1d1c325d4334b295b7f842724c430b8da26cc0b7 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 17:49:26 -0400 Subject: [PATCH 11/14] =?UTF-8?q?n=C3=A3o=20=C3=A9=20preciso=20liberar=20a?= =?UTF-8?q?=20sala=20(a=20classe=20vai=20fazer=20isso)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/me/chester/minitruco/server/ComandoS.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/server/src/main/java/me/chester/minitruco/server/ComandoS.java b/server/src/main/java/me/chester/minitruco/server/ComandoS.java index ca526bc1..3905503c 100644 --- a/server/src/main/java/me/chester/minitruco/server/ComandoS.java +++ b/server/src/main/java/me/chester/minitruco/server/ComandoS.java @@ -17,12 +17,6 @@ public void executa(String[] args, JogadorConectado j) { if (s != null) { s.remove(j); j.println("S"); - // TODO: ao esvaziar uma sala completamente, liberar ela? -// if (s.getNumPessoas() == 0) { -// // Se esvaziou a sala, volta as regras para o default -// s.baralhoLimpo = false; -// s.manilhaVelha = false; -// } s.mandaInfoParaTodos(); } else { j.println("X FS"); From 9c814512678f9d15f51526d8d6e415545942a580 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 18:07:40 -0400 Subject: [PATCH 12/14] =?UTF-8?q?Garante=20que=20`R=20T`=20e=20`R=20I`=20n?= =?UTF-8?q?=C3=A3o=20fazem=20nada=20fora=20da=20sala=20ou=20com=20jogo=20e?= =?UTF-8?q?m=20andamento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../me/chester/minitruco/server/Sala.java | 14 +++-- .../minitruco/server/ComandoRTest.java | 25 +++++++++ .../me/chester/minitruco/server/SalaTest.java | 51 ++++++++++++++----- 3 files changed, 70 insertions(+), 20 deletions(-) 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 23695f45..0f70d109 100644 --- a/server/src/main/java/me/chester/minitruco/server/Sala.java +++ b/server/src/main/java/me/chester/minitruco/server/Sala.java @@ -377,14 +377,13 @@ public synchronized void liberaJogo() { /** * Rotaciona os outros jogadores, trocando o adversário a cada chamada. - *

- * Não faz nada se o solicitante não for o gerente. * * @param solicitante Jogador que solicitou a rotação - * @return true se rotacionou, false se não (porque o solicitante não é o gerente) + * @return true se rotacionou, false se o solicitante não for o gerente, ou + * houver jogo em andamento */ public boolean trocaParceiro(JogadorConectado solicitante) { - if (solicitante != getGerente()) { + if (solicitante != getGerente() || getPartida() != null) { return false; } @@ -402,14 +401,13 @@ public boolean trocaParceiro(JogadorConectado solicitante) { /** * Inverte a dupla adversária. - *

- * Não faz nada se o solicitante não for o gerente. * * @param solicitante Jogador que solicitou a inversão - * @return true se inverteu, false se não (porque o solicitante não é o gerente) + * @return true se inverteu, false se o solicitante não for o gerente, ou + * houver jogo em andamento */ public boolean inverteAdversarios(JogadorConectado solicitante) { - if (solicitante != getGerente()) { + if (solicitante != getGerente() || getPartida() != null) { return false; } diff --git a/server/src/test/java/me/chester/minitruco/server/ComandoRTest.java b/server/src/test/java/me/chester/minitruco/server/ComandoRTest.java index 2378eed2..38b96f08 100644 --- a/server/src/test/java/me/chester/minitruco/server/ComandoRTest.java +++ b/server/src/test/java/me/chester/minitruco/server/ComandoRTest.java @@ -1,6 +1,7 @@ package me.chester.minitruco.server; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -54,6 +55,28 @@ void testIgnoraOutrosUsuarios() { Comando.interpreta("R T", j2); Comando.interpreta("R I", j2); verify(j1, never()).println(any()); + verify(j2, never()).println(any()); + verify(j3, never()).println(any()); + } + + @Test + void testIgnoraSeNaoEstaEmUmaSala() { + sala.remove(j1); + Comando.interpreta("R T", j1); + Comando.interpreta("R I", j1); + verify(j1, never()).println(any()); + verify(j2, never()).println(any()); + verify(j3, never()).println(any()); + } + + @Test + void testIgnoraSeJogoEmAndamento() { + sala.iniciaPartida(j1); + Comando.interpreta("R T", j1); + Comando.interpreta("R I", j1); + verify(j1, never()).println(startsWith("I ")); + verify(j2, never()).println(startsWith("I ")); + verify(j3, never()).println(startsWith("I ")); } @Test @@ -64,6 +87,8 @@ void testIgnoraArgumentosInvalidos() { Comando.interpreta("R R R", j1); Comando.interpreta("R WTF", j1); verify(j1, never()).println(any()); + verify(j2, never()).println(any()); + verify(j3, never()).println(any()); } } diff --git a/server/src/test/java/me/chester/minitruco/server/SalaTest.java b/server/src/test/java/me/chester/minitruco/server/SalaTest.java index 234221c5..acb1b949 100644 --- a/server/src/test/java/me/chester/minitruco/server/SalaTest.java +++ b/server/src/test/java/me/chester/minitruco/server/SalaTest.java @@ -62,9 +62,9 @@ void setUp() { Sala.limpaSalas(); j1 = spy(new JogadorConectado(mock(Socket.class))); - j2 = new JogadorConectado(mock(Socket.class)); - j3 = new JogadorConectado(mock(Socket.class)); - j4 = new JogadorConectado(mock(Socket.class)); + j2 = spy(new JogadorConectado(mock(Socket.class))); + j3 = spy(new JogadorConectado(mock(Socket.class))); + j4 = spy(new JogadorConectado(mock(Socket.class))); j5 = new JogadorConectado(mock(Socket.class)); j6 = new JogadorConectado(mock(Socket.class)); j7 = new JogadorConectado(mock(Socket.class)); @@ -76,17 +76,24 @@ void setUp() { j3.setNome("j3"); j4.setNome("j4"); - // Mocks para jogadores que podem iniciar/encerrar partida - doNothing().when(j1).println(any()); - - jj1 = new JogadorConectado(null); - jj2 = new JogadorConectado(null); - jj3 = new JogadorConectado(null); - jj4 = new JogadorConectado(null); + jj1 = spy(new JogadorConectado(null)); + jj2 = spy(new JogadorConectado(null)); + jj3 = spy(new JogadorConectado(null)); + jj4 = spy(new JogadorConectado(null)); jj1.setNome("jj1"); jj2.setNome("jj2"); jj3.setNome("jj3"); jj4.setNome("jj4"); + + // Mocks para jogadores que podem estar em partida iniciada/encerada + doNothing().when(j1).println(any()); + doNothing().when(j2).println(any()); + doNothing().when(j3).println(any()); + doNothing().when(j4).println(any()); + doNothing().when(jj1).println(any()); + doNothing().when(jj2).println(any()); + doNothing().when(jj3).println(any()); + doNothing().when(jj4).println(any()); } // Isso é importante para não haver ambiguidade sobre @@ -238,13 +245,22 @@ void testTrocaParceiroSimplesRetornaTrueETroca() { } @Test - void testTrocaParceiroRetornaFalsoEIgnoraSeNaoForGerente() { + void testTrocaParceiroRetornaFalseEIgnoraSeNaoForGerente() { Sala s = criaSalaComGerenteNaPosicao2(); assertPosicoes(s, jj1, jj2, jj3, jj4); assertFalse(s.trocaParceiro(jj1)); assertPosicoes(s, jj1, jj2, jj3, jj4); } + @Test + void testTrocaParceiroRetornaFalseEIgnoraSeEstiverJogando() { + Sala s = criaSalaComGerenteNaPosicao1(); + s.iniciaPartida(jj1); + assertPosicoes(s, jj1, jj2, jj3, jj4); + assertFalse(s.trocaParceiro(jj1)); + assertPosicoes(s, jj1, jj2, jj3, jj4); + } + @Test void testTrocaParceiroNaoAlteraQuemÉOGerente() { Sala s = criaSalaComGerenteNaPosicao2(); @@ -287,13 +303,24 @@ void testInverteAdversariosSimplesRetornaTrueEInverte() { } @Test - void testInverteAdversariosIgnoraNaoGerenteERetornaFalse() { + void testInverteAdversariosRetornaFalseEIgnoraSeNaoForGerente() { Sala s = criaSalaComGerenteNaPosicao2(); assertPosicoes(s, jj1, jj2, jj3, jj4); assertFalse(s.inverteAdversarios(jj1)); assertPosicoes(s, jj1, jj2, jj3, jj4); } + @Test + void testInverteAdversariosRetornaFalseEIgnoraSeEstiverJogando() { + Sala s = criaSalaComGerenteNaPosicao1(); + s.iniciaPartida(jj1); + assertPosicoes(s, jj1, jj2, jj3, jj4); + assertFalse(s.inverteAdversarios(jj1)); + assertPosicoes(s, jj1, jj2, jj3, jj4); + } + + + @Test void testInverteAdversariosComGerenteNaPosicao2() { Sala s = criaSalaComGerenteNaPosicao2(); From 78e67a8701b90fb01d6c5d31dbaa6846a8409f79 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 18:39:52 -0400 Subject: [PATCH 13/14] =?UTF-8?q?Evita=20inconsist=C3=AAncias=20sincroniza?= =?UTF-8?q?ndo=20todos=20os=20m=C3=A9todos=20de=20uma=20inst=C3=A2ncia=20d?= =?UTF-8?q?e=20Sala?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../me/chester/minitruco/server/Sala.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) 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 0f70d109..57deb77a 100644 --- a/server/src/main/java/me/chester/minitruco/server/Sala.java +++ b/server/src/main/java/me/chester/minitruco/server/Sala.java @@ -120,7 +120,7 @@ public static synchronized boolean colocaEmSalaPrivada(JogadorConectado j, Strin * @return true se tudo correr bem, false se a sala estiver lotada ou o * jogador já estiver em outra sala */ - public boolean adiciona(JogadorConectado j) { + public synchronized boolean adiciona(JogadorConectado j) { // Se o jogador já está numa sala, não permite if (j.getSala() != null) { return false; @@ -153,7 +153,7 @@ public boolean adiciona(JogadorConectado j) { * @return Jogador mais antigo, ou null se a sala não tiver jogadores * remotos */ - public Jogador getGerente() { + public synchronized Jogador getGerente() { JogadorConectado g = null; for (int i = 0; i <= 3; i++) { if (!(jogadores[i] instanceof JogadorConectado)) { @@ -172,7 +172,7 @@ public Jogador getGerente() { * * @return Número de Pessoas */ - public int getNumPessoas() { + public synchronized int getNumPessoas() { int numPessoas = 0; for (int i = 0; i <= 3; i++) { if (jogadores[i] != null) { @@ -190,7 +190,7 @@ public int getNumPessoas() { * @param j Jogador a remover * @return true se removeu, false se ele não estava lá */ - public boolean remove(JogadorConectado j) { + public synchronized boolean remove(JogadorConectado j) { for (int i = 0; i <= 3; i++) { if (jogadores[i] == j) { // Finaliza partida em andamento, se houver. @@ -239,7 +239,7 @@ public Partida getPartida() { /** * Manda a notificação de informação da sala ("I ...") para todos os membros. */ - public void mandaInfoParaTodos() { + public synchronized void mandaInfoParaTodos() { String mensagem = getInfo(); for (int i = 0; i <= 3; i++) { if (jogadores[i] instanceof JogadorConectado) { @@ -249,6 +249,7 @@ public void mandaInfoParaTodos() { } } + // TODO só é public para testes; reescrever eles pra testar mandaInfoParaTodos ao inves deste /** * Monta a string de informação da sala. *

@@ -306,7 +307,7 @@ public String getInfo() { * * @param solicitante Jogador que solicitou o início da partida. */ - public void iniciaPartida(Jogador solicitante) { + public synchronized void iniciaPartida(Jogador solicitante) { if (partida != null) { return; } @@ -340,7 +341,7 @@ public void iniciaPartida(Jogador solicitante) { * @param j Jogador consultado * @return posição de 1 a 4, ou 0 se o jogador não está na sala */ - public int getPosicao(Jogador j) { + public synchronized int getPosicao(Jogador j) { for (int i = 0; i <= 3; i++) { if (jogadores[i] == j) { return i + 1; @@ -356,7 +357,7 @@ public int getPosicao(Jogador j) { * @return objeto que representa o jogador, ou null se a posição for * inválida ou não estiver ocupada */ - public Jogador getJogador(int i) { + public synchronized Jogador getJogador(int i) { if (i >= 1 && i <= 4) return jogadores[i - 1]; else @@ -382,7 +383,7 @@ public synchronized void liberaJogo() { * @return true se rotacionou, false se o solicitante não for o gerente, ou * houver jogo em andamento */ - public boolean trocaParceiro(JogadorConectado solicitante) { + public synchronized boolean trocaParceiro(JogadorConectado solicitante) { if (solicitante != getGerente() || getPartida() != null) { return false; } @@ -406,7 +407,7 @@ public boolean trocaParceiro(JogadorConectado solicitante) { * @return true se inverteu, false se o solicitante não for o gerente, ou * houver jogo em andamento */ - public boolean inverteAdversarios(JogadorConectado solicitante) { + public synchronized boolean inverteAdversarios(JogadorConectado solicitante) { if (solicitante != getGerente() || getPartida() != null) { return false; } From 6cb04d88e606a67207ee47e2825576847fcb7924 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Fri, 21 Jul 2023 18:40:32 -0400 Subject: [PATCH 14/14] Chama o comando `R` (para trocar/inverter) na ClienteInternetActivity --- .../multiplayer/internet/ClienteInternetActivity.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/chester/minitruco/android/multiplayer/internet/ClienteInternetActivity.java b/app/src/main/java/me/chester/minitruco/android/multiplayer/internet/ClienteInternetActivity.java index 4dc04d31..0e67bc4a 100644 --- a/app/src/main/java/me/chester/minitruco/android/multiplayer/internet/ClienteInternetActivity.java +++ b/app/src/main/java/me/chester/minitruco/android/multiplayer/internet/ClienteInternetActivity.java @@ -54,7 +54,12 @@ protected void onCreate(Bundle savedInstanceState) { findViewById(R.id.btnIniciarBluetooth).setOnClickListener(v -> { enviaLinha("Q"); }); - + findViewById(R.id.btnInverter).setOnClickListener(v -> { + enviaLinha("R I"); + }); + findViewById(R.id.btnTrocar).setOnClickListener(v -> { + enviaLinha("R T"); + }); new Thread(() -> { try {