Compare commits

...

7 Commits

7 changed files with 473 additions and 84 deletions

83
src/Chronometre.java Normal file
View File

@@ -0,0 +1,83 @@
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* Composant chronomètre (mm:ss) pour le jeu du pendu.
* - Démarre/stoppe/réinitialise le temps
* - S'affiche comme une barre en haut de la fenêtre
*
* @version 1.0
* @author Adrien
* Date : 08-10-2025
* Licence :
*/
public class Chronometre extends JPanel implements ActionListener {
private final JLabel timeLabel = new JLabel("00:00", SwingConstants.CENTER);
private final Timer timer; // tick chaque seconde
private boolean running = false;
private long startMillis = 0L;
private long accumulatedMillis = 0L;
public Chronometre() {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
timeLabel.setFont(new Font("Segoe UI", Font.BOLD, 20));
add(timeLabel, BorderLayout.CENTER);
// Timer Swing (1s)
timer = new Timer(1000, this);
}
/** Démarre le chronomètre (ou reprend si en pause). */
public void start() {
if (!running) {
running = true;
startMillis = System.currentTimeMillis();
timer.start();
repaint();
}
}
/** Met en pause le chronomètre. */
public void stop() {
if (running) {
running = false;
accumulatedMillis += System.currentTimeMillis() - startMillis;
timer.stop();
repaint();
}
}
/** Remet le chronomètre à 00:00 (sans le relancer). */
public void reset() {
running = false;
startMillis = 0L;
accumulatedMillis = 0L;
timer.stop();
updateLabel(0L);
}
/** Temps écoulé total (en millisecondes). */
public long getElapsedMillis() {
long now = System.currentTimeMillis();
long runningMillis = running ? (now - startMillis) : 0L;
return accumulatedMillis + runningMillis;
}
/** Tick du timer : met à jour l'affichage. */
@Override
public void actionPerformed(ActionEvent actionEvent) {
updateLabel(getElapsedMillis());
}
// --- interne ---
private void updateLabel(long elapsedMillis) {
long totalSeconds = elapsedMillis / 1000;
long minutes = totalSeconds / 60;
long seconds = totalSeconds % 60;
timeLabel.setText(String.format("%02d:%02d", minutes, seconds));
}
}

View File

@@ -2,87 +2,101 @@ import javax.swing.*;
import java.awt.*; import java.awt.*;
/** /**
* La classe <code>Dessin</code> gère uniquement le dessin du pendu * La classe <code>Dessin</code> gère uniquement le dessin du pendu,
* avec révélation progressive en fonction du nombre d'erreurs (stage).
* *
* @version 1.0 * @version 1.1
* @author Adrien * author Adrien
* Date : 08-10-2025 * Date : 08-10-2025
* Licence :
*/ */
public class Dessin extends JPanel { public class Dessin extends JPanel {
/** Nombre d'étapes max pour le personnage (hors potence). */
public static final int MAX_STAGE = 6;
/** Étape actuelle (erreurs) : 0..6 */
private int stage = 0;
// --- Constructeur --- // --- Constructeur ---
public Dessin() { public Dessin() {
// Taille préférée pour s'intégrer dans Fenetre
setPreferredSize(new Dimension(600, 350)); setPreferredSize(new Dimension(600, 350));
setBackground(new Color(245, 245, 245)); setBackground(new Color(245, 245, 245));
} }
/** Définit l'étape (nb d'erreurs) et redessine. */
public void setStage(int newStage) {
int clamped = Math.max(0, Math.min(newStage, MAX_STAGE));
if (clamped != this.stage) {
this.stage = clamped;
repaint();
}
}
public int getStage() { return stage; }
// --- Dessin principal --- // --- Dessin principal ---
@Override @Override
protected void paintComponent(Graphics graphics) { protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics); super.paintComponent(graphics);
// Anti-aliasing pour des traits plus doux Graphics2D g2 = (Graphics2D) graphics.create();
Graphics2D graphics2D = (Graphics2D) graphics.create(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setStroke(new BasicStroke(3f));
graphics2D.setStroke(new BasicStroke(3f)); g2.setColor(Color.DARK_GRAY);
graphics2D.setColor(Color.DARK_GRAY);
// Repères et proportions // Repères et proportions
int width = getWidth(); int width = getWidth();
int height = getHeight(); int height = getHeight();
int marginPixels = Math.min(width, height) / 12; // marge proportionnelle int margin = Math.min(width, height) / 12;
// Potence : socle // Potence (toujours affichée)
int baseYCoordinate = height - marginPixels; int baseY = height - margin;
graphics2D.drawLine(marginPixels, baseYCoordinate, width / 2, baseYCoordinate); g2.drawLine(margin, baseY, width / 2, baseY); // socle
int postX = margin + (width / 12);
// Montant vertical g2.drawLine(postX, baseY, postX, margin); // montant
int postXCoordinate = marginPixels + (width / 12);
graphics2D.drawLine(postXCoordinate, baseYCoordinate, postXCoordinate, marginPixels);
// Traverse horizontale
int beamLength = width / 3; int beamLength = width / 3;
graphics2D.drawLine(postXCoordinate, marginPixels, postXCoordinate + beamLength, marginPixels); g2.drawLine(postX, margin, postX + beamLength, margin); // traverse
g2.drawLine(postX, margin + height / 10, postX + width / 12, margin); // renfort
// Renfort diagonal // Corde (toujours)
graphics2D.drawLine(postXCoordinate, marginPixels + height / 10, postXCoordinate + width / 12, marginPixels); int ropeX = postX + beamLength;
int ropeTopY = margin;
int ropeBottomY = margin + height / 12;
g2.drawLine(ropeX, ropeTopY, ropeX, ropeBottomY);
// Corde // Géométrie du personnage
int ropeXCoordinate = postXCoordinate + beamLength; int headR = Math.min(width, height) / 16;
int ropeTopYCoordinate = marginPixels; int headCX = ropeX;
int ropeBottomYCoordinate = marginPixels + height / 12; int headCY = ropeBottomY + headR;
graphics2D.drawLine(ropeXCoordinate, ropeTopYCoordinate, ropeXCoordinate, ropeBottomYCoordinate); int bodyTopY = headCY + headR;
int bodyBottomY = bodyTopY + height / 6;
int armSpan = width / 10;
int shouldersY = bodyTopY + height / 24;
int legSpan = width / 12;
// Personnage : tête // Étapes du personnage
int headRadiusPixels = Math.min(width, height) / 16; if (stage >= 1) { // tête
int headCenterX = ropeXCoordinate; g2.drawOval(headCX - headR, headCY - headR, headR * 2, headR * 2);
int headCenterY = ropeBottomYCoordinate + headRadiusPixels; }
graphics2D.drawOval(headCenterX - headRadiusPixels, headCenterY - headRadiusPixels, headRadiusPixels * 2, headRadiusPixels * 2); if (stage >= 2) { // corps
g2.drawLine(headCX, bodyTopY, headCX, bodyBottomY);
}
if (stage >= 3) { // bras gauche
g2.drawLine(headCX, shouldersY, headCX - armSpan, shouldersY + height / 20);
}
if (stage >= 4) { // bras droit
g2.drawLine(headCX, shouldersY, headCX + armSpan, shouldersY + height / 20);
}
if (stage >= 5) { // jambe gauche
g2.drawLine(headCX, bodyBottomY, headCX - legSpan, bodyBottomY + height / 8);
}
if (stage >= 6) { // jambe droite
g2.drawLine(headCX, bodyBottomY, headCX + legSpan, bodyBottomY + height / 8);
}
// Corps g2.dispose();
int bodyTopYCoordinate = headCenterY + headRadiusPixels;
int bodyBottomYCoordinate = bodyTopYCoordinate + height / 6;
graphics2D.drawLine(headCenterX, bodyTopYCoordinate, headCenterX, bodyBottomYCoordinate);
// Bras
int armSpanPixels = width / 10;
int shouldersYCoordinate = bodyTopYCoordinate + height / 24;
graphics2D.drawLine(headCenterX, shouldersYCoordinate, headCenterX - armSpanPixels, shouldersYCoordinate + height / 20);
graphics2D.drawLine(headCenterX, shouldersYCoordinate, headCenterX + armSpanPixels, shouldersYCoordinate + height / 20);
// Jambes
int legSpanPixels = width / 12;
graphics2D.drawLine(headCenterX, bodyBottomYCoordinate, headCenterX - legSpanPixels, bodyBottomYCoordinate + height / 8);
graphics2D.drawLine(headCenterX, bodyBottomYCoordinate, headCenterX + legSpanPixels, bodyBottomYCoordinate + height / 8);
graphics2D.dispose();
} }
// Affichage
@Override @Override
public String toString() { public String toString() { return ""; }
return "";
}
} }

View File

@@ -22,7 +22,9 @@ public class Fenetre {
private JLabel wordLabel; private JLabel wordLabel;
private JTextField letterInput; private JTextField letterInput;
private JButton sendButton; private JButton sendButton;
private JPanel drawZone; // instance de Dessin private JPanel drawZone;
private Chronometre chronometre;
private JLabel scoreLabel;
// --- Constructeur --- // --- Constructeur ---
public Fenetre() { public Fenetre() {
@@ -32,6 +34,17 @@ public class Fenetre {
root.setLayout(new BoxLayout(root, BoxLayout.Y_AXIS)); root.setLayout(new BoxLayout(root, BoxLayout.Y_AXIS));
window.setContentPane(root); window.setContentPane(root);
// Barre supérieure : chrono + score
JPanel topBar = new JPanel(new BorderLayout());
chronometre = new Chronometre();
topBar.add(chronometre, BorderLayout.CENTER);
scoreLabel = new JLabel("Score : " + Score.BASE, SwingConstants.RIGHT);
scoreLabel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 12));
scoreLabel.setFont(new Font("Segoe UI", Font.BOLD, 16));
topBar.add(scoreLabel, BorderLayout.EAST);
root.add(topBar);
chronometre.start(); // démarrage automatique
drawZone = new Dessin(); drawZone = new Dessin();
root.add(drawZone); root.add(drawZone);
@@ -84,15 +97,6 @@ public class Fenetre {
public JButton getSendButton() { return sendButton; } public JButton getSendButton() { return sendButton; }
public JLabel getWordLabel() { return wordLabel; } public JLabel getWordLabel() { return wordLabel; }
public JPanel getDrawZone() { return drawZone; } public JPanel getDrawZone() { return drawZone; }
public Chronometre getChronometre() { return chronometre; }
// --- Méthode principale de test --- public JLabel getScoreLabel() { return scoreLabel; }
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
Fenetre f = new Fenetre();
// On passe le handler directement ici (pas de setOnLetterSubmitted)
new Event(f, ch ->
JOptionPane.showMessageDialog(f.getWindow(), "Lettre reçue : " + ch + " (sans logique de jeu)")
);
});
}
} }

75
src/MenuDifficulte.java Normal file
View File

@@ -0,0 +1,75 @@
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.function.Consumer;
/**
* Fenêtre de sélection de la difficulté (Facile / Moyen / Difficile).
* Notifie le choix via un Consumer<String> fourni au constructeur.
*
* @version 1.0
* @author Adrien
* Date : 08-10-2025
* Licence :
*/
public class MenuDifficulte implements ActionListener {
private final JFrame frame;
private final Consumer<String> onDifficultyChosen;
/** Construit la fenêtre et prépare les boutons. */
public MenuDifficulte(Consumer<String> onDifficultyChosen) {
this.onDifficultyChosen = onDifficultyChosen;
frame = new JFrame("Jeu du Pendu — Choix de difficulté");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400, 250);
frame.setLocationRelativeTo(null);
frame.setLayout(new BorderLayout(0, 10));
JLabel title = new JLabel("Choisissez une difficulté", SwingConstants.CENTER);
title.setFont(new Font("Segoe UI", Font.BOLD, 18));
title.setBorder(BorderFactory.createEmptyBorder(20, 10, 0, 10));
frame.add(title, BorderLayout.NORTH);
JPanel buttonsPanel = new JPanel(new GridLayout(1, 3, 10, 10));
buttonsPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
JButton easyBtn = new JButton("Facile");
JButton mediumBtn = new JButton("Moyen");
JButton hardBtn = new JButton("Difficile");
easyBtn.setActionCommand("Facile");
mediumBtn.setActionCommand("Moyen");
hardBtn.setActionCommand("Difficile");
easyBtn.addActionListener(this);
mediumBtn.addActionListener(this);
hardBtn.addActionListener(this);
buttonsPanel.add(easyBtn);
buttonsPanel.add(mediumBtn);
buttonsPanel.add(hardBtn);
frame.add(buttonsPanel, BorderLayout.CENTER);
}
/** Affiche la fenêtre. */
public void show() { frame.setVisible(true); }
/** Ferme la fenêtre. */
public void close() { frame.dispose(); }
/** Accès optionnel au JFrame. */
public JFrame getFrame() { return frame; }
/** Réception des clics sur les boutons. */
@Override
public void actionPerformed(ActionEvent actionEvent) {
String difficulty = actionEvent.getActionCommand(); // "Facile" | "Moyen" | "Difficile"
frame.dispose();
if (onDifficultyChosen != null) {
onDifficultyChosen.accept(difficulty);
}
}
}

View File

@@ -1,23 +1,97 @@
/** /**
* La classe <code>Partie</code> * La classe <code>Partie</code>
* *
* @version * @version 0.2
* @author * @author Aurélien
* Date : * Date : 08-10-25
* Licence : * Licence :
*/ */
public class Partie { public class Partie {
//Contantes
private static final byte REMAININGTRY = 6 ;
private static final byte CARACTERCODESHIFT = 65 ; //Décalage ASCI > 'A'
//Attributs //Attributs
private char[] secretword ;
private byte wordsize ;
private boolean[] foundletters ;
private boolean[] entriesletters = new boolean[26] ; //Pseudo Alphabée
private byte remainingtry = REMAININGTRY ;
//Constructeur //Constructeur
public Partie() { public Partie() {
this.secretword = generateSecretWord() ;
this.wordsize = (byte) secretword.length ;
this.foundletters = new boolean[wordsize] ;
} }
//Méthodes
//Affichage // Getters
public String toString() { public char[] getSecretWord() { return this.secretword ; }
return "" ; public boolean[] getFoundLetters() { return this.foundletters ; }
public byte getRemainingTry() { return this.remainingtry ; }
/** Représentation masquée du mot, ex: "_ _ A _ _" */
public String getMaskedWord() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < secretword.length; i++) {
sb.append(foundletters[i] ? secretword[i] : '_');
if (i < secretword.length - 1) sb.append(' ');
}
return sb.toString();
} }
/** Vérifie l'état de la partie en cours. */
public boolean gameIsEnding() {
if(this.remainingtry <= 0){
return true ;
}else if(wordIsFound()){
return true ;
}else{
return false ;
}
}
/**
* Vérifie si la lettre reçue a déjà été jouée puis met à jour
* entriesletters / foundletters / remainingtry.
* @return true si la lettre était déjà présente (doublon).
*/
public boolean isAlreadyEntries(char letter) {
short caractercode = (short) letter ; //Récupération du code du caractère
if(this.entriesletters[caractercode-CARACTERCODESHIFT]){
this.remainingtry-- ; //Décrément des essais
return true ;
}else{
boolean isfind = false ;
for(byte i = 0 ; i < this.wordsize ; i++){ //Parcours du "secretword"
if(this.secretword[i] == letter){
this.foundletters[i] = true ;
isfind = true ;
}
}
if(isfind == false){
this.remainingtry-- ; //Décrément des essais
}
this.entriesletters[caractercode-CARACTERCODESHIFT] = true ; //Ajout au tableau des lettres jouées
return false ;
}
}
private char[] generateSecretWord() {
char[] word = {'P','I','Z','Z','A'};
//À implémenter plus tard
return word ;
}
private boolean wordIsFound() {
for(byte i = 0 ; i < this.wordsize ; i++){
if(!this.foundletters[i]){ //Si une lettre n'est pas trouvée
return false ;
}
}
return true ;
}
@Override
public String toString() { return "" ; }
} }

View File

@@ -1,14 +1,117 @@
import javax.swing.*;
import java.util.function.Consumer;
/** /**
* La classe <code>Pendu</code> * Point d'entrée : affiche le menu de difficulté puis lance la fenêtre du jeu.
* * Lie Fenetre (vue) et Partie (logique) via un handler.
* @version * Met à jour le dessin du pendu à chaque erreur.
* @author *
* Date : * @version 1.3
* Licence : * author Adrien
*/ * Date : 08-10-2025
* Licence :
*/
public class Pendu { public class Pendu {
public static void main(String[] args){
} public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
MenuDifficulte menu = new MenuDifficulte(difficulty -> openGameWindow(difficulty));
menu.show();
});
}
/** Ouvre la fenêtre du jeu, crée la Partie et branche les événements. */
private static void openGameWindow(String difficulty) {
Fenetre fenetre = new Fenetre();
fenetre.getWindow().setTitle("Jeu du Pendu — " + difficulty);
Partie partie = new Partie();
// Affichage initial du mot masqué (via Partie)
fenetre.getWordLabel().setText(partie.getMaskedWord());
// Stage initial (0 erreur)
if (fenetre.getDrawZone() instanceof Dessin) {
((Dessin) fenetre.getDrawZone()).setStage(0);
}
// Handler : applique Partie puis met à jour l'UI (mot + dessin)
Consumer<Character> handler = new GameLetterHandler(fenetre, partie);
// Branchement des événements clavier/bouton
new Event(fenetre, handler);
}
/**
* Handler de lettres :
* - applique Partie.isAlreadyEntries
* - met à jour le mot affiché (Partie#getMaskedWord)
* - calcule le stage = MAX_STAGE - remainingTry et met à jour le Dessin
* - gère la fin de partie
*/
private static class GameLetterHandler implements Consumer<Character> {
private final Fenetre fenetre;
private final Partie partie;
private final Dessin dessinPanel;
GameLetterHandler(Fenetre fenetre, Partie partie) {
this.fenetre = fenetre;
this.partie = partie;
this.dessinPanel = (fenetre.getDrawZone() instanceof Dessin)
? (Dessin) fenetre.getDrawZone() : null;
}
@Override
public void accept(Character ch) {
boolean alreadyPlayed = partie.isAlreadyEntries(ch);
// Mise à jour du mot
fenetre.getWordLabel().setText(partie.getMaskedWord());
// Erreurs -> stage pour le dessin
if (dessinPanel != null) {
int stage = Dessin.MAX_STAGE - partie.getRemainingTry();
dessinPanel.setStage(stage);
}
// Mise à jour du score courant (erreurs + temps actuel)
long elapsed = (fenetre.getChronometre() != null)
? fenetre.getChronometre().getElapsedMillis() : 0L;
int errors = Dessin.MAX_STAGE - partie.getRemainingTry();
int score = Score.compute(errors, elapsed);
if (fenetre.getScoreLabel() != null) {
fenetre.getScoreLabel().setText("Score : " + score);
}
if (alreadyPlayed) {
JOptionPane.showMessageDialog(fenetre.getWindow(),
"Lettre déjà jouée : " + ch + "\nEssais restants : " + partie.getRemainingTry());
}
// Fin de partie
if (partie.gameIsEnding()) {
// Stoppe le chronomètre pour figer le temps
if (fenetre.getChronometre() != null) {
fenetre.getChronometre().stop();
}
// Score final recalculé (au cas où une seconde vient de passer)
long finalElapsed = (fenetre.getChronometre() != null)
? fenetre.getChronometre().getElapsedMillis() : elapsed;
int finalErrors = Dessin.MAX_STAGE - partie.getRemainingTry();
int finalScore = Score.compute(finalErrors, finalElapsed);
boolean win = !fenetre.getWordLabel().getText().contains("_");
String msg = (win
? "Bravo ! Mot trouvé : "
: "Perdu ! Le mot était : ")
+ String.valueOf(partie.getSecretWord())
+ "\nScore : " + finalScore;
JOptionPane.showMessageDialog(fenetre.getWindow(), msg);
fenetre.getLetterInput().setEnabled(false);
fenetre.getSendButton().setEnabled(false);
}
}
}
} }

36
src/Score.java Normal file
View File

@@ -0,0 +1,36 @@
/**
* Calcule le score du pendu à partir du nombre d'erreurs et du temps écoulé.
*
* Formule (simple et configurable) :
* score = max(0, BASE - erreurs * ERROR_PENALTY - secondes * TIME_PENALTY_PER_SEC)
*
* @version 1.0
* @author Adrien
* Date : 08-10-2025
* Licence :
*/
public final class Score {
/** Score de départ. */
public static final int BASE = 1000;
/** Malus par erreur. (6 erreurs max par défaut) */
public static final int ERROR_PENALTY = 120;
/** Malus par seconde écoulée. */
public static final double TIME_PENALTY_PER_SEC = 1.0;
private Score() {}
/**
* - nombre d'erreurs (>=0)
* - temps écoulé en millisecondes
* - score >= 0
*/
public static int compute(int errors, long elapsedMillis) {
if (errors < 0) errors = 0;
double timePenalty = (elapsedMillis / 1000.0) * TIME_PENALTY_PER_SEC;
int value = (int)Math.round(BASE - errors * ERROR_PENALTY - timePenalty);
return Math.max(0, value);
}
}