Final Project PBO - Sliding Image Puzzle

Muhammad Izzuddin A

05111740000035

Game Sliding Puzzle


Deskripsi

Permainan puzzle geser terdiri dari bingkai ubin persegi bernomor dalam urutan acak, dengan satu ubin hilang, goal dari puzzle adalah menempatkan ubin dalam urutan dengan menggeser ubin dengan menggunakan ruang kosong. Aplikasi kali ini menggunakan prinsip yang sama, namun menggunakan potongan-potongan dari sebuah gambar yang diacak. Pemain harus dapat menyusun kembali potongan-potongan menjadi gambar yang utuh.

Class Diagram


Dekronstruksi Module/Class yang dibutuhkan

Game ini memerlukan beberapa modul sebagai berikut:

  1. Puzzle.java (Class utama program, mengatur rule permainan yaitu bagaimana cara menggeser puzzle, mengecek apakah puzzle sudah benar dan menampilkan puzzle)
  2. PuzzlePiece.java (Class yang merepresentasikan ubin/piece dan mengatur tampilan dari ubin)
  3. ImageOpener.java (Class yang berfungsi untuk mencari dan membuka file image di komputer sebagai puzzle)
  4. Sound.java (Class untuk memutar audio)

Implementasi (Source Code)

  1. Puzzle.java
  2.  package puzzle;  
     import javax.swing.AbstractAction;  
     import javax.swing.BorderFactory;  
     import javax.swing.JButton;  
     import javax.swing.JComponent;  
     import javax.swing.JFrame;  
     import javax.swing.JOptionPane;  
     import javax.swing.JPanel;  
     import java.awt.BorderLayout;  
     import java.awt.Color;  
     import java.awt.EventQueue;  
     import java.awt.GridLayout;  
     import java.awt.Image;  
     import java.awt.Point;  
     import java.awt.event.ActionEvent;  
     import java.awt.image.BufferedImage;  
     import java.awt.image.CropImageFilter;  
     import java.awt.image.FilteredImageSource;  
     import java.io.IOException;  
     import java.util.ArrayList;  
     import java.util.Collections;  
     import java.util.List;  
     import java.util.concurrent.TimeUnit;  
     public class Puzzle extends JFrame {  
       private JPanel panel;  
       private ImageOpener IO;  
       private BufferedImage source;  
       private BufferedImage resized;  
       private Image image;  
       private PuzzlePiece lastButton;  
       private int width, height;  
       private List<PuzzlePiece> buttons;  
       private List<Point> solution;  
       private final int NUMBER_OF_BUTTONS = 12;  
       private final int DESIRED_WIDTH = 300;  
       private Sound fanfare = new Sound("fanfare.wav");  
       private long startTime;  
       private long endTime;  
       public Puzzle() {  
         IO = new ImageOpener();  
         initUI();  
       }  
       private void initUI() {  
         // list of puzzle piece position for solution  
         solution = new ArrayList<>();  
         solution.add(new Point(0, 0));  
         solution.add(new Point(0, 1));  
         solution.add(new Point(0, 2));  
         solution.add(new Point(1, 0));  
         solution.add(new Point(1, 1));  
         solution.add(new Point(1, 2));  
         solution.add(new Point(2, 0));  
         solution.add(new Point(2, 1));  
         solution.add(new Point(2, 2));  
         solution.add(new Point(3, 0));  
         solution.add(new Point(3, 1));  
         solution.add(new Point(3, 2));  
         buttons = new ArrayList<>();  
         panel = new JPanel();  
         panel.setBorder(BorderFactory.createLineBorder(Color.gray));  
         panel.setLayout(new GridLayout(4, 3, 0, 0));  
         // open image  
         try {  
           IO.openImage();  
           source = IO.loadImage();  
           int h = getNewHeight(source.getWidth(), source.getHeight());  
           resized = IO.resizeImage(source, DESIRED_WIDTH, h,  
               BufferedImage.TYPE_INT_ARGB);  
         } catch (IOException ex) {  
           JOptionPane.showMessageDialog(this, "Could not load image", "Error",  
               JOptionPane.ERROR_MESSAGE);  
         }  
         width = resized.getWidth(null);  
         height = resized.getHeight(null);  
         add(panel, BorderLayout.CENTER);  
         // generate puzzle piece from image  
         for (int i = 0; i < 4; i++) {  
           for (int j = 0; j < 3; j++) {  
             image = createImage(new FilteredImageSource(resized.getSource(),  
                 new CropImageFilter(j * width / 3, i * height / 4,  
                     (width / 3), height / 4)));  
             var button = new PuzzlePiece(image);  
             button.putClientProperty("position", new Point(i, j));  
             // set blank piece on bottom-right  
             if (i == 3 && j == 2) {  
               lastButton = new PuzzlePiece();  
               lastButton.setBorderPainted(false);  
               lastButton.setContentAreaFilled(false);  
               lastButton.setLastButton();  
               lastButton.putClientProperty("position", new Point(i, j));  
             } else {  
               buttons.add(button);  
             }  
           }  
         }  
         // shuffle piece  
         Collections.shuffle(buttons);  
         buttons.add(lastButton);  
         // add puzzle piece to UI panel  
         for (int i = 0; i < NUMBER_OF_BUTTONS; i++) {  
           var btn = buttons.get(i);  
           panel.add(btn);  
           btn.setBorder(BorderFactory.createLineBorder(Color.gray));  
           btn.addActionListener(new ClickAction());  
         }  
         pack();  
         setTitle("Image Puzzle");  
         setResizable(false);  
         setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  
         setLocationRelativeTo(null);  
         // start the counter  
         startTime = System.nanoTime();  
       }  
       // get new heights for image  
       private int getNewHeight(int w, int h) {  
         double ratio = DESIRED_WIDTH / (double) w;  
         int newHeight = (int) (h * ratio);  
         return newHeight;  
       }  
       private class ClickAction extends AbstractAction {  
         @Override  
         public void actionPerformed(ActionEvent e) {  
           checkButton(e);  
           checkSolution();  
         }  
         // check piece selected by user,   
         // and swap if it's adjacent to a blank piece  
         private void checkButton(ActionEvent e) {  
           int lidx = 0;  
           for (PuzzlePiece button : buttons) {  
             if (button.isLastButton()) {  
               lidx = buttons.indexOf(button);  
             }  
           }  
           var button = (JButton) e.getSource();  
           int bidx = buttons.indexOf(button);  
           if ((bidx - 1 == lidx) || (bidx + 1 == lidx)  
               || (bidx - 3 == lidx) || (bidx + 3 == lidx)) {  
             Collections.swap(buttons, bidx, lidx);  
             updateButtons();  
           }  
         }  
         // re-render button  
         private void updateButtons() {  
           panel.removeAll();  
           for (JComponent btn : buttons) {  
             panel.add(btn);  
           }  
           panel.validate();  
         }  
       }  
       // check current piece arrangement  
       private void checkSolution() {  
         var current = new ArrayList<Point>();  
         for (JComponent btn : buttons) {  
           current.add((Point) btn.getClientProperty("position"));  
         }  
         if (compareList(solution, current)) {  
           fanfare.play();  
           endTime = System.nanoTime();  
           long totalTime = TimeUnit.NANOSECONDS.toSeconds(endTime - startTime);  
           JOptionPane.showMessageDialog(panel,   
               "You finished in " + totalTime + " seconds!",  
               "Congratulation!", JOptionPane.INFORMATION_MESSAGE);  
           int ans = JOptionPane.showOptionDialog(this,   
               "Continue?",   
               "Game Over",   
               JOptionPane.YES_NO_OPTION,   
               JOptionPane.QUESTION_MESSAGE, null, null, null);  
           if(ans == JOptionPane.NO_OPTION){  
             System.exit(0);  
           } else EventQueue.invokeLater(() -> {  
             var puzzle = new Puzzle();
             this.dispose();
             puzzle.setVisible(true);  
           });  
         }  
       }  
       // compare current piece arrangement to solution  
       public static boolean compareList(List ls1, List ls2) {  
         return ls1.toString().contentEquals(ls2.toString());  
       }  
       public static void main(String[] args) {  
         EventQueue.invokeLater(() -> {  
           var puzzle = new Puzzle();  
           puzzle.setVisible(true);  
         });  
       }  
     }  
    
  3. PuzzlePiece.java
  4.  package puzzle;  
     import javax.swing.JButton;  
     import java.awt.Image;  
     import javax.swing.BorderFactory;  
     import javax.swing.ImageIcon;  
     import java.awt.Color;  
     import java.awt.EventQueue;  
     import java.awt.GridLayout;  
     import java.awt.Image;  
     import java.awt.Point;  
     import java.awt.event.ActionEvent;  
     import java.awt.event.MouseAdapter;  
     import java.awt.event.MouseEvent;  
     class PuzzlePiece extends JButton {  
       /* indikator piece yang berisi image kosong (true = blank piece)  
        * player hanya bisa menukar tempat antar piece dengan piece kosong  
         */  
       private boolean isLastButton;  
       public PuzzlePiece() {  
         super();  
         initUI();  
       }  
       public PuzzlePiece(Image image) {  
         super(new ImageIcon(image));  
         initUI();  
       }  
       private void initUI() {  
         isLastButton = false;  
         BorderFactory.createLineBorder(Color.gray);  
         addMouseListener(new MouseAdapter() {  
           @Override  
           public void mouseEntered(MouseEvent e) {  
             setBorder(BorderFactory.createLineBorder(Color.yellow));  
           }  
           @Override  
           public void mouseExited(MouseEvent e) {  
             setBorder(BorderFactory.createLineBorder(Color.gray));  
           }  
         });  
       }  
       public void setLastButton() {  
         isLastButton = true;  
       }  
       public boolean isLastButton() {  
         return isLastButton;  
       }  
     }  
    
  5. ImageOpener.java
  6.  package puzzle;  
     import javax.imageio.ImageIO;  
     import java.awt.image.BufferedImage;  
     import java.io.File;  
     import java.io.IOException;  
     import javax.swing.JFileChooser;  
     import javax.swing.filechooser.FileNameExtensionFilter;  
     import javax.swing.filechooser.FileSystemView;  
     public class ImageOpener  
     {  
       private String filename;  
       /**  
        * Constructor for objects of class ImageOpener  
        */  
       public ImageOpener()  
       {  
         filename = null;  
       }  
       public void openImage(){  
         /* Show file dialogue */  
         JFileChooser chooser = new JFileChooser(FileSystemView.getFileSystemView().getHomeDirectory());  
         FileNameExtensionFilter filter = new FileNameExtensionFilter("JPG & GIF Images", "jpg", "gif");  
         chooser.setFileFilter(filter);  
         int returnVal = chooser.showOpenDialog(null);  
         if(returnVal == JFileChooser.APPROVE_OPTION) {  
           this.filename = chooser.getSelectedFile().getAbsolutePath();  
         }  
       }  
       public BufferedImage loadImage() throws IOException {  
         var bimg = ImageIO.read(new File(filename));  
         return bimg;  
       }  
       public BufferedImage resizeImage(BufferedImage originalImage, int width,  
                        int height, int type) {  
         var resizedImage = new BufferedImage(width, height, type);  
         var g = resizedImage.createGraphics();  
         g.drawImage(originalImage, 0, 0, width, height, null);  
         g.dispose();  
         return resizedImage;  
       }  
     }  
    
  7. Sound.java
  8.  package puzzle;  
     import java.io.IOException;  
     import java.net.URL;  
     import java.net.MalformedURLException;  
     import javax.sound.sampled.AudioInputStream;  
     import javax.sound.sampled.AudioSystem;  
     import javax.sound.sampled.Clip;  
     import javax.sound.sampled.LineUnavailableException;  
     import javax.sound.sampled.UnsupportedAudioFileException;  
     import java.io.InputStream;  
     public class Sound {  
       private Clip clip;  
       public Sound(String fileName) {  
         // specify the sound to play  
         // (assuming the sound can be played by the audio system)  
         // from a wave File  
         try {  
           URL url = getClass().getResource(fileName);   
           if (url != null) {  
             AudioInputStream sound = AudioSystem.getAudioInputStream(url);  
             // load the sound into memory (a Clip)  
             clip = AudioSystem.getClip();  
             clip.open(sound);  
           } else {  
             throw new RuntimeException("Sound: file not found: " + fileName);  
           }  
         }  
         catch (MalformedURLException e) {  
           e.printStackTrace();  
           throw new RuntimeException("Sound: Malformed URL: " + e);  
         }  
         catch (UnsupportedAudioFileException e) {  
           e.printStackTrace();  
           throw new RuntimeException("Sound: Unsupported Audio File: " + e);  
         }  
         catch (IOException e) {  
           e.printStackTrace();  
           throw new RuntimeException("Sound: Input/Output Error: " + e);  
         }  
         catch (LineUnavailableException e) {  
           e.printStackTrace();  
           throw new RuntimeException("Sound: Line Unavailable Exception Error: " + e);  
         }  
       // play, stop, loop the sound clip  
       }  
       public void play(){  
         clip.setFramePosition(0); // Must always rewind!  
         clip.start();  
       }  
       public void loop(){  
         clip.loop(Clip.LOOP_CONTINUOUSLY);  
       }  
       public void stop(){  
           clip.stop();  
       }  
     }  
    

Demo Aplikasi

  1. Tampilan awal game, user akan diminta untuk membuka file gambar yang akan digunakan sebagai puzzle.


  2. Tampilan utama game, user dapat mengubah posisi puzzle piece menggunakan left click dari mouse. 

  3. Tampilan akhir permainan.

  4. Setelah permainan berakhir, user dapat memilih untuk melanjutkan bermain dengan menekan tombol YES atau selesai dengan menekan tombol NO.

Video pembahasan source code dan demo

Video pembahasan source code


Video demo gameplay


Instalasi dan link Download

  1. Pastikan JDK (Java development kit) telah terinstal di komputer anda
  2. Download aplikasi .jar di link ini
  3. Jalankan file jar dengan melakukan double click atau menggunakan command java -jar sliding-puzzle.jar
  4. Pilih file gambar yang akan digunakan sebagai puzzle


Comments

Popular posts from this blog

Tugas 4 PBO - Aplikasi Ticket Machine

Tugas 6 PBO - Game World of Zuul

ETS PBO