📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Sistemas com Java (Swing/JavaFX). Recomendado revisar antes de começar.
O objetivo é criar uma réplica do jogo “Genius” (Simon) em Java 21, com foco em uma arquitetura limpa e práticas modernas de desenvolvimento. Como nenhum código foi fornecido, construiremos a solução do zero, seguindo uma abordagem didática.
💡 O mesmo conceito em outra stack: o mesmo Genius/Simon também existe em Kotlin/Android no curso Mobile Nativo Android (versões Demo e Classic).
O primeiro passo é construir a interface gráfica (GUI) baseada na imagem. Usaremos a biblioteca Swing para compatibilidade.
Princípios:
GeniusGame) atuará como o “Controlador” (manipulando eventos) e a “Visão” (estendendo JFrame). Em aplicações maiores, separaríamos isso, mas para um jogo simples, é aceitável.null layout). Usaremos BorderLayout e GridLayout para um design responsivo.Passo a Passo:
GeniusGame que herda de JFrame.JLabel para pontuação e JButton para as cores e o início.JFrame usará BorderLayout.NORTH): Um JPanel com FlowLayout para os placares (scoreLabel, recordLabel).CENTER): Um JPanel com GridLayout(2, 2) para os quatro botões coloridos.SOUTH): Um JPanel com FlowLayout para o startButton.createColorButton() para evitar repetição de código (Princípio DRY - Don’t Repeat Yourself) ao configurar os botões.SwingUtilities.invokeLater()./* Aula 1: Estrutura da GUI */
import javax.swing.*;
import java.awt.*;
// Usamos um Enum para representar os botões.
// Isso é mais seguro e legível que usar Strings ou números.
enum ColorButton {
GREEN, RED, YELLOW, BLUE
}
public class GeniusGame extends JFrame {
private JLabel scoreLabel;
private JLabel recordLabel;
private JButton startButton;
private JButton greenButton, redButton, yellowButton, blueButton;
public GeniusGame() {
setTitle("Jogo Genius (Java 21)");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(400, 500);
setLayout(new BorderLayout(10, 10));
setLocationRelativeTo(null);
// --- 1. Painel de Pontuação (Norte) ---
JPanel scorePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 10));
scoreLabel = new JLabel("Pontuação: 0");
recordLabel = new JLabel("Recorde: 6"); // Valor inicial da imagem
scorePanel.add(scoreLabel);
scorePanel.add(recordLabel);
add(scorePanel, BorderLayout.NORTH);
// --- 2. Painel de Cores (Centro) ---
JPanel colorPanel = new JPanel(new GridLayout(2, 2, 10, 10));
greenButton = createColorButton(Color.GREEN, ColorButton.GREEN);
redButton = createColorButton(Color.RED, ColorButton.RED);
yellowButton = createColorButton(Color.YELLOW, ColorButton.YELLOW);
blueButton = createColorButton(Color.BLUE, ColorButton.BLUE);
colorPanel.add(greenButton);
colorPanel.add(redButton);
colorPanel.add(yellowButton);
colorPanel.add(blueButton);
add(colorPanel, BorderLayout.CENTER);
// --- 3. Painel de Controle (Sul) ---
JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 10));
startButton = new JButton("Iniciar Jogo");
controlPanel.add(startButton);
add(controlPanel, BorderLayout.SOUTH);
}
// Método auxiliar (Princípio DRY)
private JButton createColorButton(Color color, ColorButton id) {
JButton button = new JButton();
button.setBackground(color);
button.setOpaque(true);
button.setBorderPainted(false);
button.setPreferredSize(new Dimension(150, 150));
// Usamos setActionCommand para identificar o botão no listener
button.setActionCommand(id.name());
return button;
}
public static void main(String[] args) {
// Correto: Inicia a GUI na Event Dispatch Thread
SwingUtilities.invokeLater(() -> {
new GeniusGame().setVisible(true);
});
}
}
Agora, adicionamos as variáveis de estado e a lógica principal (o “Modelo” do nosso jogo).
Princípios:
Passo a Passo:
GeniusGame para:
java.util.List<ColorButton> sequence: Armazena a sequência de cores.java.util.Random random: Gerador de números aleatórios.int score, int highScore: Pontuação.int playerIndex: Onde o jogador está na sequência atual.boolean isPlayerTurn: Controla de quem é a vez.sequence = new ArrayList<>()).startGame(): Reseta o jogo e inicia a primeira rodada.nextRound(): Adiciona uma nova cor à sequência e chama playSequence().playerTurn(): Ativa os botões para o jogador.checkPlayerInput(ColorButton pressedColor): Verifica se o jogador acertou a cor.gameOver(): Finaliza o jogo e atualiza o recorde.updateScoreboard(): Atualiza os JLabels.enableColorButtons(boolean enable): Habilita/desabilita os botões de cor./* Aula 2: Adicionando Lógica e Estado (incremental) */
// Adicionar estes campos ao topo da classe GeniusGame
private java.util.List<ColorButton> sequence;
private java.util.Random random;
private int score;
private int highScore = 6; // Da imagem
private int playerIndex;
private boolean isPlayerTurn;
// Adicionar ao final do construtor GeniusGame()
public GeniusGame() {
// ... (código da Aula 1) ...
sequence = new java.util.ArrayList<>();
random = new java.util.Random();
score = 0;
playerIndex = 0;
isPlayerTurn = false;
// Desabilitar botões coloridos até o jogo começar
enableColorButtons(false);
// (Ação dos botões será na Aula 3)
}
// --- Métodos de Lógica do Jogo (Adicionar à classe) ---
private void startGame() {
sequence.clear();
score = 0;
playerIndex = 0;
updateScoreboard();
isPlayerTurn = false;
startButton.setEnabled(false);
nextRound();
}
private void nextRound() {
isPlayerTurn = false;
enableColorButtons(false);
playerIndex = 0;
// Adiciona uma nova cor aleatória à sequência
sequence.add(ColorButton.values()[random.nextInt(4)]);
// Tocar a sequência (será implementado na Aula 3)
playSequence();
}
private void playerTurn() {
isPlayerTurn = true;
enableColorButtons(true);
}
private void checkPlayerInput(ColorButton pressedColor) {
if (!isPlayerTurn) return;
if (sequence.get(playerIndex) == pressedColor) {
// Acerto
playerIndex++;
if (playerIndex == sequence.size()) {
// Jogador completou a rodada
score++;
updateScoreboard();
isPlayerTurn = false;
enableColorButtons(false);
// Pausa antes da próxima rodada (usando Timer Swing)
javax.swing.Timer nextRoundTimer = new javax.swing.Timer(1000, e -> nextRound());
nextRoundTimer.setRepeats(false); // Importante: executar apenas uma vez
nextRoundTimer.start();
}
} else {
// Erro
gameOver();
}
}
private void gameOver() {
isPlayerTurn = false;
enableColorButtons(false);
startButton.setEnabled(true);
if (score > highScore) {
highScore = score;
}
updateScoreboard();
JOptionPane.showMessageDialog(this, "Game Over! Pontuação: " + score, "Fim de Jogo", JOptionPane.INFORMATION_MESSAGE);
}
private void updateScoreboard() {
scoreLabel.setText("Pontuação: " + score);
recordLabel.setText("Recorde: " + highScore);
}
private void enableColorButtons(boolean enable) {
greenButton.setEnabled(enable);
redButton.setEnabled(enable);
yellowButton.setEnabled(enable);
blueButton.setEnabled(enable);
}
// Método placeholder
private void playSequence() {
// Implementação crucial na próxima aula
System.out.println("Tocando sequência: " + sequence);
// Após tocar, deve chamar playerTurn()
playerTurn();
}
Este é o núcleo da aplicação: manipular cliques e fornecer feedback (piscar os botões) sem travar a interface.
Princípios:
Thread.sleep()) nela irão congelar a aplicação.doInBackground(): Roda na thread de trabalho (aqui usamos Thread.sleep()).publish()/process(): Usado para enviar atualizações parciais da thread de trabalho para a EDT (para piscar o botão).done(): Roda na EDT quando doInBackground() termina (para chamar playerTurn()).Passo a Passo:
ActionListener ao startButton (chamando startGame()) e aos botões coloridos (chamando checkPlayerInput()).highlightButton(): Crie um método que altera a cor de um botão para “brilhante” (feedback visual).playSequence() com SwingWorker:
SwingWorker<Void, ColorButton>.doInBackground(), itere pela sequence.publish(color) para “acender” o botão.Thread.sleep() para a pausa.publish(null) para “apagar” o botão.process(), chame highlightButton(color) para atualizar a GUI.done(), chame playerTurn().checkPlayerInput()./* Aula 3: Eventos e Concorrência (incremental) */
import java.util.List; // Importar java.util.List
// Adicionar ao construtor GeniusGame()
public GeniusGame() {
// ... (código da Aula 2) ...
// --- 4. Registro de Eventos ---
startButton.addActionListener(e -> startGame());
// Listener único para todos os botões coloridos
java.awt.event.ActionListener colorListener = e -> {
// Usamos o ActionCommand definido na Aula 1
checkPlayerInput(ColorButton.valueOf(e.getActionCommand()));
};
greenButton.addActionListener(colorListener);
redButton.addActionListener(colorListener);
yellowButton.addActionListener(colorListener);
blueButton.addActionListener(colorListener);
}
// --- Novos Métodos de Feedback Visual ---
// Substituir o placeholder da Aula 2
private void playSequence() {
new SwingWorker<Void, ColorButton>() {
@Override
protected Void doInBackground() throws Exception {
Thread.sleep(1000); // Pausa antes de começar
for (ColorButton color : sequence) {
publish(color); // Publica a cor para "acender"
Thread.sleep(600);
publish(null); // Publica null para "apagar"
Thread.sleep(200);
}
return null;
}
@Override
protected void process(List<ColorButton> chunks) {
// Este método roda na EDT, seguro para GUI
for (ColorButton color : chunks) {
highlightButton(color);
}
}
@Override
protected void done() {
// Roda na EDT ao final
highlightButton(null);
playerTurn(); // Agora é a vez do jogador
}
}.execute();
}
private void highlightButton(ColorButton color) {
// Reseta todos para a cor base
greenButton.setBackground(Color.GREEN);
redButton.setBackground(Color.RED);
yellowButton.setBackground(Color.YELLOW);
blueButton.setBackground(Color.BLUE);
if (color == null) return;
// Destaca o botão específico
// Usamos .brighter() para um efeito simples
switch (color) {
case GREEN:
greenButton.setBackground(Color.GREEN.brighter().brighter());
break;
case RED:
redButton.setBackground(Color.RED.brighter().brighter());
break;
case YELLOW:
yellowButton.setBackground(Color.YELLOW.brighter().brighter());
break;
case BLUE:
blueButton.setBackground(Color.BLUE.brighter().brighter());
break;
}
}
// --- Atualização no checkPlayerInput (para feedback) ---
private void checkPlayerInput(ColorButton pressedColor) {
if (!isPlayerTurn) return;
// --- Adicionar Feedback Visual Imediato ---
highlightButton(pressedColor);
// Usamos um Timer para "apagar" o botão após 200ms
javax.swing.Timer highlightTimer = new javax.swing.Timer(200, e -> highlightButton(null));
highlightTimer.setRepeats(false);
highlightTimer.start();
// --- Fim do Feedback ---
if (sequence.get(playerIndex) == pressedColor) {
// (Lógica de acerto da Aula 2)
// ...
} else {
gameOver();
}
}
O SwingWorker é funcional, mas verboso. Com o Java 21 (Project Loom), podemos simplificar drasticamente a concorrência usando Threads Virtuais.
Princípios:
Thread.sleep()) é barato e não bloqueia a thread do sistema operacional (Kernel Thread).Thread.ofVirtual().start() para rodar a lógica de espera. Quando precisamos atualizar a GUI, usamos SwingUtilities.invokeLater() para enviar essa atualização de volta à EDT.Passo a Passo (Refatoração):
playSequence(): Substitua o SwingWorker por uma Thread Virtual. O código fica linear e muito mais fácil de ler.highlightButton(): Use uma Switch Expression (Java 14+) para um código mais limpo.var: Adote a inferência de tipo para variáveis locais (Java 10+)./* Aula 4: Refatoração para Java 21 (incremental) */
// 1. Refatorar playSequence() (Substitui o SwingWorker da Aula 3)
private void playSequence() {
// Java 21: Usando Threads Virtuais (Project Loom)
Thread.ofVirtual().start(() -> {
try {
Thread.sleep(1000);
for (var color : sequence) { // Usando 'var' (Java 10+)
// 1. Atualiza a GUI (pisca) -> Deve ser na EDT
SwingUtilities.invokeLater(() -> highlightButton(color));
Thread.sleep(600);
// 2. Atualiza a GUI (apaga) -> Deve ser na EDT
SwingUtilities.invokeLater(() -> highlightButton(null));
Thread.sleep(200);
}
// 3. Habilita o turno do jogador -> Deve ser na EDT
SwingUtilities.invokeLater(this::playerTurn); // Usando method reference
} catch (InterruptedException e) {
// Em apps reais, tratar o cancelamento
Thread.currentThread().interrupt();
}
});
}
// 2. Refatorar highlightButton() (Substitui o da Aula 3)
private void highlightButton(ColorButton color) {
// Reseta todos
greenButton.setBackground(Color.GREEN);
redButton.setBackground(Color.RED);
yellowButton.setBackground(Color.YELLOW);
blueButton.setBackground(Color.BLUE);
if (color == null) return;
// Java 14+: Switch Expression
// Obtém a cor base do botão que será destacado
Color baseColor = switch (color) {
case GREEN -> Color.GREEN;
case RED -> Color.RED;
case YELLOW -> Color.YELLOW;
case BLUE -> Color.BLUE;
};
// Obtém o botão que será destacado
JButton buttonToHighlight = switch (color) {
case GREEN -> greenButton;
case RED -> redButton;
case YELLOW -> yellowButton;
case BLUE -> blueButton;
};
// Aplica o brilho
buttonToHighlight.setBackground(baseColor.brighter().brighter());
}
Este é o arquivo GeniusGame.java completo, aplicando todas as aulas e refatorações.
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Implementação do Jogo Genius (Simon) usando Java 21 (Swing e Virtual Threads).
* Foco: Boas práticas de concorrência em GUI e código moderno.
*/
public class GeniusGame extends JFrame {
// Enum para estado de cor (type-safe)
private enum ColorButton {
GREEN, RED, YELLOW, BLUE
}
// --- Componentes da GUI ---
private final JLabel scoreLabel;
private final JLabel recordLabel;
private final JButton startButton;
private final JButton greenButton, redButton, yellowButton, blueButton;
// --- Estado do Jogo ---
private final List<ColorButton> sequence;
private final Random random;
private int score;
private int highScore = 6; // Valor inicial da imagem
private int playerIndex;
private boolean isPlayerTurn;
public GeniusGame() {
setTitle("Jogo Genius (Java 21)");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(400, 500);
setLayout(new BorderLayout(10, 10));
setLocationRelativeTo(null);
// --- Estado ---
sequence = new ArrayList<>();
random = new Random();
score = 0;
playerIndex = 0;
isPlayerTurn = false;
// --- 1. Painel de Pontuação (Norte) ---
JPanel scorePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 10));
scoreLabel = new JLabel("Pontuação: " + score);
recordLabel = new JLabel("Recorde: " + highScore);
scorePanel.add(scoreLabel);
scorePanel.add(recordLabel);
add(scorePanel, BorderLayout.NORTH);
// --- 2. Painel de Cores (Centro) ---
JPanel colorPanel = new JPanel(new GridLayout(2, 2, 10, 10));
greenButton = createColorButton(Color.GREEN, ColorButton.GREEN);
redButton = createColorButton(Color.RED, ColorButton.RED);
yellowButton = createColorButton(Color.YELLOW, ColorButton.YELLOW);
blueButton = createColorButton(Color.BLUE, ColorButton.BLUE);
colorPanel.add(greenButton);
colorPanel.add(redButton);
colorPanel.add(yellowButton);
colorPanel.add(blueButton);
add(colorPanel, BorderLayout.CENTER);
// --- 3. Painel de Controle (Sul) ---
JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 10));
startButton = new JButton("Iniciar Jogo");
controlPanel.add(startButton);
add(controlPanel, BorderLayout.SOUTH);
// --- 4. Registro de Eventos ---
startButton.addActionListener(e -> startGame());
ActionListener colorListener = e -> {
checkPlayerInput(ColorButton.valueOf(e.getActionCommand()));
};
greenButton.addActionListener(colorListener);
redButton.addActionListener(colorListener);
yellowButton.addActionListener(colorListener);
blueButton.addActionListener(colorListener);
// Estado inicial dos botões
enableColorButtons(false);
}
// Método auxiliar (Princípio DRY)
private JButton createColorButton(Color color, ColorButton id) {
var button = new JButton(); // 'var'
button.setBackground(color);
button.setOpaque(true);
button.setBorderPainted(false);
button.setPreferredSize(new Dimension(150, 150));
button.setActionCommand(id.name());
return button;
}
// --- Métodos de Lógica do Jogo ---
private void startGame() {
sequence.clear();
score = 0;
playerIndex = 0;
updateScoreboard();
isPlayerTurn = false;
startButton.setEnabled(false);
nextRound();
}
private void nextRound() {
isPlayerTurn = false;
enableColorButtons(false);
playerIndex = 0;
sequence.add(ColorButton.values()[random.nextInt(4)]);
playSequence();
}
private void playerTurn() {
isPlayerTurn = true;
enableColorButtons(true);
}
private void checkPlayerInput(ColorButton pressedColor) {
if (!isPlayerTurn) return;
// Feedback visual imediato
highlightButton(pressedColor);
var highlightTimer = new Timer(200, e -> highlightButton(null));
highlightTimer.setRepeats(false);
highlightTimer.start();
if (sequence.get(playerIndex) == pressedColor) {
// Acerto
playerIndex++;
if (playerIndex == sequence.size()) {
// Jogador completou a rodada
score++;
updateScoreboard();
isPlayerTurn = false;
enableColorButtons(false);
var nextRoundTimer = new Timer(1000, e -> nextRound());
nextRoundTimer.setRepeats(false);
nextRoundTimer.start();
}
} else {
// Erro
gameOver();
}
}
private void gameOver() {
isPlayerTurn = false;
enableColorButtons(false);
startButton.setEnabled(true);
if (score > highScore) {
highScore = score;
}
updateScoreboard();
JOptionPane.showMessageDialog(this, "Game Over! Pontuação: " + score, "Fim de Jogo", JOptionPane.INFORMATION_MESSAGE);
}
private void updateScoreboard() {
scoreLabel.setText("Pontuação: " + score);
recordLabel.setText("Recorde: " + highScore);
}
private void enableColorButtons(boolean enable) {
greenButton.setEnabled(enable);
redButton.setEnabled(enable);
yellowButton.setEnabled(enable);
blueButton.setEnabled(enable);
}
// --- Concorrência e Feedback Visual (Java 21) ---
private void playSequence() {
// Java 21: Usando Threads Virtuais (Project Loom)
Thread.ofVirtual().start(() -> {
try {
Thread.sleep(1000); // Pausa antes de começar
for (var color : sequence) { // 'var'
// 1. Atualiza a GUI (pisca) -> Deve ser na EDT
SwingUtilities.invokeLater(() -> highlightButton(color));
Thread.sleep(600);
// 2. Atualiza a GUI (apaga) -> Deve ser na EDT
SwingUtilities.invokeLater(() -> highlightButton(null));
Thread.sleep(200);
}
// 3. Habilita o turno do jogador -> Deve ser na EDT
SwingUtilities.invokeLater(this::playerTurn); // Method reference
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
private void highlightButton(ColorButton color) {
// Reseta todos
greenButton.setBackground(Color.GREEN);
redButton.setBackground(Color.RED);
yellowButton.setBackground(Color.YELLOW);
blueButton.setBackground(Color.BLUE);
if (color == null) return;
// Java 14+: Switch Expression
var buttonToHighlight = switch (color) {
case GREEN -> greenButton;
case RED -> redButton;
case YELLOW -> yellowButton;
case BLUE -> blueButton;
};
// Pega a cor base e ilumina
Color baseColor = buttonToHighlight.getBackground();
buttonToHighlight.setBackground(baseColor.brighter().brighter());
// Opcional: Tocar som
// Toolkit.getDefaultToolkit().beep();
}
// --- Ponto de Entrada ---
public static void main(String[] args) {
// Garante a execução na Event Dispatch Thread (EDT)
SwingUtilities.invokeLater(() -> {
new GeniusGame().setVisible(true);
});
}
}
Arquitetura Atual (MVC Simplificado):
sequence, score, isPlayerTurn) e a lógica (checkPlayerInput, nextRound).JFrame, JButton, JLabel).ActionListeners e os métodos de lógica (startGame).GeniusGame. Para um projeto desta escala, é aceitável.Pontos de Melhoria (Próximos Passos):
GameLogic (Modelo): Conteria sequence, score, highScore e os métodos nextRound, checkInput. Não teria nenhuma referência ao Swing.GameView (Visão): Conteria o JFrame e os componentes. Exporia métodos como highlight(ColorButton c) e updateScore(int s).GameController (Controlador): Conteria os ActionListeners e faria a ponte: “Quando startButton for clicado, chame gameLogic.start() e atualize gameView”.Platform.runLater() em vez de SwingUtilities.invokeLater()).Toolkit.getDefaultToolkit().beep() é limitado. Seria melhor usar javax.sound.sampled para tocar arquivos .wav específicos para cada cor.GameLogic (se separada) poderia ser facilmente testada unitariamente (JUnit 5) sem depender da GUI, verificando se checkInput retorna o resultado correto para uma sequência conhecida.https://www.freeconvert.com/pt/mp3-to-wav
https://online-audio-converter.com/pt/
https://s3.amazonaws.com/freecodecamp/simonSound4.mp3
https://s3.amazonaws.com/freecodecamp/simonSound3.mp3
https://s3.amazonaws.com/freecodecamp/simonSound2.mp3
https://s3.amazonaws.com/freecodecamp/simonSound1.mp3
—`
No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd javadesk_genius_01_swing
javac -d bin src/**/*.java
java -cp bin com.exemplo.Main
# ou mvn test
[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta navegar até a pasta antes de executar: cd proj_aplicacoes_full_stack/projetos/javadesk_genius_01_swing