/*************************************************************************** * File : Pacman.java (C) Copyright 1995-1996 * * Version : 2.04 * * Author : Alexander Jean-Claude Bottema * * Email : alexb@csd.uu.se * * Paradigm/Lang: Imperative/Java (by Sun Microsystems) * * Music : "Tyranny of Beauty" and "220 Volt" by Tangarine Dream * * Original : 1995 Dec 15 * * Current : 1996 Mar 31 * * Purpose : Implementing the classic game Pacman originally designed * * by Toru Iwatani and his associates. * *-------------------------------------------------------------------------* * CHANGES: * * * * 960426 Sound on/off works even if sound server is not * * properly initialized. * * 960420 Hiscore registration works now with Netscape * * 960420 Font problems fixed (now dynamically computed) * * 960410 (Mouse clickable) Game controls added. * * 960410 User settable options added (sound, speed, ...) * * 960409 Partial update added. * ***************************************************************************/ /* --------------------- * Dependencies * --------------------- */ import java.io.*; import java.awt.*; import java.applet.*; import java.lang.Integer; import java.net.*; import java.awt.image.ImageObserver; /** * MyObserver class. At the time I started the project I didn't have access * to the MediaTracker class, so I implemented it (a sort of) manually. * The main idea is to force image loading by drawing the image on a * scratch area buffer. There are probably better ways to do it, but the * major change from Java alpha to Java-beta was switching from * synchronous to asynchronous updating. * * @author Alexander Jean-Claude Bottema */ final class MyObserver implements ImageObserver { /** * Scratch buffer size. */ final static int width = 300, height = 150; /** * Scratch buffer. It is static since we only need one regardless * of how many clients who use it (only one can use at a time.) */ static Image buffer = null; static Graphics graphics = null; /** * Image have been fully loaded. */ static boolean sync_flag; /** * Variables to store the actual parameters of draw() method * call. */ Image image; int x, y; /** * An ImageObserver method redefined. It returns true if further * updates are needed. */ public boolean imageUpdate( Image im, int infoflags, int x, int y, int w, int h ) { if (infoflags == ALLBITS) { graphics.drawImage( image, x, y, null ); sync_flag = true; return false; } return true; } /** * Waits for the image to be fully updated by polling. */ void waitUntilReady() { while (!sync_flag) { try { Thread.sleep(100); } catch (InterruptedException e) { } } } /** * MyObserver constructor. It must be supplied with the current * applet in order to create the needed scratch buffers. */ public MyObserver( Applet app ) { buffer = app.createImage( 300, 150 ); graphics = buffer.getGraphics(); } /** * Ensure that the image is loaded. */ public void load( Image im ) { sync_flag = false; if (!draw( graphics, im, 0, 0 )) waitUntilReady(); } /** * Draws the image in the scratch graphics area. It returns true * iff the operation was successful, i.e. the image data have * been loaded. */ public boolean draw( Graphics g, Image im, int in_x, int in_y ) { image = im; x = in_x; y = in_y; return g.drawImage( im, x, y, this ); } } /** * Scene class. The board where the maze and the objects are displayed * is called the "scene." The scene handles all graphic updates and the * main reason is to maintain the background in the conjunction with * the moving objects. I had a lot of trouble in getting this to work, * but the final solution was to allocate a double workspace where only * one of the spaces is shown on screen. The unmodified background is * then constantly copied to the visible workspace to clear the objects. * The operation is much faster than computing what maze segments to be * redrawn. * * @author Alexander Jean-Claude Bottema */ final class Scene { /** * Score status row height in pixels. */ final static int score_row_height = 32; /** * Lives status row height in pixels. */ final static int lives_row_height = 48; /** * Total status height in pixels. */ final static int status_height = score_row_height + lives_row_height; /** * The margin (in pixels) between workspaces. */ final static int margin = 300; /** * Text font and color to be used. */ Font text_font; Color text_color = new Color( 255, 255, 255 ); int approx_char_width; int approx_char_height; /** * Black color and color of (loading) bar */ final static Color black_color = new Color( 0, 0, 0 ); final static Color bar_color = new Color( 255, 0, 0 ); /** * The maze that is currently used by the scene. */ Maze maze; /** * The applet that is currently used. */ Applet applet; /** * The Pacman canvas object that is currently used. */ PacmanCanvas canvas; /** * The Pacman itself, called the "Chomp". We need one of its images * to display the number of lives left. */ Image chomp; /** * The logo image. The scene can be requested to draw it, so we * need its image. */ Image logo; /** * The width and height of the visible scene. */ int width, height; /** * Workspace buffer */ static Image buffer; static Graphics buffer_g; /** * Loading bar length (in percent) */ int loading_bar = 0; /** * Partial update margin, i.e. the width of the frame of the * partial updates. */ final static int partial_update_margin = 10; /** * Constructor. */ Scene( Applet app, String dir, Maze m, PacmanCanvas canv ) { URL url = app.getCodeBase(); MyObserver mo = new MyObserver( app ); // Store maze and canvas maze = m; canvas = canv; // Load images chomp = app.getImage( url, dir + "pacman_east_1.gif" ); logo = app.getImage( url, dir + "pacman_logo.gif" ); mo.load( chomp ); mo.load( logo ); // Get width and height using maze width = m.getWidth(); height = m.getHeight() + status_height; // Allocate workspace buffer if (buffer == null) { buffer = app.createImage( 2 * width + margin, height ); buffer_g = buffer.getGraphics(); } // Compute font int i; for (i = 10; i < 40; i++) { text_font = new Font( "Helvetica", Font.PLAIN, i ); buffer_g.setFont( text_font ); if (buffer_g.getFontMetrics().getHeight() >= 25 && buffer_g.getFontMetrics().getHeight() <= 35) break; } approx_char_height = buffer_g.getFontMetrics().getHeight(); approx_char_width = buffer_g.getFontMetrics().charWidth('M'); if (i == 40) System.out.println("Error. Couldn't adjust size of font."); // Clear scene clearAll(); } /** * Reset loading bar. */ public void resetLoadingBar() { loading_bar = 0; clearAll(); drawLoadingBar(); } /** * Increase loading bar (given percentage.) */ public void incLoadingBar( int inc ) { loading_bar += inc; if (loading_bar > 100) loading_bar = 100; drawLoadingBar(); } /** * Draw loading bar. */ public void drawLoadingBar() { // Ouch! I use hard coordinates here. I know it is // ugly programming, but the loading bar was a fast // hack, since so many complained about the game // loading delay. buffer_g.setColor( text_color ); buffer_g.setFont( text_font ); buffer_g.drawString( "Loading...", 50, 80 ); buffer_g.setColor( bar_color ); buffer_g.drawRect( 50, 100, 350, 20 ); buffer_g.fillRect( 50, 100, (350*loading_bar)/100, 20 ); repaint(); } /** * Draw score. Remember that the scene class handles all * graphic update of the scene and that includes the score. */ public void drawScore( int score ) { buffer_g.setFont( text_font ); buffer_g.setColor( text_color ); // Convert score to string with preceeding zeros. String score_string = (new Integer(score)).toString(); for (int i = score_string.length(); i < 6; i++) score_string = "0" + score_string; score_string = "SCORE " + score_string; // Clear old score buffer_g.copyArea( width + margin, 0, 200, score_row_height, -width-margin, 0 ); // Display new score buffer_g.drawString( score_string, 32, score_row_height - 3 ); } /** * Draw logo at the given coordinates. */ public void drawLogo( int x, int y ) { buffer_g.drawImage( logo, x, y, null ); } /** * Draw the number of lives left. */ public void drawLives( int lives ) { // Clear old status buffer_g.copyArea( width + margin, height - lives_row_height, width, lives_row_height, -width-margin, 0 ); // Display lives for (int i = 1; i < lives; i++) buffer_g.drawImage( chomp, 5 + 32 * i, height - lives_row_height + 5, null ); } /** * Put background from invisible workspace. */ public void putBackground() { buffer_g.copyArea( width+margin, score_row_height, width, height - status_height, -width-margin, 0 ); } /** * Store background from visible workspac to invisible workspace. */ public void setBackground() { buffer_g.copyArea( 0, 0, width, height, width+margin, 0 ); } /** * Clear background in invisible workspace. */ public void clearBackground() { buffer_g.setColor( black_color ); buffer_g.fillRect( 0, score_row_height, width, height - status_height ); buffer_g.setColor( text_color ); } /** * Clear entire workspace. */ public void clearAll() { buffer_g.setColor( black_color ); buffer_g.setPaintMode(); buffer_g.fillRect( 0, 0, 2*width+margin, height ); buffer_g.setColor( text_color ); } /** * Load visible and invisible workspace with maze image. */ public void putMaze() { maze.paintOntoImage( buffer, 0, score_row_height, 0, 0, width, height - status_height ); buffer_g.copyArea( 0, score_row_height, width, height - status_height, width + margin, 0 ); repaint( 0, 0, width, height ); } /** * Put maze quanta at given coordinates. */ public void putMaze( int x, int y ) { maze.paint( buffer_g, 0, score_row_height, x, y, width + margin ); } /** * Put image at given coordinates. */ public void putImage( Image im, int x, int y ) { buffer_g.drawImage( im, x, y + score_row_height, null ); if (canvas.isPartialUpdate()) special_repaint( x - partial_update_margin, y - partial_update_margin + score_row_height, im.getWidth( null ) + 2 * partial_update_margin, im.getHeight( null ) + 2 * partial_update_margin ); } /** * Clear image of size (w,h) with background data at given * coordinates. */ public void clearImage( int x, int y, int w, int h ) { buffer_g.copyArea( x + width + margin, y + score_row_height, w, h, -width-margin, 0 ); } /** * Display string at given coordinates. */ public void putText( String s, int x, int y ) { buffer_g.setFont( text_font ); buffer_g.setColor( text_color ); buffer_g.drawString( s, x, y + score_row_height ); } /** * Clear string at given coordinates. */ public void clearText( String s, int x, int y ) { clearImage( x, y - approx_char_height, s.length() * approx_char_width, approx_char_height ); } /** * Paint dot at given maze coordinates. */ public void paintDot( int x, int y, boolean big ) { maze.paintDot( buffer_g, x, y, big ); } /** * Partial repaint of scene. */ public void repaint( int x, int y, int w, int h ) { canvas.repaint( x, y, w, h ); } /** * Immediate partial repaint of scene. */ public void special_repaint( int x, int y, int w, int h ) { canvas.special_repaint( x, y, w, h ); } /** * Total repaint of scene. */ public void repaint() { canvas.repaint(); } /** * Update. */ public void update( Graphics g ) { // System.out.println( g.getClipRect() ); g.drawImage( buffer, 0, 0, null ); } /** * Return width of scene. */ public int getWidth() { return width; } /** * Return height of scene. */ public int getHeight() { return height; } } /** * Maze class. The maze consists of various segments. Information about * junctions and such are extracted when the objects are at positions * modulo segment size. This is just the classic way of doing it. * * @author Alexander Jean-Claude Bottema * */ final class Maze { /** * Maze appearance. */ final static String maze_orig[] = { "3777777777777437777777777774", "8111111111111881111111111118", "8137741377741881377741377418", "8280081800081881800081800828", "8157761577761561577761577618", "8111111111111111111111111118", "8137741341377777741341377418", "8157761881577437761881577618", "8111111881111881111881111118", "5777741857741881377681377776", "0000081837761561577481800000", "0000081880000000000881800000", "0000081880377997740881800000", "7777761560800000080561577777", "0000001000800000080001000000", "7777741340800000080341377777", "0000081880577777760881800000", "0000081880000000000881800000", "0000081880377777740881800000", "3777761560577437760561577774", "8111111111111881111111111118", "8137741377741881377741377418", "8157481577761561577761837618", "8211881111111111111111881128", "5741881341377777741341881376", "3761561881577437761881561574", "8111111881111881111881111118", "8137777657741881377657777418", "8157777777761561577777777618", "8111111111111111111111111118", "5777777777777777777777777776" }; /** * Maze logic data. */ final static String maze_junc[] = { "----------------------------", "-J****J*****J--J*****J****J-", "-*----*-----*--*-----*----*-", "-*-XX-*-XXX-*--*-XXX-*-XX-*-", "-*----*-----*--*-----*----*-", "-J****J**J**J**J**J**J****J-", "-*----*--*--------*--*----*-", "-*----*--*--------*--*----*-", "-J****J--J**J--J**J--J****J-", "------*-----*--*-----*------", "XXXXX-*-----*--*-----*-XXXXX", "XXXXX-*--J J J J--*-XXXXX", "XXXXX-*-- ---//--- --*-XXXXX", "------*-- - - --*------", " L J- -J R ", "------*-- - - --*------", "XXXXX-*-- -------- --*-XXXXX", "XXXXX-*--J J--*-XXXXX", "XXXXX-*-- -------- --*-XXXXX", "------*-- -------- --*------", "-J****J**J**J--J**J**J****J-", "-*----*-----*--*-----*----*-", "-*----*-----*--*-----*----*-", "-J*J--J**J**J**J**J**J--J*J-", "---*--*--*--------*--*--*---", "---*--*--*--------*--*--*---", "-J*J**J--J**J--J**J--J**J*J-", "-*----------*--*----------*-", "-*----------*--*----------*-", "-J**********J**J**********J-", "----------------------------" }; /** * The scene where the maze is used. */ Scene scene; /** * The Pacman canvas where the maze is used. */ PacmanCanvas canvas; /** * Reference to the sound server. */ SoundServer sound_server; /** * Mutuable maze data */ char maze_data[][]; char maze_junc_data[][]; /** * Image names of maze segments */ final static String image_names[] = { /* 0 */ "maze_empty.gif", /* 1 */ "maze_dot.gif", /* 2 */ "maze_bigdot.gif", /* 3 */ "maze_ul.gif", /* 4 */ "maze_ur.gif", /* 5 */ "maze_dl.gif", /* 6 */ "maze_dr.gif", /* 7 */ "maze_h.gif", /* 8 */ "maze_v.gif", /* 9 */ "maze_door.gif", null }; /** * Maze block size. Must be a power of 2. */ final static int block_size = 16; final static int half_block_size = block_size / 2; final static int block_mask = block_size - 1; /** * Food count is initialized to the intial number of dots in * the maze. */ final static int initial_food_cnt = 250; int food_cnt = initial_food_cnt; /** * Maze image segments. */ Image images[]; /** * Number of images. */ int no_of_imgs; /** * Maze width and height. */ int width, height; /** * Reference to the applet. */ Applet applet; /** * Directory to the images. */ String image_dir; /** * Retreat mode flag and counter. */ boolean retreat_mode; int mode_cnt = 0; int difficulty = 50; /** * Constructor */ Maze( Applet app, String dir, PacmanCanvas canv, SoundServer ss ) { MyObserver mo = new MyObserver( app ); applet = app; image_dir = dir; canvas = canv; sound_server = ss; URL url = app.getCodeBase(); height = maze_orig.length; width = maze_orig[0].length(); reset(); maze_junc_data = new char [height][]; for (int i = 0; i < height; i++) maze_junc_data[i] = maze_junc[i].toCharArray(); } /** * Read all needed images. */ public void readImages() { URL url = applet.getCodeBase(); MyObserver mo = new MyObserver( applet ); images = new Image[32]; no_of_imgs = 0; for (int i = 0; image_names[i] != null; i++) { Image im = applet.getImage( url, image_dir + image_names[i] ); mo.load( im ); if (im != null) images[no_of_imgs++] = im; scene.incLoadingBar( 1 ); } } /** * Reset maze. */ public void reset() { food_cnt = initial_food_cnt; maze_data = new char [height][]; for (int i = 0; i < height; i++) maze_data[i] = maze_orig[i].toCharArray(); } /** * Set scene. */ public void setScene( Scene sc ) { scene = sc; } /** * Paint partial maze (x,y,w,h) onto given image at * offset (x_off,y_off). */ public void paintOntoImage( Image im, int x_off, int y_off, int x, int y, int w, int h ) { int right = x + w; int bottom = y + h; Graphics g = im.getGraphics(); for (int i = y / block_size; i * block_size < bottom; i++) for (int j = x / block_size; j * block_size < right; j++) g.drawImage( images[maze_data[i][j]-'0'], j * block_size - x + x_off, i * block_size - y + y_off, null ); } /** * Paint maze data segment (x,y) in graphics image at offset * (x_o,y_o) and the additional x-offset x_off. */ public void paint( Graphics g, int x_o, int y_o, int x, int y, int x_off ) { g.drawImage( images[maze_data[y][x]-'0'], (x * block_size) + x_o + x_off, y_o + (y * block_size), null ); } /** * Return width of maze. */ public int getWidth() { return width * block_size; } /** * Return height of maze. */ public int getHeight() { return height * block_size; } /** * Return true iff the given coordinate is discrete, i.e. it * is at a well defined maze segment coordinate. */ public boolean isDiscrete( int x, int y ) { return x >= 0 && x < 432 && // Specials... ((x - half_block_size) & block_mask) == 0 && ((y - half_block_size) & block_mask) == 0; } /** * Return true iff the given value is discrete, i.e. it * is at a well defined maze segment boundary. */ public boolean isDiscrete( int v ) { return ((v - half_block_size) & block_mask) == 0; } /** * Return value in pixels given discrete value. */ public int discretePos( int v ) { return ((v - half_block_size) / block_size); } /** * Return true iff the site coordinate (x+dx,y+dy) in pixels * is valid. */ public boolean isSiteValid( int x, int y, int dx, int dy ) { int xx = (x - half_block_size) / block_size; int yy = (y - half_block_size) / block_size; // These are the specials exits in the maze if (xx + dx < 0 || xx + dx > 27) return true; return maze_data[ yy + dy ][ xx + dx ] <= '2'; } /** * Return true iff the coordinate (x,y) in pixels is a junction. */ public boolean isJunction( int x, int y ) { int xx = (x - half_block_size) / block_size; int yy = (y - half_block_size) / block_size; // Return false if outside maze if (xx < 0 || xx > 27) return false; char c = maze_junc_data[ yy ][ xx ]; return c == 'J' || c == 'L' || c == 'R'; } /** * Get junction data at coordinate (x,y) */ public char getJunction( int x, int y ) { int xx = (x - half_block_size) / block_size; int yy = (y - half_block_size) / block_size; // Return blank character if outside maze. if (xx < 0 || xx > 27) return ' '; return maze_junc_data[ yy ][ xx ]; } /** * (Try to) Eat maze dot at (x,y) */ public boolean eat( int x, int y ) { int x_d = x - half_block_size; int y_d = y - half_block_size; if ((x_d & block_mask) == 0 && (y_d & block_mask) == 0) { int mx = x_d / block_size; int my = y_d / block_size; if (mx >= 0 && mx < 28) { char c = maze_data[my][mx]; if (c == '1') sound_server.playEatPill(); if (c == '1' || c == '2') { canvas.incScore( (c == '1') ? 10 : 50 ); maze_data[my][mx] = '0'; scene.putMaze( mx, my ); if (--food_cnt == 0) canvas.setReadyMaze(); return c == '2'; } } } return false; } /** * Return value in pixels given maze segment value */ public int positionInPixels( int d ) { return d * block_size + half_block_size; } /** * Paint dot at maze segment coordinates (x,y). big denotes * big dot. */ public void paintDot( Graphics g, int x, int y, boolean big ) { g.drawImage( big ? images[2] : images[1], x, y, null ); } /** * Set difficulty. The maze keeps track of attack and retreat * modes of the ghosts. It is kind of strange why this * characteristics is put in the maze class, but after I pondered * the problem it is not such a bad idea after all. */ public void setDifficulty( int d ) { difficulty = d; } /** * Increment counter which determines retreat/attack mode of * ghosts. */ public void countMode() { if (++mode_cnt > 350) { mode_cnt = 0; int rnd = (int)(Math.random() * 100); boolean old_retreat_mode = retreat_mode; retreat_mode = rnd > difficulty; if (old_retreat_mode == retreat_mode && retreat_mode) retreat_mode = false; } } /** * Return true iff the game is in retreat mode. */ public boolean isRetreatMode() { return retreat_mode; } } /** * Actor class. All moving objects are considered actors on a scene. * These are their common characteristics that we can abstractly reason * about. * * @author Alexander Jean-Claude Bottema * */ abstract class Actor { /** * Directions. */ final static String dirs[] = { "east", "west", "north", "south" }; final static int no_of_dirs = 4; /** * References to canvas, scene and maze. */ PacmanCanvas canvas; Scene scene; Maze maze; /** * Coordinate, speed and direction. Speed and direction yield * slightly redundant information, but still convenient. */ public int x, y; public int dx, dy; int dir; /** * Set new direction. */ public void setDir( int new_dx, int new_dy ) { dx = new_dx; dy = new_dy; // Update 'dir' variable w.r.t. differential values if (dx > 0) dir = 0; else if (dx < 0) dir = 1; else if (dy < 0) dir = 2; else dir = 3; } /** * Set direction to the opposite. */ public void setDirOpposite() { dx = -dx; dy = -dy; setDir( dx, dy ); } /** * Set position coordinates. */ public void set( int xx, int yy ) { x = xx; y = yy; } /** * Clear, draw and run (iterate) actor. */ abstract public void clear(); abstract public void draw(); abstract public void run(); } /** * Chomp (Pacman) class. This class implements the methods of * the chomp. * * @author Alexander Jean-Claude Bottema * */ final class Chomp extends Actor { /** * Image prefix name */ final static String image_prefix = "pacman_"; /** * Number of chomps used for single direction animation. */ final static int no_of_chomps = 3; /** * Number of images required for a single direction. * Note that Pacman opens and closes his mouth continously. */ final static int no_of_images = no_of_chomps * 2 + 1; /** * Width and height in pixels of Pacman. */ final static int width = 27, height = 27; /** * Face/mouth offset. */ final static int face_off = width / 2 + 1; /** * No of images used when killed. */ final static int no_of_images_when_killed = 8; /** * Initial coordinates (in pixels) of Pacman. */ static int start_x, start_y; /** * Reference to the applet */ Applet applet; /** * Images of chomp of all directions. */ Image chomp[][] = new Image[no_of_dirs][no_of_chomps]; Image chomp_stop; /** * Images of chomp of all directions and animations. */ Image images[][] = new Image[no_of_dirs][no_of_images]; Image images_when_killed[] = new Image[no_of_images_when_killed]; /** * References to the ghosts */ Ghost ghost[] = new Ghost[4]; /** * Reference to the sound server */ SoundServer sound_server; /** * Wanted direction */ int want_dx = 0, want_dy = 0; /** * Image counter (used for animation.) */ int image_cnt = 0; /** * Used for animation when killed. */ int dead_count = 0; int barely_alive_count; boolean is_dead = false; /** * True if demo mode. */ boolean demo_mode; /** * Directory of images. */ String directory; /** * Constructor. */ Chomp( Applet app, String dir, Scene sc, Maze mz, SoundServer ss ) { directory = dir; applet = app; scene = sc; maze = mz; start_x = mz.positionInPixels( 13 ) + 8; start_y = mz.positionInPixels( 23 ); sound_server = ss; reset(); } /** * Read required images. */ public void readImages() { URL url = applet.getCodeBase(); MyObserver mo = new MyObserver( applet ); chomp_stop = applet.getImage( url, directory + "pacman_stop.gif" ); for (int d = 0; d < no_of_dirs; d++) { for (int i = 0; i < no_of_chomps; i++) { chomp[d][i] = applet.getImage( url, directory + "pacman_" + dirs[d] + "_"+i+".gif" ); mo.load( chomp[d][i] ); } scene.incLoadingBar( 1 ); } for (int d = 0; d < no_of_dirs; d++) for (int i = 0; i < no_of_images; i++) { if (i == 0) images[d][i] = chomp_stop; else if (i > 0 && i <= no_of_chomps) images[d][i] = chomp[d][i-1]; else if (i > no_of_chomps && i <= 2*no_of_chomps) images[d][i] = chomp[d][2*no_of_chomps-i]; } for (int i = 0; i < no_of_images_when_killed; i++) { images_when_killed[i] = applet.getImage( url, directory + "pacman_die_" + i + ".gif" ); mo.load( images_when_killed[i] ); scene.incLoadingBar( 1 ); } } /** * Reset coordinates, direction and differential speed. */ public void reset() { x = start_x; y = start_y; is_dead = false; setDir( 0, 0 ); changeDir( 0, 0 ); image_cnt = 0; } /** * Set ghost references. */ public void setGhosts( Ghost a, Ghost b, Ghost c, Ghost d ) { ghost[0] = a; ghost[1] = b; ghost[2] = c; ghost[3] = d; } /** * Set new wanted direction. */ public void changeDir( int dx, int dy ) { want_dx = dx; want_dy = dy; } /** * Clear chomp from scene. */ public void clear() { scene.clearImage( x - face_off, y - face_off, width, height ); } /** * Draw chomp onto scene. */ public void draw() { scene.putImage( images[dir][image_cnt], x - face_off, y - face_off ); } /** * Draw and iterate famous death sequence. */ public void deadDraw() { if (dead_count == no_of_images_when_killed + 1) { if (--barely_alive_count > 0) { draw(); sound_server.playNoBackground(); } else { sound_server.playDead(); dead_count--; } } else if (dead_count > 0) { scene.putImage( images_when_killed[ no_of_images_when_killed - dead_count ], x - face_off, y - face_off ); dead_count--; } } /** * Return true when death sequence is complete. */ public boolean isDeadFinished() { return dead_count == 0; } /** * Standard iteration of chomp. */ public void run() { int w = maze.getWidth(); if (x < -32) x = w + 32; if (x > w + 32) x = -32; if (maze.eat( x, y )) // Strength pill eaten { sound_server.playNormalBackground(); sound_server.playEatPowerPill(); for (int i = 0; i < 4; i++) ghost[i].becomeScared(); } if (dx != 0 || dy != 0) { x += (dx * 2); y += (dy * 2); if (++image_cnt >= no_of_images) image_cnt = 0; } if (maze.isDiscrete( x, y )) { if (want_dx != dx || want_dy != dy) { if (maze.isSiteValid( x, y, want_dx, want_dy )) setDir( want_dx, want_dy ); } if (!demo_mode && !maze.isSiteValid( x, y, dx, dy )) { setDir( 0, 0 ); changeDir( 0, 0 ); image_cnt = 0; } } else if (x == start_x && y == start_y) { if (want_dx == -1 || want_dx == 1) setDir( want_dx, 0 ); } } /** * This method just moves the chomp without regard to maze * and other surrounding elements. */ public void actualMove() { if (++image_cnt >= no_of_images) image_cnt = 0; x += (dx * 2); // Move slightly faster than usual. y += (dy * 2); } /** * Kill chomp. Initialize associated counters. */ public void kill() { dead_count = no_of_images_when_killed + 1; barely_alive_count = 10; is_dead = true; } /** * Return true if chomp is dead. */ public boolean isDead() { return is_dead; } /** * Return true if chomp is preparing his death. */ public boolean isPrepareDeath() { return barely_alive_count > 0; } /** * Set demo mode (on/off.) */ public void setDemoMode( boolean d ) { demo_mode = d; } } /** * Ghost class. This class implements the methods of ghosts * * @author Alexander Jean-Claude Bottema * */ final class Ghost extends Actor { /** * Image prefix name */ final static String image_prefix = "ghost_"; /** * Number of lower ghost parts used for animation. */ final static int no_of_lowers = 2; /** * Ghost colors. These are the original. */ final static int no_of_colors = 4; final static String colors[] = { "red", "yellow", "pink", "blue" }; /** * Ghost scores. */ final static int scores[] = { 200, 400, 800, 1600 }; /** * Width in pixels. */ final static int width = 27; /** * Height in pixels. */ final static int height_upper = 16, height_lower = 11; final static int height = height_upper + height_lower; /** * Face and eyes offsets */ final static int face_off = width / 2 + 1; final static int eyes_off = 4; /** * Scareness duration */ static int scared_duration = 400; /** * Directory of images */ static String directory; /** * Images */ static Image img_upper[][]; static Image img_lower[][]; static Image img_scared; static Image img_scores[]; static Image img_eyes[]; /** * Reference to the applet */ static Applet applet; /** * Reference to the Pacman canvas */ PacmanCanvas canvas; /** * Ghost state. 0 = Idle, 1 = Enter, 2 = Normal, * 3 = Eyes, 4 = Vanished */ int state; /** * Score counter. 0 = 200, 1 = 400, 2 = 800 and 3 = 1600 */ static int score_count = 0; /** * This is set to the appropriate score index of the ghost when eaten. */ int score_eaten = -1; /** * Ghost is scared if greater than zero. */ int scared_count; /** * How many times they bounce against the walls in the house * before they enter the maze. */ int idle_time; /** * Intelligence in percent. 100% denotes that no randomness is * used to guide the decisions of the ghost. Decisions are only * made at junction points and the ghost always make 90 degrees * turns. */ int iq; /** * Reference to the chomp (Pacman.) */ Chomp chomp; /** * Color of ghost. */ int color; /** * Counters for animation. */ int image_cnt = 0, switch_cnt = 0; /** * Coordinates of entrance to maze */ int door_x, door_y; /** * Reference to sound server */ SoundServer sound_server; /** * Constructor. */ Ghost( Applet app, String dir, Scene sc, Maze mz, int colr, Chomp ch, PacmanCanvas canv, SoundServer ss ) { MyObserver mo = new MyObserver( app ); // Store references to needed objects. applet = app; directory = dir; scene = sc; maze = mz; color = colr; chomp = ch; canvas = canv; sound_server = ss; // Position of door entrance. door_x = maze.positionInPixels(13) + 8; door_y = maze.positionInPixels(11); // Allocate memory for images if needed. if (img_upper == null) { img_upper = new Image[no_of_dirs][no_of_colors]; img_lower = new Image[no_of_colors][no_of_lowers]; img_eyes = new Image[no_of_dirs]; img_scores = new Image[scores.length]; } } /** * Read needed images. */ public void readImages() { URL url = applet.getCodeBase(); MyObserver mo = new MyObserver( applet ); for (int d = 0; d < no_of_dirs; d++) for (int i = 0; i < no_of_colors; i++) { img_upper[d][i] = applet.getImage( url, directory + image_prefix + colors[i] + "_" + dirs[d] + ".gif" ); mo.load( img_upper[d][i] ); scene.incLoadingBar( 1 ); } for (int c = 0; c < no_of_colors; c++) for (int i = 0; i < no_of_lowers; i++) { img_lower[c][i] = applet.getImage( url, directory + image_prefix + colors[c] + "_lower_" + i + ".gif" ); mo.load( img_lower[c][i] ); scene.incLoadingBar( 1 ); } for (int d = 0; d < no_of_dirs; d++) { img_eyes[d] = applet.getImage( url, directory + image_prefix + "eyes_" + dirs[d] + ".gif" ); mo.load( img_eyes[d] ); scene.incLoadingBar( 1 ); } for (int i = 0; i < scores.length; i++) { img_scores[i] = applet.getImage( url, directory + image_prefix + "score_" + scores[i] + ".gif" ); mo.load( img_scores[i] ); scene.incLoadingBar( 1 ); } img_scared = applet.getImage( url, directory + image_prefix + "scared.gif" ); mo.load( img_scared ); scene.incLoadingBar( 1 ); } /** * Reinitialize ghost parameters according to given values. */ public void init( int col, int row, int init_state, int init_idle, int init_iq ) { x = maze.positionInPixels( col ) + 8; y = maze.positionInPixels( row ); state = init_state; idle_time = init_idle; iq = init_iq; score_eaten = -1; scared_count = 0; } /** * Reset ghost parameters. */ public void reset() { state = 2; iq = 100; score_eaten = -1; scared_count = 0; } /** * Clear ghost from scene. */ public void clear() { scene.clearImage( x - face_off, y - face_off, width, height ); } /** * Return true if ghosts has been eaten. */ public boolean isEaten() { return score_eaten != -1; } /** * Draw ghost on scene. */ public void draw() { if (score_eaten >= 0) { scene.putImage( img_scores[score_eaten], x - face_off, y - eyes_off ); score_eaten = -1; return; } if (state == 4) // Dijkstra probably hates me for this :) return; if (state == 3) { scene.putImage( img_eyes[dir], x - face_off, y - eyes_off ); } else if (scared_count > 0) { if (scared_count >= 100 || (((scared_count / 8) & 1) == 0)) scene.putImage( img_scared, x - face_off, y - face_off ); } else { scene.putImage( img_upper[dir][color], x - face_off, y - face_off ); scene.putImage( img_lower[color][image_cnt], x - face_off, y - face_off + height_upper ); } } /** * Set state of ghost. State switching may alter speed of ghost * and its modulo coordinates must be rounded off properly. */ public void setState( int new_s ) { state = new_s; if (state == 3) { x = (x >> 2) << 2; y = (y >> 2) << 2; } } /** * Ghost vanishes. */ public void becomeVanished() { setState( 4 ); } /** * Ghost becomes scared. */ public void becomeScared() { if (state < 3) { scared_count = scared_duration; // Switch to opposite direction. if (state == 2) setDirOpposite(); } resetScoreCount(); } /** * Set scared duration. This is different for various levels. */ public void setScaredDuration(int dur) { scared_duration = dur; } /** * Switch image of lower part of ghost. */ public void switchLowers() { if (++image_cnt >= no_of_lowers) image_cnt = 0; } /** * Return true iff chomp touches ghost. */ public boolean touch() { if (state == 4) return false; int dist_x = x - chomp.x; int dist_y = y - chomp.y; return dist_x * dist_x + dist_y * dist_y <= 169; // 169 = 13*13 } /** * Set score on eaten ghost. */ public void setScore() { score_eaten = score_count; } /** * Increase score index counter. */ public void incScoreCount() { score_count++; } /** * Return true iff ghost is in "eyes" mode, i.e. it has recently * been eaten. */ public boolean isEyes() { return state == 3; } /** * Return true iff ghost is scared. */ public boolean isScared() { return scared_count > 0; } /** * Iterate ghost movement. This is a very large method, but * the state machine is quite complicated. */ public void run() { int w = maze.getWidth(); // Wrap ghost position if it tries to enter outside maze. // This is the case when the ghost enters the secret exists. if (x < -32) x = w + 32; if (x > w + 32) x = -32; if (state < 3) // i.e. not eyes { if (touch()) if (scared_count > 0) { sound_server.playEyesBackground(); setState( 3 ); // Ghost becomes eyes scared_count = 0; setScore(); canvas.incScore( scores[score_count] ); incScoreCount(); } else { // Pacman is killed by ghost !! chomp.kill(); } } // Switch image of lower part of ghost. This is the // standard ghost animation. if (++switch_cnt >= 3) { switch_cnt = 0; switchLowers(); } // Check if ghost coordinate is a discrete maze position. if (maze.isDiscrete( y )) { if (state == 0) // State == Idle { if (maze.discretePos(y) == 13 || maze.discretePos(y) == 15) { setDir( dx, -dy ); if (--idle_time == 0) setState( 1 ); // state == Enter } } else if (state == 1) // State == Enter { int center = maze.positionInPixels(13) + 8; if (x < center) setDir( 1, 0 ); else if (x > center) setDir( -1, 0 ); else setDir( 0, -1 ); if (maze.discretePos(y) == 11) { setState( 2 ); // state == Normal int rnd = (int)(Math.random() * 2); setDir( (rnd == 0) ? -1 : 1, 0 ); } } else if (maze.isDiscrete( x )) { // Is the ghost at a junction point? // If so, choose a new direction, but never the // direction the ghost came from. if (maze.isJunction( x, y )) { boolean north_blocked = false, south_blocked = false, east_blocked = false, west_blocked = false; // Block original direction. if (dx < 0) east_blocked = true; else if (dx > 0) west_blocked = true; else if (dy < 0) south_blocked = true; else north_blocked = true; if (!maze.isSiteValid( x, y, 0, -1 )) north_blocked = true; if (!maze.isSiteValid( x, y, 0, 1 )) south_blocked = true; if (!maze.isSiteValid( x, y, 1, 0 )) east_blocked = true; if (!maze.isSiteValid( x, y, -1, 0)) west_blocked = true; int new_dir; int rnd_val = (int)(Math.random() * 100); if (state < 3 && rnd_val > iq) { boolean blocked; do { blocked = false; new_dir = (int)(Math.random() * 4); if (new_dir == 4) new_dir = 3; switch (new_dir) { case 0: if (north_blocked) blocked = true; break; case 1: if (south_blocked) blocked = true; break; case 2: if (east_blocked) blocked = true; break; case 3: if (west_blocked) blocked = true; break; } } while (blocked); } else { // We score each direction. The direction with // highest score is chosen. 0 = north, 1 = south, // 2 = east and 3 = west. int score_0 = 0, score_1 = 0, score_2 = 0, score_3 = 0; // Target is either the coordinates of the chomp // or the coordinates of the house (when ghost // is in eyes mode.) int target_x, target_y; if (state < 3) { target_x = chomp.x; target_y = chomp.y; } else { target_x = door_x; target_y = door_y; } new_dir = -1; // Set scores according to simple heuristics. if (x < target_x) { score_0 += 20; score_1 += 20; score_2 += 100; score_3 -= 500; } if (x > target_x) { score_0 += 20; score_1 += 20; score_2 -= 500; score_3 += 100; } if (y < target_y) { score_0 -= 500; score_1 += 100; score_2 += 20; score_3 += 20; } if (y > target_y) { score_0 += 100; score_1 -= 500; score_2 += 20; score_3 += 20; } // If the ghost is scared the heuristic function // is reversed. if (state < 3 && (maze.isRetreatMode() || scared_count > 0)) { score_0 = -score_0; score_1 = -score_1; score_2 = -score_2; score_3 = -score_3; } char j = maze.getJunction(x, y); if (j == 'L') score_3 -= 50; else if (j == 'R') score_2 -= 50; if (north_blocked) score_0 -= 10000; if (south_blocked) score_1 -= 10000; if (east_blocked) score_2 -= 10000; if (west_blocked) score_3 -= 10000; // Choose the first valid direction with highest // score. Precisely three comparisons is needed // to determine the maximum value. if (score_0 > score_1) if (score_2 > score_3) if (score_0 > score_2) new_dir = 0; else new_dir = 2; else if (score_0 > score_3) new_dir = 0; else new_dir = 3; else if (score_2 > score_3) if (score_1 > score_2) new_dir = 1; else new_dir = 2; else if (score_1 > score_3) new_dir = 1; else new_dir = 3; } switch (new_dir) { case 0: setDir( 0, -1 ); break; // North case 1: setDir( 0, 1 ); break; // South case 2: setDir( 1, 0 ); break; // Eest case 3: setDir( -1, 0 ); break; // West } } } else if (state == 3) // Eyes { if (x == door_x) { if (y == door_y) setDir( 0, 1 ); else if (maze.isDiscrete( y ) && maze.discretePos( y ) == 14) { setDir( 0, -1 ); setState( 1 ); // Enter } } } } if (scared_count > 0) { x += dx; y += dy; if (--scared_count == 0) { sound_server.playNormalBackground(); x = (x >> 1) << 1; y = (y >> 1) << 1; } } else if (state < 3) { if ((y == 232 && (x < 64 || x > 368))) { x += dx; y += dy; } else { x += (dx << 1); y += (dy << 1); } } else { x += (dx << 2); y += (dy << 2); } } /** * Move ghost without regard to maze and other surrounding * elements. */ public void actualMove() { if (scared_count > 0) { x += dx; y += dy; } else { x += (dx << 1); y += (dy << 1); } if (++switch_cnt >= 3) { switch_cnt = 0; switchLowers(); } } /** * Reset ghost score index counter */ public void resetScoreCount() { score_count = 0; } /** * Set intelligence of ghost */ public void setIQ( int new_iq ) { iq = new_iq; } } /** * Bonus class. This class handles the bonus items that popup in * the center of the maze. * * @author Alexander Jean-Claude Bottema * */ final class Bonus extends Actor { /** * Image prefix name. */ final static String image_prefix = "bonus_"; /** * Number of different icons. */ final static int no_of_icons = 8; /** * The size of an icon in pixels. */ final static int width = 27, height = 27; /** * Offset to center of icon. */ static int off = width / 2 + 1; /** * Icon scores. */ final static int icon_scores[] = { 100, 300, 500, 700, 1000, 2000, 3000, 5000 }; /** * The maximum number of "popup" times on each level. */ final static int popup_times_max = 5; /** * The current icon index. */ int current_icon = 0; /** * Keeps track when to popup the next icon. */ int popup_count = 0; /** * Popup counter. The icons should only appear a fixed number of * times at each level. */ int popup_times = popup_times_max; /** * Duration of the icon/score of icon. */ int visible_icon_count = 0; int visible_score_count = 0; /** * References to other required objects. */ Applet applet; String directory; Chomp chomp; Scene scene; Maze maze; PacmanCanvas canvas; SoundServer sound_server; /** * Icon images and score images. */ Image icons[] = new Image[no_of_icons]; Image scores[] = new Image[no_of_icons]; /** * Constructor. */ Bonus( Applet app, String dir, Maze mz, Scene sc, Chomp ch, PacmanCanvas canv, SoundServer ss ) { MyObserver mo = new MyObserver( app ); applet = app; directory = dir; scene = sc; canvas = canv; chomp = ch; maze = mz; sound_server = ss; reset(); } /** * Set coordinates of bonus icon. */ public void reset() { set( maze.positionInPixels( 13 ) + 8, maze.positionInPixels( 17 ) ); } /** * Set icon number w.r.t. given level and initialize parameters. */ public void resetLevel( int level ) { current_icon = level / 2; if (current_icon > 7) current_icon = 7; visible_icon_count = 0; visible_score_count = 0; resetPopupCount(); popup_times = popup_times_max; } /** * Read bonus icon images. */ public void readImages() { URL url = applet.getCodeBase(); MyObserver mo = new MyObserver( applet ); for (int i = 0; i < no_of_icons; i++) { icons[i] = applet.getImage( url, directory + image_prefix + "icon_"+i+".gif" ); mo.load( icons[i] ); scores[i] = applet.getImage( url, directory + image_prefix + "score_"+i+".gif" ); mo.load( scores[i] ); scene.incLoadingBar( 1 ); } } /** * Clear bonus icon. */ public void clear() { scene.clearImage( x - off, y - off, width, height ); } /** * Draw bonus icon. */ public void draw() { if (visible_score_count > 0) scene.putImage( scores[current_icon], x - off, y - off ); else if (visible_icon_count > 0) scene.putImage( icons[current_icon], x - off, y - off ); } /** * Compute and set icon duration. */ public void enter() { visible_icon_count = 350 + (int)(Math.random() * 200); } /** * Bonus icon gets eaten. */ public void eat() { visible_icon_count = 0; visible_score_count = 70; canvas.incScore( icon_scores[current_icon] ); sound_server.playEatBonus(); } /** * Iterate. */ public void run() { if (visible_icon_count > 0) { if (chomp.y == y && chomp.x > x - 10 && chomp.x < x + 10) eat(); else visible_icon_count--; } else if (visible_score_count > 0) visible_score_count--; else { if (popup_count > 0) { if (--popup_count == 0) { if (popup_times > 0) { popup_times--; enter(); } resetPopupCount(); } } } } /** * Initialize duration when to popup next icon. */ public void resetPopupCount() { popup_count = 400 + (int)(Math.random() * 300); } } /** * SoundServer class. This class handles all sounds of the * game. * * @author Alexander Jean-Claude Bottema * */ final class SoundServer { /** * The maximum number of sounds. */ static final int max_no_of_sounds = 10; /** * Sound file names. These have been found on the Internet. */ static String sound_names[] = { "begin.au", "siren.au", "credit.au", "fruit.au", "power.au", "eat.au", "eyes.au", "dead.au" }; /** * Reference to the applet. */ Applet applet; /** * Sound clips. */ AudioClip sounds[]; /** * Sound background mode. The "normal" mode is pretty * hysterical in the Pacman game, but it is the original sound. * 0 = Silent, 1 = Normal, 2 = Power pill eaten, 3 = Eyes mode. */ int background_mode = 0; /** * Switch to new background mode according to this variable. */ int new_mode = 0; /** * We do not want the eyes background sound mode to sound * forever. */ int eyes_count = 0; /** * Boolean variables that denote various sounds. */ boolean power_pill_eaten = false; boolean pill_eaten = false; boolean ghost_eaten = false; boolean bonus_eaten = false; boolean play_intro = false; boolean play_dead = false; /** * Sound server can be activated/deactivated to turn sound * on/off. */ boolean activated = true; /** * Sound server is automatically disabled in demo mode. */ boolean demo_mode = false; /** * Constructor. */ public SoundServer(Applet app) { applet = app; } /** * Set demo mode on/off. */ public void setDemoMode( boolean mode ) { demo_mode = mode; } /** * Toggle sound on/off. */ public void toggleSound() { if (activated) off(); else on(); } /** * Boolean variable that is true iff sound server is fully initialized. */ boolean initialized = false; /** * Enable sound server */ public void on() { activated = true; } /** * Disable sound server */ public void off() { activated = false; background_mode = 0; silent(); } /** * Initialize sound server. We need the scene since the sound * server increases the loading bar upon audio clip loading. */ public void init( Scene scene ) { URL url = applet.getCodeBase(); sounds = new AudioClip[max_no_of_sounds]; for (int i = 0; i < sound_names.length; i++) { sounds[i] = applet.getAudioClip(url, "audio/" + sound_names[i]); scene.incLoadingBar( 4 ); try { Thread.sleep( 100 ); } catch (InterruptedException e) { } } initialized = true; } /** * Set play intro melody boolean variable. */ public void playIntro() { play_intro = true; } /** * Play no background. */ public void playNoBackground() { eyes_count = 0; new_mode = 0; background_mode = 0; silent(); } /** * Play normal background. */ public void playNormalBackground() { if (eyes_count == 0) new_mode = 1; } /** * Play "scared" background, i.e. when a power pill has been * eaten. */ public void playScaredBackground() { if (eyes_count == 0) new_mode = 2; } /** * Play eyes background. */ public void playEyesBackground() { eyes_count = 50; new_mode = 3; } /** * Play the sound of an eaten pill. */ public void playEatPill() { pill_eaten = true; } /** * Play the sound when a power pill is eaten. */ public void playEatPowerPill() { power_pill_eaten = true; } /** * Play the sound when a ghost is eaten. */ public void playEatGhost() { ghost_eaten = true; } /** * Play the sound when a bonus item is eaten. */ public void playEatBonus() { bonus_eaten = true; } /** * Play the sound when the chomp dies. */ public void playDead() { play_dead = true; } /** * Stop all sound clips and make everything silent. */ public void silent() { if (initialized) { int i; for (i = 0; i < 8; i++) sounds[i].stop(); } } /** * Iterate sound server. Read the attributes and play * the appropriate sounds. */ public void iterate() { if (eyes_count > 0) eyes_count--; if (activated && !demo_mode) { if (play_intro) sounds[0].play(); if (play_dead) sounds[7].play(); if (new_mode != background_mode) { background_mode = new_mode; silent(); switch (background_mode) { case 1: sounds[1].loop(); break; case 2: sounds[4].loop(); break; case 3: sounds[6].loop(); break; default: break; } } if (ghost_eaten) sounds[5].play(); else if (power_pill_eaten || bonus_eaten) sounds[3].play(); else if (pill_eaten) sounds[2].play(); } play_intro = false; play_dead = false; ghost_eaten = false; power_pill_eaten = false; pill_eaten = false; bonus_eaten = false; } } /** * SendScore class. This class handles score registrations. * * @author Alexander Jean-Claude Bottema * */ class SendScore extends Applet { /** * CGI script to activate. */ final static String http_addr = "http://www.csd.uu.se/~alexb/hiscores/pacman_reg.cgi"; /** * Score database name. */ final static String db_name = "pacman.scores"; /** * Name, score and level to registrate. */ static String name; static String score; static String level; /** * Constraints on name/score/level lengths. */ final static int max_db_len = 32; final static int max_name_len = 64; final static int max_score_len = 16; final static int max_level_len = 16; /** * Length of checksum in bytes. */ static int max_checksum_len = 2; /** * Total length of registration record in bytes. */ static int max_total_len = max_db_len + max_name_len + max_score_len + max_level_len + max_checksum_len; /** * Checksum bytes (prevents from cheating.) */ byte csum_0, csum_1; /** * Registration message to send. */ String url_msg; /** * Constructor. */ SendScore( String n, int sc, int lev ) { name = ""; for (int i = 0; i < n.length() && i < 60; i++) { if (n.charAt(i) == ' ') name += '_'; else if (n.charAt(i) != '\"') name += n.charAt(i); } if (name.length() == 0) return; score = (new Integer(sc)).toString(); level = (new Integer(lev)).toString(); computeChecksum(); makeMessage(); sendMessage(); } /** * Compute checksum. */ void computeChecksum() { int name_len = name.length(); int score_len = score.length(); csum_0 = 0; csum_1 = 0; for (int i = 0; i < name_len && i < 60; i++) csum_0 ^= (byte)(name.charAt(i)); for (int i = 0; i < score_len; i++) csum_1 ^= ((byte)(score.charAt(i)) | csum_0); csum_0 = (byte)((csum_0 & 0x1f) | 0x40); csum_1 = (byte)((csum_1 & 0x1f) | 0x40); } /** * Construct message. */ void makeMessage() { int db_len = db_name.length(); int name_len = name.length(); int score_len = score.length(); int level_len = level.length(); String msg = ""; for (int j = 0; j < db_len; j++) msg += db_name.charAt(j); msg += "$"; for (int j = 0; j < name_len; j++) msg += name.charAt(j); msg += "$"; for (int j = 0; j < score_len; j++) msg += score.charAt(j); msg += "$"; for (int j = 0; j < level_len; j++) msg += level.charAt(j); msg += "$"; msg += (char)csum_0; msg += (char)csum_1; url_msg = msg; // System.out.println("Url msg: " + url_msg); } /** * Send message to score registration server using * an ordinary URL-connection. */ void sendMessage() { try { URL url = new URL( http_addr + "?" + url_msg ); // System.out.println("Sending: "+ url.toString() ); URLConnection urlc = url.openConnection(); urlc.getInputStream(); } catch (IOException e) { System.out.println("Error: " + e); return; } } } /** * PacmanCanvas class. This class is the top level class of * the game. It interprets various key pressings and moves the * objects in a synchronized manner. * * @author Alexander Jean-Claude Bottema * */ final class PacmanCanvas extends Canvas { /** * Number of ghosts. It cannot be easily changed though! */ final static int no_of_ghosts = 4; /** * The speed counter is used to make the chomp/or the ghosts * move slightly faster or slower than usual. */ final static int speed_cnt_max = 10; /** * Normal idle time of a scene update. Should be increased if * the update process is expensive. */ static int normal_idle = 50; /** * These idle times denote Very Slow, Slow, Medium, Fast and Very Fast. */ static final int normal_idles[] = { 300, 100, 50, 20, 10, 0 }; /** * Partial update flag. Turn this on/off to switch update method. */ static boolean partial_update = false; /** * Directory of GIF-images */ String pixmaps_dir; /** * References to required objects. */ Applet applet; Maze maze; Chomp chomp; Ghost ghosts[]; Bonus bonus; Scene scene; SoundServer sound_server; /** * Demo mode on/off. */ boolean demo_mode = true; /** * Game started yes/no. */ boolean begin_game = false; /** * Instructions mode yes/no. */ boolean instructions_mode = true; int instructions_cnt = 0; /** * Parameters to handle difficulty of the game. These are * set by the setLevelProperties() method. */ int pacman_difficulty; int pacman_blinky_iq; int pacman_ghost_a_iq; int pacman_ghost_b_iq; int pacman_ghost_c_iq; int pacman_scared_duration; /** * Makes chomp becoming slightly slower/faster than the ghosts. */ int speed = -1; int speed_cnt = 0; /** * Ready text state counters. */ int ready_text = 2; int ready_maze = 0; /** * Initial score, level and lives. */ public int score = 0; int level = 1; int lives = 3; /** * Player name entered in the text field. */ TextField user_name; /** * Constructor. */ PacmanCanvas( Applet app, TextField name, SoundServer ss ) { pixmaps_dir = "pixmaps/"; ghosts = new Ghost[no_of_ghosts]; maze = new Maze( app, pixmaps_dir, this, ss ); scene = new Scene( app, pixmaps_dir, maze, this ); reshape( 0, 0, scene.getWidth(), scene.getHeight() ); applet = app; sound_server = ss; user_name = name; } /** * Return state of update method. */ public boolean isPartialUpdate() { return partial_update; } /** * Since the standard API merges multiple repaints into larger * ones, we make our own repainting method that avoid this annoying * behavior. */ public void special_repaint( int x, int y, int w, int h ) { Graphics g = getGraphics().create( x, y, w, h ); g.translate( -x, -y ); scene.update( g ); } /** * Set level properties and difficulty. */ public void setLevelProperties() { pacman_difficulty = level * 10 + 40; if (pacman_difficulty > 100) pacman_difficulty = 100; pacman_blinky_iq = 95 + level; if (pacman_blinky_iq > 100) pacman_blinky_iq = 100; pacman_ghost_a_iq = 70 + level * 5; if (pacman_ghost_a_iq > 95) pacman_ghost_a_iq = 95; pacman_ghost_b_iq = pacman_ghost_a_iq; pacman_ghost_c_iq = 50 + level * 5; if (pacman_ghost_c_iq > 80) pacman_ghost_c_iq = 80; switch (level % 3) { case 0: pacman_scared_duration = 175; break; case 1: pacman_scared_duration = 450; break; case 2: pacman_scared_duration = 325; break; } maze.setDifficulty( pacman_difficulty ); ghosts[0].setScaredDuration( pacman_scared_duration ); ghosts[0].setIQ( pacman_blinky_iq ); ghosts[1].setIQ( pacman_ghost_a_iq ); ghosts[2].setIQ( pacman_ghost_b_iq ); ghosts[3].setIQ( pacman_ghost_c_iq ); speed = 1 - (level - 1) / 3; if (speed < -1) speed = -1; bonus.resetLevel( level ); } /** * Set ready maze (makes the maze flashing when one has eaten * all its pills.) */ public void setReadyMaze() { ready_maze = 10; if (!demo_mode) level++; } /** * Increase score. */ public void incScore( int inc ) { if (!demo_mode) { int old_score = score; score += inc; if ((old_score < 10000 && score >= 10000) || (old_score < 50000 && score >= 50000) || (old_score < 100000 && score >= 100000) || (old_score < 500000 && score >= 500000)) { lives++; scene.drawLives( lives ); } scene.drawScore( score ); } } /** * Initialize Pacman canvas. */ public void init() { pixmaps_dir = "pixmaps/"; demo_mode = true; begin_game = false; instructions_mode = true; speed = -1; speed_cnt = 0; ready_text = 2; ready_maze = 0; score = 0; level = 1; lives = 3; instructions_cnt = 0; chomp = new Chomp( applet, pixmaps_dir, scene, maze, sound_server ); chomp.readImages(); for (int i = 0; i < no_of_ghosts; i++) ghosts[i] = new Ghost( applet, pixmaps_dir, scene, maze, i, chomp, this, sound_server ); ghosts[0].readImages(); chomp.setGhosts( ghosts[0], ghosts[1], ghosts[2], ghosts[3] ); bonus = new Bonus( applet, pixmaps_dir, maze, scene, chomp, this, sound_server ); bonus.readImages(); maze.setScene( scene ); maze.readImages(); sound_server.init( scene ); reset(); } /** * Reset Pacman canvas. */ public void reset() { chomp.reset(); bonus.reset(); bonus.resetLevel( level ); // Set initial coordinates of ghosts. ghosts[0].init( 13, 11, 2, 0, 100 ); ghosts[1].init( 11, 14, 0, 14, 70 ); ghosts[2].init( 13, 14, 0, 7, 50 ); ghosts[3].init( 15, 14, 0, 21, 30 ); // Set initial directions of ghosts. ghosts[0].setDir( -1, 0 ); ghosts[1].setDir( 0, -1 ); ghosts[2].setDir( 0, 1 ); ghosts[3].setDir( 0, -1 ); clearActors(); scene.putMaze(); scene.drawScore( score ); scene.drawLives( lives ); chomp.setDemoMode( demo_mode ); if (demo_mode) { chomp.changeDir( 1, 0 ); chomp.setDir( 1, 0 ); } } /** * Repaint canvas = repaint scene. */ public void paint(Graphics g) { scene.repaint(); } /** * Set parameters to begin game. */ public void beginGame() { sound_server.setDemoMode( false ); scene.clearAll(); level = 1; lives = 3; // Change back to 3 score = 0; // Change back to 0 demo_mode = false; instructions_mode = false; clearActors(); maze.reset(); reset(); ready_text = 8; begin_game = false; } /** * Set parameters for demonstration. */ public void beginDemo() { sound_server.setDemoMode( true ); scene.clearAll(); level = 1; lives = 3; demo_mode = true; clearActors(); maze.reset(); reset(); ready_text = 2; } /** * Set parameters for instructions. */ public void beginInstructions() { sound_server.setDemoMode( true ); instructions_mode = true; instructions_cnt = 0; } /** * The general event handler. */ public boolean handleEvent(Event e) { // Request focus if mouse button is pressed in the canvas area. switch (e.id) { case Event.MOUSE_DOWN: requestFocus(); return true; } // Direction of chomp can be switched by various // sets of keys. if (!demo_mode && !instructions_mode) switch (e.id) { case Event.KEY_ACTION: case Event.KEY_PRESS: switch (e.key) { case Event.RIGHT: case '6': case 'd': case 'D': case 'j': case 'J': chomp.changeDir( 1, 0 ); return true; case Event.LEFT: case 'a': case 'A': case '4': case 'g': case 'G': chomp.changeDir( -1, 0 ); return true; case Event.UP: case 'w': case 'W': case '8': case 'y': case 'Y': chomp.changeDir( 0, -1 ); return true; case Event.DOWN: case 'x': case 'X': case '2': case 'n': case 'N': chomp.changeDir( 0, 1 ); return true; default: return super.handleEvent(e); } default: return super.handleEvent(e); } else { // SPACE to begin game. if ((e.id == Event.KEY_ACTION || e.id == Event.KEY_PRESS) && e.key == ' ') { begin_game = true; return true; } else return super.handleEvent(e); } } /** * Handle actions, e.g. changing game of speed. */ public boolean action( Event e, Object arg ) { // Check for checkboxes. if (arg instanceof Boolean) { String cbs = ((Checkbox)(e.target)).getLabel(); if (cbs.equals("Sound")) { if (((Boolean)arg).booleanValue()) sound_server.on(); else sound_server.off(); return true; } if (cbs.equals("Partial Update")) { partial_update = ((Boolean)arg).booleanValue(); return true; } } // Check for choice of game speed. if (e.target instanceof Choice) { normal_idle = normal_idles[ ((Choice)(e.target)).getSelectedIndex() ]; // System.out.println("Speed is now set to " + normal_idle); return true; } // Check for game control buttons if (e.target instanceof Button) { String l = ((Button)e.target).getLabel(); if (l.equals("U")) { chomp.changeDir( 0, -1 ); return true; } if (l.equals("D")) { chomp.changeDir( 0, 1 ); return true; } if (l.equals("L")) { chomp.changeDir( -1, 0 ); return true; } if (l.equals("R")) { chomp.changeDir( 1, 0 ); return true; } if (l.equals("Start")) { begin_game = true; return true; } } return false; } /** * Chomp strategy in demonstration mode. Currently purely random, but * it can be easily changed. */ public void demoDecision() { if (maze.isDiscrete( chomp.x, chomp.y ) && maze.isJunction( chomp.x, chomp.y )) { boolean north_blocked = false, south_blocked = false, east_blocked = false, west_blocked = false; if (chomp.dx < 0) east_blocked = true; else if (chomp.dx > 0) west_blocked = true; else if (chomp.dy < 0) south_blocked = true; else north_blocked = true; if (!maze.isSiteValid( chomp.x, chomp.y, 0, -1 )) north_blocked = true; if (!maze.isSiteValid( chomp.x, chomp.y, 0, 1 )) south_blocked = true; if (!maze.isSiteValid( chomp.x, chomp.y, 1, 0 )) east_blocked = true; if (!maze.isSiteValid( chomp.x, chomp.y, -1, 0)) west_blocked = true; int rnd_val; boolean blocked; do { blocked = false; rnd_val = (int)(Math.random() * 4); if (rnd_val == 4) rnd_val = 3; switch (rnd_val) { case 0: if (north_blocked) blocked = true; break; case 1: if (east_blocked) blocked = true; break; case 2: if (west_blocked) blocked = true; break; case 3: if (south_blocked) blocked = true; break; } } while (blocked); switch (rnd_val) { case 0: chomp.changeDir( 0, -1 ); chomp.setDir( 0, -1 ); break; case 1: chomp.changeDir( 1, 0 ); chomp.setDir( 1, 0 ); break; case 2: chomp.changeDir( -1, 0 ); chomp.setDir( -1, 0 ); break; case 3: chomp.changeDir( 0, 1 ); chomp.setDir( 0, 1 ); break; } } } /** * "Ready" text iterator. */ public void readyTextRun() { drawActors(); String s; int xpos; if (demo_mode) { s = "GAME OVER"; xpos = maze.positionInPixels( 9 ); } else { s = "READY!"; xpos = maze.positionInPixels( 11 ); } if (ready_text == 8) sound_server.playIntro(); if (ready_text > 1) scene.putText( s, xpos, maze.positionInPixels(17) + 8 ); else { scene.clearText( s, xpos, maze.positionInPixels(17) + 8 ); sound_server.playNormalBackground(); } repaint_scene(); ready_text--; setLevelProperties(); } /** * Iterator when chomp is killed. */ public int chompDeadRun() { clearActors(); chomp.deadDraw(); if (chomp.isPrepareDeath()) { for (int i = 0; i < no_of_ghosts; i++) { ghosts[i].draw(); ghosts[i].switchLowers(); } } repaint_scene(); return 150; } /** * Draw instructions. */ public void drawSomeInstructions() { scene.paintDot( scene.getWidth() / 3, scene.getHeight() / 2, false ); scene.paintDot( scene.getWidth() / 3, scene.getHeight() / 2 + 24, true ); scene.putText( " 10 PTS", scene.getWidth() / 3 + 10, scene.getHeight() / 2 - 16 ); scene.putText( " 50 PTS", scene.getWidth() / 3 + 10, scene.getHeight() / 2 + 10 ); scene.putText( "UP", 13*16-8, 20*20 ); scene.putText( "Y", 13*16, 21*20 ); scene.putText( "LEFT G J RIGHT", 7*16, 22*20 ); scene.putText( "N", 13*16, 23*20 ); scene.putText( "DOWN", 12*16-8, 24*20 ); scene.putText( "Alexander Jean-Claude Bottema", 40, 16 ); scene.putText( "presents", 10*16, 36 ); scene.drawLogo( 100, 80 ); scene.putText( "Press SPACE to begin", 100, scene.getHeight() - 40 ); } /** * Iterator for instructions. This is a quite complex state * machine. */ public int instructionsRun() { int idleTime = normal_idle; if (instructions_cnt == 0) { instructions_cnt++; scene.clearAll(); int sx = scene.getWidth() + 50; int sy = scene.getHeight() / 3; chomp.set( sx, sy ); for (int i = 0; i < no_of_ghosts; i++) { ghosts[i].reset(); ghosts[i].set( sx + (i+2)*40, sy ); } chomp.setDir( -1, 0 ); chomp.changeDir( -1, 0 ); for (int i = 0; i < no_of_ghosts; i++) ghosts[i].setDir( -1, 0 ); scene.paintDot( 10, sy + 24, true ); scene.setBackground(); drawSomeInstructions(); } if (instructions_cnt == 1 && chomp.x < 30) { instructions_cnt++; chomp.setDir( 1, 0 ); chomp.changeDir( 1, 0 ); for (int i = 0; i < no_of_ghosts; i++) { ghosts[i].setScaredDuration( 10000 ); ghosts[i].becomeScared(); } scene.clearAll(); drawSomeInstructions(); } if (instructions_cnt == 2 && chomp.x > scene.getWidth() + 50) { instructions_mode = false; beginDemo(); return 1000; } for (int i = 0; i < no_of_ghosts; i++) { if (ghosts[i].touch()) { ghosts[i].setScore(); ghosts[i].becomeVanished(); ghosts[i].incScoreCount(); idleTime = 1000; } } clearActors(); ++speed_cnt; if (speed_cnt < speed_cnt_max) chomp.actualMove(); if (speed_cnt >= speed_cnt_max) speed_cnt = 0; for (int i = 0; i < no_of_ghosts; i++) ghosts[i].actualMove(); drawActors(); return idleTime; } /** * Clear all actors. */ public void clearActors() { if (!instructions_mode) bonus.clear(); chomp.clear(); for (int i = 0; i < no_of_ghosts; i++) ghosts[i].clear(); } /** * Draw all actors. */ public void drawActors() { if (!instructions_mode) bonus.draw(); chomp.draw(); for (int i = 0; i < no_of_ghosts; i++) ghosts[i].draw(); } /** * Main iterator. */ public synchronized int run() { if (begin_game) { beginGame(); return 200; } if (instructions_mode) { int w = instructionsRun(); repaint_scene(); return w; } if (ready_maze > 0) { sound_server.playNoBackground(); if ((ready_maze & 1) == 0) scene.putBackground(); else scene.clearBackground(); if (--ready_maze == 0) { maze.reset(); reset(); ready_text = 2; } drawActors(); repaint_scene(); return 250; } if (chomp.isDead()) { if (!chomp.isDeadFinished()) return chompDeadRun(); else { if (!demo_mode) { if (--lives == 0) { new SendScore( user_name.getText(), score, level ); beginInstructions(); return 1000; } } else { beginInstructions(); return 2000; } reset(); repaint_scene(); ready_text = 2; } } if (ready_text > 0) { readyTextRun(); return (ready_text == 1) ? 2000 : 500; } // Normal move maze.countMode(); clearActors(); boolean eaten = false; ++speed_cnt; if (speed_cnt < speed_cnt_max || speed <= 0) for (int i = 0; i < no_of_ghosts; i++) { ghosts[i].run(); if (ghosts[i].isEaten()) { eaten = true; break; } } if (!instructions_mode) bonus.run(); if (!eaten) { if (demo_mode) demoDecision(); if (speed_cnt < speed_cnt_max || speed >= 0) chomp.run(); } if (speed_cnt >= speed_cnt_max) speed_cnt = 0; drawActors(); if (!isPartialUpdate()) repaint_scene(); boolean scared_sound = false; for (int i = 0; i < no_of_ghosts; i++) if (ghosts[i].isScared()) { scared_sound = true; break; } if (scared_sound) sound_server.playScaredBackground(); else sound_server.playNormalBackground(); if (eaten) sound_server.playEatGhost(); return eaten ? 1000 : normal_idle; } /** * Repaint scene */ public void repaint_scene() { repaint( 0, 0, scene.getWidth(), scene.getHeight() ); } /** * Graphics update. */ public synchronized void update( Graphics g ) { scene.update( g ); } } /** * Pacman class. This class is the top level class of the entire applet. * * @author Alexander Jean-Claude Bottema * */ final public class Pacman extends Applet implements Runnable { boolean time_to_die = false; static PacmanCanvas canvas = null; TextField text; static SoundServer sound_server; /** * Initialize applet. */ public void init() { time_to_die = false; setLayout( new BorderLayout() ); text = new TextField(); if (sound_server != null) sound_server.silent(); sound_server = new SoundServer( this ); if (canvas == null) canvas = new PacmanCanvas( this, text, sound_server ); Panel p = new Panel(); Checkbox snd = new Checkbox( "Sound" ); snd.setState( true ); p.add( snd ); p.add( new Checkbox( "Partial Update" ) ); Choice c = new Choice(); c.addItem( "Very Slow" ); c.addItem( "Slow" ); c.addItem( "Medium" ); c.addItem( "Fast" ); c.addItem( "Very Fast" ); c.addItem( "Ultimate" ); c.select( "Medium" ); Panel ps = new Panel(); ps.add( new Label( "Speed: ") ); ps.add( c ); p.add( ps ); Panel status = new Panel(); status.setLayout( new BorderLayout() ); status.add( "North", text ); status.add( "South", p ); // Game controls Panel controls = new Panel(); controls.setLayout( new FlowLayout( FlowLayout.CENTER, 40, 0 ) ); Panel dirs = new Panel(); // Fill empty slots with empty panels dirs.setLayout( new GridLayout( 3, 3 ) ); dirs.add( new Panel() ); dirs.add( new Button( "U" ) ); dirs.add( new Panel() ); dirs.add( new Button( "L" ) ); dirs.add( new Panel() ); dirs.add( new Button( "R" ) ); dirs.add( new Panel() ); dirs.add( new Button( "D" ) ); controls.add( new Button( "Start" ) ); controls.add( dirs ); add( "North", status ); add( "Center", canvas ); add( "South", controls ); } /** * Main applet iterator. */ public synchronized void run() { show(); canvas.init(); while (!time_to_die) { int idleTime = canvas.run(); // System.out.println("Idle: " + idleTime); sound_server.iterate(); if (idleTime > 0) { try { Thread.sleep( idleTime ); } catch (InterruptedException e) { } } } } /** * Redirect all actions to the Pacman canvas. */ public boolean action( Event e, Object arg ) { return canvas.action( e, arg ); } /** * Start applet. */ public void start() { time_to_die = false; (new Thread(this)).start(); } /** * Stop applet. */ public void stop() { time_to_die = true; sound_server.silent(); destroy(); } }