diff --git a/Makefile b/Makefile index 8833e5c..f6b0108 100644 --- a/Makefile +++ b/Makefile @@ -15,16 +15,22 @@ JCFLAGS = -encoding UTF-8 -implicit:none -cp $(OUT) -d $(OUT) CLASSFILES = Pendu.class \ Partie.class \ Fenetre.class \ - Dessin.class + Dessin.class \ + Mots.class \ + Event.class \ + LetterInputFilter.class \ + MenuDifficulte.class \ + Chronometre.class \ + Score.class # Dépendances -$(OUT)Pendu.class : $(IN)Pendu.java $(OUT)Partie.class $(OUT)Fenetre.class +$(OUT)Pendu.class : $(IN)Pendu.java $(OUT)Partie.class $(OUT)Fenetre.class $(OUT)Event.class $(OUT)MenuDifficulte.class $(OUT)Score.class $(JC) $(JCFLAGS) $< $(OUT)Partie.class : $(IN)Partie.java $(OUT)Mots.class $(JC) $(JCFLAGS) $< -$(OUT)Fenetre.class : $(IN)Fenetre.java $(OUT)Partie.class $(OUT)Dessin.class +$(OUT)Fenetre.class : $(IN)Fenetre.java $(OUT)Partie.class $(OUT)Dessin.class $(OUT)Chronometre.class $(OUT)Score.class $(JC) $(JCFLAGS) $< $(OUT)Dessin.class : $(IN)Dessin.java @@ -33,6 +39,21 @@ $(OUT)Dessin.class : $(IN)Dessin.java $(OUT)Mots.class : $(IN)Mots.java $(JC) $(JCFLAGS) $< +$(OUT)Event.class : $(IN)Event.java $(OUT)Fenetre.class $(OUT)LetterInputFilter.class + $(JC) $(JCFLAGS) $< + +$(OUT)LetterInputFilter.class : $(IN)LetterInputFilter.java $(OUT)Fenetre.class + $(JC) $(JCFLAGS) $< + +$(OUT)MenuDifficulte.class : $(IN)MenuDifficulte.java + $(JC) $(JCFLAGS) $< + +$(OUT)Chronometre.class : $(IN)Chronometre.java + $(JC) $(JCFLAGS) $< + +$(OUT)Score.class : $(IN)Score.java + $(JC) $(JCFLAGS) $< + # Commandes Pendu : $(OUT)Pendu.class diff --git a/src/Chronometre.java b/src/Chronometre.java new file mode 100644 index 0000000..609f020 --- /dev/null +++ b/src/Chronometre.java @@ -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)); + } +} diff --git a/src/Dessin.java b/src/Dessin.java index c90cfaf..27a8f12 100644 --- a/src/Dessin.java +++ b/src/Dessin.java @@ -2,87 +2,101 @@ import javax.swing.*; import java.awt.*; /** -* La classe Dessin gère uniquement le dessin du pendu +* La classe Dessin gère uniquement le dessin du pendu, +* avec révélation progressive en fonction du nombre d'erreurs (stage). * -* @version 1.0 -* @author Adrien +* @version 1.1 +* author Adrien * Date : 08-10-2025 -* Licence : */ 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 --- public Dessin() { - // Taille préférée pour s'intégrer dans Fenetre setPreferredSize(new Dimension(600, 350)); 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 --- @Override protected void paintComponent(Graphics graphics) { super.paintComponent(graphics); - // Anti-aliasing pour des traits plus doux - Graphics2D graphics2D = (Graphics2D) graphics.create(); - graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - graphics2D.setStroke(new BasicStroke(3f)); - graphics2D.setColor(Color.DARK_GRAY); + Graphics2D g2 = (Graphics2D) graphics.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setStroke(new BasicStroke(3f)); + g2.setColor(Color.DARK_GRAY); // Repères et proportions int width = getWidth(); int height = getHeight(); - int marginPixels = Math.min(width, height) / 12; // marge proportionnelle + int margin = Math.min(width, height) / 12; - // Potence : socle - int baseYCoordinate = height - marginPixels; - graphics2D.drawLine(marginPixels, baseYCoordinate, width / 2, baseYCoordinate); - - // Montant vertical - int postXCoordinate = marginPixels + (width / 12); - graphics2D.drawLine(postXCoordinate, baseYCoordinate, postXCoordinate, marginPixels); - - // Traverse horizontale + // Potence (toujours affichée) + int baseY = height - margin; + g2.drawLine(margin, baseY, width / 2, baseY); // socle + int postX = margin + (width / 12); + g2.drawLine(postX, baseY, postX, margin); // montant 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 - graphics2D.drawLine(postXCoordinate, marginPixels + height / 10, postXCoordinate + width / 12, marginPixels); + // Corde (toujours) + int ropeX = postX + beamLength; + int ropeTopY = margin; + int ropeBottomY = margin + height / 12; + g2.drawLine(ropeX, ropeTopY, ropeX, ropeBottomY); - // Corde - int ropeXCoordinate = postXCoordinate + beamLength; - int ropeTopYCoordinate = marginPixels; - int ropeBottomYCoordinate = marginPixels + height / 12; - graphics2D.drawLine(ropeXCoordinate, ropeTopYCoordinate, ropeXCoordinate, ropeBottomYCoordinate); + // Géométrie du personnage + int headR = Math.min(width, height) / 16; + int headCX = ropeX; + int headCY = ropeBottomY + headR; + 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 - int headRadiusPixels = Math.min(width, height) / 16; - int headCenterX = ropeXCoordinate; - int headCenterY = ropeBottomYCoordinate + headRadiusPixels; - graphics2D.drawOval(headCenterX - headRadiusPixels, headCenterY - headRadiusPixels, headRadiusPixels * 2, headRadiusPixels * 2); + // Étapes du personnage + if (stage >= 1) { // tête + g2.drawOval(headCX - headR, headCY - headR, headR * 2, headR * 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 - 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(); + g2.dispose(); } - // Affichage @Override - public String toString() { - return ""; - } + public String toString() { return ""; } } diff --git a/src/Fenetre.java b/src/Fenetre.java index a62c14e..dac077f 100644 --- a/src/Fenetre.java +++ b/src/Fenetre.java @@ -22,7 +22,9 @@ public class Fenetre { private JLabel wordLabel; private JTextField letterInput; private JButton sendButton; - private JPanel drawZone; // instance de Dessin + private JPanel drawZone; + private Chronometre chronometre; + private JLabel scoreLabel; // --- Constructeur --- public Fenetre() { @@ -32,6 +34,17 @@ public class Fenetre { root.setLayout(new BoxLayout(root, BoxLayout.Y_AXIS)); 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(); root.add(drawZone); @@ -84,15 +97,6 @@ public class Fenetre { public JButton getSendButton() { return sendButton; } public JLabel getWordLabel() { return wordLabel; } public JPanel getDrawZone() { return drawZone; } - - // --- Méthode principale de test --- - 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)") - ); - }); - } + public Chronometre getChronometre() { return chronometre; } + public JLabel getScoreLabel() { return scoreLabel; } } \ No newline at end of file diff --git a/src/MenuDifficulte.java b/src/MenuDifficulte.java new file mode 100644 index 0000000..81af18c --- /dev/null +++ b/src/MenuDifficulte.java @@ -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 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 onDifficultyChosen; + + /** Construit la fenêtre et prépare les boutons. */ + public MenuDifficulte(Consumer 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); + } + } +} diff --git a/src/Partie.java b/src/Partie.java index 0052a7a..84e2f8f 100644 --- a/src/Partie.java +++ b/src/Partie.java @@ -10,7 +10,7 @@ import java.util.Random; */ public class Partie { //Contantes - private static final byte REMAININGTRY = 11 ; + private static final byte REMAININGTRY = 6 ; private static final byte CARACTERCODESHIFT = 65 ; //Décalage ASCI > 'A' //Attributs @@ -133,3 +133,4 @@ public class Partie { System.out.println("Essais restants : " + game.getRemainingTry()); } } + diff --git a/src/Pendu.java b/src/Pendu.java index e478d9c..cfebb1d 100644 --- a/src/Pendu.java +++ b/src/Pendu.java @@ -1,14 +1,135 @@ +import javax.swing.*; +import java.util.function.Consumer; /** -* La classe Pendu -* -* @version -* @author -* Date : -* Licence : -*/ + * 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. + * Met à jour le dessin du pendu à chaque erreur. + * + * @version 1.4 + * author Adrien + * Date : 08-10-2025 + * Licence : + */ 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é (construit ici) + fenetre.getWordLabel().setText(buildMaskedWord(partie)); + + // Stage initial (0 erreur) + if (fenetre.getDrawZone() instanceof Dessin) { + ((Dessin) fenetre.getDrawZone()).setStage(0); + } + + // On mémorise les essais initiaux pour calculer les erreurs = initialTries - remainingTry + final int initialTries = partie.getRemainingTry(); + + // Handler : applique Partie puis met à jour l'UI (mot + dessin + score) + Consumer handler = new GameLetterHandler(fenetre, partie, initialTries); + + // Branchement des événements clavier/bouton + new Event(fenetre, handler); + } + + /** Construit la chaîne "_ _ A _ _" à partir de l'état de Partie (sans modifier Partie). */ + private static String buildMaskedWord(Partie partie) { + char[] word = partie.getSecretWord(); + boolean[] found = partie.getFoundLetters(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < word.length; i++) { + sb.append(found[i] ? word[i] : '_'); + if (i < word.length - 1) sb.append(' '); + } + return sb.toString(); + } + + /** + * Handler de lettres : + * - applique Partie.isAlreadyEntries + * - met à jour le mot affiché + * - calcule errors = initialTries - remainingTry, puis stage = min(errors, Dessin.MAX_STAGE) + * - met à jour le score + * - gère la fin de partie + */ + private static class GameLetterHandler implements Consumer { + private final Fenetre fenetre; + private final Partie partie; + private final int initialTries; // essais au démarrage (peut être 11 avec ta Partie) + private final Dessin dessinPanel; + + GameLetterHandler(Fenetre fenetre, Partie partie, int initialTries) { + this.fenetre = fenetre; + this.partie = partie; + this.initialTries = initialTries; + 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(buildMaskedWord(partie)); + + // Erreurs -> stage pour le dessin (borné à MAX_STAGE) + int errors = Math.max(0, initialTries - partie.getRemainingTry()); + if (dessinPanel != null) { + int stage = Math.min(errors, Dessin.MAX_STAGE); + dessinPanel.setStage(stage); + } + + // Mise à jour du score courant (si tu utilises Score + Chronometre) + long elapsed = (fenetre.getChronometre() != null) + ? fenetre.getChronometre().getElapsedMillis() : 0L; + if (fenetre.getScoreLabel() != null) { + int score = Score.compute(errors, elapsed); + 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 (si Score/Chronometre présents) + long finalElapsed = (fenetre.getChronometre() != null) + ? fenetre.getChronometre().getElapsedMillis() : elapsed; + int finalErrors = Math.max(0, initialTries - 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()) + + (fenetre.getScoreLabel() != null ? "\nScore : " + finalScore : ""); + + JOptionPane.showMessageDialog(fenetre.getWindow(), msg); + + fenetre.getLetterInput().setEnabled(false); + fenetre.getSendButton().setEnabled(false); + } + } + } } diff --git a/src/Score.java b/src/Score.java new file mode 100644 index 0000000..2bed1b3 --- /dev/null +++ b/src/Score.java @@ -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); + } +}