diff --git a/.github/.keep b/.github/.keep new file mode 100644 index 00000000..e69de29b diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 09c58b95..e175b5ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,8 +5,6 @@ on: # Runs on pushes targeting the default branch push: branches: [main] - pull_request: - branches: [main] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/README.md b/README.md index e240001b..041961b9 100644 --- a/README.md +++ b/README.md @@ -1 +1,68 @@ -REPLACE THIS WITH A DESCRIPTION OF YOUR GAME (in the README.md file). +# Boolean Bonanza + +# Team Color + +cyan + +# Developers + +- Alex Trexler (alextrex@udel.edu) +- Michael Murphy (murphyme@udel.edu) +- Clay Wilfong (clayw@udel.edu) + +# Blurb + +Boolean Bonanza is a dynamic puzzle game where players are presented with a grid of squares labeled with boolean values and operators such as True, False, And, Or and Not. The goal of the game is to click and move these blocks throughout the rows and columns of the grid to craft a boolean statement that evaluates to True. After successfully making a row or column that evaluates to True, the row or column will be cleared from the grid and the player will add points to their score. Boolean Bonanza is designed to offer a fun way to learn boolean logic. + +# Basic Instructions + +Starting the Game + +- Select one of the three options on the menu: + - Tutorial (An interactive walkthrough of the game) + - Play 3 x 3 (A game mode that presents a 3 x 3 grid of boolean blocks and operators) + - Play 5 x 5 (A game mode that presents a 5 x 5 grid of boolean blocks and operators) + +Gameplay Instructions + +- Interact with the grid by clicking on one of the blocks. (The block will be tinted indicating you have clicked on it) +- Click on another block to swap its position with your already selected block. +- Maneuver the blocks in this manner to make a row or column that demonstrates a boolean statement that evaluates to True such as: + - T AND T + - F OR T + - T AND T OR F + +Scoring / Time + +- After making a row or column that evaluates to True, the row or column will disappear and add points to the player's score. +- The player must make as many True boolean statements as they can before the time limit in the corner of the screen runs out and ends the game + +# Screenshot + +![screenshot](https://github.com/UD-S24-CISC374/final-project-cyan/blob/main/docs/large.png?raw=true) + +# Gameplay Video + +https://www.youtube.com/embed/qgengQMsyD0?si=A9qvKEe1d8bq5z-h + +# Educational Game Design Document + +Link to our [egdd](https://github.com/UD-S24-CISC374/final-project-cyan/blob/main/docs/egdd.md) + +# Credits + +Boolean block textures: Alex Trexler + +Block break sound: Cork by Blender Foundation (submitted by Lamoot) https://opengameart.org/content/cork + +Gameplay background music: Contemplation by Bart https://opengameart.org/content/contemplation + +Button press sound: Cloud Click by Mobeyee Sounds https://mobeyee.com + +Main menu music: Puzzle Menu by caret7 https://opengameart.org/content/puzzle-menu + +Block Break Animations: AlotofImpacts - 5 Frame Impacts by JoesAlotofthings https://opengameart.org/users/joesalotofthings + + + + diff --git a/assets/audio/effects/click.mp3 b/assets/audio/effects/click.mp3 new file mode 100644 index 00000000..ab99aac6 Binary files /dev/null and b/assets/audio/effects/click.mp3 differ diff --git a/assets/audio/effects/cork.mp3 b/assets/audio/effects/cork.mp3 new file mode 100644 index 00000000..2849e323 Binary files /dev/null and b/assets/audio/effects/cork.mp3 differ diff --git a/assets/audio/music/contemplation.mp3 b/assets/audio/music/contemplation.mp3 new file mode 100644 index 00000000..7c7c2ba7 Binary files /dev/null and b/assets/audio/music/contemplation.mp3 differ diff --git a/assets/audio/music/puzzlemenu.ogg b/assets/audio/music/puzzlemenu.ogg new file mode 100644 index 00000000..dc1a6e25 Binary files /dev/null and b/assets/audio/music/puzzlemenu.ogg differ diff --git a/assets/backgrounds/3x3backplate.png b/assets/backgrounds/3x3backplate.png new file mode 100644 index 00000000..5851990e Binary files /dev/null and b/assets/backgrounds/3x3backplate.png differ diff --git a/assets/backgrounds/5x5backplate.png b/assets/backgrounds/5x5backplate.png new file mode 100644 index 00000000..49000bc1 Binary files /dev/null and b/assets/backgrounds/5x5backplate.png differ diff --git a/assets/blocks/And.png b/assets/blocks/And.png new file mode 100644 index 00000000..4676c77a Binary files /dev/null and b/assets/blocks/And.png differ diff --git a/assets/blocks/False.png b/assets/blocks/False.png new file mode 100644 index 00000000..c0e48a9b Binary files /dev/null and b/assets/blocks/False.png differ diff --git a/assets/blocks/Not.png b/assets/blocks/Not.png new file mode 100644 index 00000000..7a2ef1d8 Binary files /dev/null and b/assets/blocks/Not.png differ diff --git a/assets/blocks/Or.png b/assets/blocks/Or.png new file mode 100644 index 00000000..faf75187 Binary files /dev/null and b/assets/blocks/Or.png differ diff --git a/assets/blocks/True.png b/assets/blocks/True.png new file mode 100644 index 00000000..59322228 Binary files /dev/null and b/assets/blocks/True.png differ diff --git a/assets/effects/effects/ImpactMetal8DkBlue .png b/assets/effects/effects/ImpactMetal8DkBlue .png new file mode 100644 index 00000000..a6d12b22 Binary files /dev/null and b/assets/effects/effects/ImpactMetal8DkBlue .png differ diff --git a/assets/effects/effects/ImpactMetal8Green .png b/assets/effects/effects/ImpactMetal8Green .png new file mode 100644 index 00000000..1141ef4a Binary files /dev/null and b/assets/effects/effects/ImpactMetal8Green .png differ diff --git a/assets/effects/effects/ImpactMetal8Purple .png b/assets/effects/effects/ImpactMetal8Purple .png new file mode 100644 index 00000000..24fbb869 Binary files /dev/null and b/assets/effects/effects/ImpactMetal8Purple .png differ diff --git a/assets/effects/effects/ImpactMetal8Red .png b/assets/effects/effects/ImpactMetal8Red .png new file mode 100644 index 00000000..93097db1 Binary files /dev/null and b/assets/effects/effects/ImpactMetal8Red .png differ diff --git a/assets/effects/effects/ImpactMetal8Yellow .png b/assets/effects/effects/ImpactMetal8Yellow .png new file mode 100644 index 00000000..535b1b6e Binary files /dev/null and b/assets/effects/effects/ImpactMetal8Yellow .png differ diff --git a/assets/effects/effects/PhysicalImpact01DkBlue 2.png b/assets/effects/effects/PhysicalImpact01DkBlue 2.png new file mode 100644 index 00000000..9fec8357 Binary files /dev/null and b/assets/effects/effects/PhysicalImpact01DkBlue 2.png differ diff --git a/assets/effects/effects/PhysicalImpact01DkBlue 3.png b/assets/effects/effects/PhysicalImpact01DkBlue 3.png new file mode 100644 index 00000000..548fb5b5 Binary files /dev/null and b/assets/effects/effects/PhysicalImpact01DkBlue 3.png differ diff --git a/assets/effects/effects/PhysicalImpact01Green 2.png b/assets/effects/effects/PhysicalImpact01Green 2.png new file mode 100644 index 00000000..e6748b23 Binary files /dev/null and b/assets/effects/effects/PhysicalImpact01Green 2.png differ diff --git a/assets/effects/effects/PhysicalImpact01Green 3.png b/assets/effects/effects/PhysicalImpact01Green 3.png new file mode 100644 index 00000000..df172638 Binary files /dev/null and b/assets/effects/effects/PhysicalImpact01Green 3.png differ diff --git a/assets/effects/effects/PhysicalImpact01Purple 2.png b/assets/effects/effects/PhysicalImpact01Purple 2.png new file mode 100644 index 00000000..cadfb594 Binary files /dev/null and b/assets/effects/effects/PhysicalImpact01Purple 2.png differ diff --git a/assets/effects/effects/PhysicalImpact01Purple 3.png b/assets/effects/effects/PhysicalImpact01Purple 3.png new file mode 100644 index 00000000..c2ad0ac1 Binary files /dev/null and b/assets/effects/effects/PhysicalImpact01Purple 3.png differ diff --git a/assets/effects/effects/PhysicalImpact01Red 2.png b/assets/effects/effects/PhysicalImpact01Red 2.png new file mode 100644 index 00000000..49042b55 Binary files /dev/null and b/assets/effects/effects/PhysicalImpact01Red 2.png differ diff --git a/assets/effects/effects/PhysicalImpact01Red 3.png b/assets/effects/effects/PhysicalImpact01Red 3.png new file mode 100644 index 00000000..16497773 Binary files /dev/null and b/assets/effects/effects/PhysicalImpact01Red 3.png differ diff --git a/assets/effects/effects/PhysicalImpact01Yellow 2.png b/assets/effects/effects/PhysicalImpact01Yellow 2.png new file mode 100644 index 00000000..57fd2e15 Binary files /dev/null and b/assets/effects/effects/PhysicalImpact01Yellow 2.png differ diff --git a/assets/effects/effects/PhysicalImpact01Yellow 3.png b/assets/effects/effects/PhysicalImpact01Yellow 3.png new file mode 100644 index 00000000..704d9a9b Binary files /dev/null and b/assets/effects/effects/PhysicalImpact01Yellow 3.png differ diff --git a/assets/effects/effects/blue.png b/assets/effects/effects/blue.png new file mode 100644 index 00000000..98641348 Binary files /dev/null and b/assets/effects/effects/blue.png differ diff --git a/assets/effects/effects/green.png b/assets/effects/effects/green.png new file mode 100644 index 00000000..04e8276b Binary files /dev/null and b/assets/effects/effects/green.png differ diff --git a/assets/effects/effects/purple.png b/assets/effects/effects/purple.png new file mode 100644 index 00000000..ef3b9153 Binary files /dev/null and b/assets/effects/effects/purple.png differ diff --git a/assets/effects/effects/red.png b/assets/effects/effects/red.png new file mode 100644 index 00000000..f0ddc818 Binary files /dev/null and b/assets/effects/effects/red.png differ diff --git a/assets/effects/effects/yellow.png b/assets/effects/effects/yellow.png new file mode 100644 index 00000000..89789ce2 Binary files /dev/null and b/assets/effects/effects/yellow.png differ diff --git a/assets/favicon.ico b/assets/favicon.ico index f268a177..74e0e62d 100644 Binary files a/assets/favicon.ico and b/assets/favicon.ico differ diff --git a/assets/img/phaser-logo.png b/assets/img/phaser-logo.png deleted file mode 100644 index d1fc105c..00000000 Binary files a/assets/img/phaser-logo.png and /dev/null differ diff --git a/assets/menu/MainMenuButton.png b/assets/menu/MainMenuButton.png new file mode 100644 index 00000000..21bf4043 Binary files /dev/null and b/assets/menu/MainMenuButton.png differ diff --git a/assets/menu/PauseButton.png b/assets/menu/PauseButton.png new file mode 100644 index 00000000..e77f8ae0 Binary files /dev/null and b/assets/menu/PauseButton.png differ diff --git a/assets/menu/PlayAgainButton.png b/assets/menu/PlayAgainButton.png new file mode 100644 index 00000000..ccc8a4e9 Binary files /dev/null and b/assets/menu/PlayAgainButton.png differ diff --git a/assets/menu/ResumeButton.png b/assets/menu/ResumeButton.png new file mode 100644 index 00000000..76a013d8 Binary files /dev/null and b/assets/menu/ResumeButton.png differ diff --git a/assets/menu/menuBackplate.png b/assets/menu/menuBackplate.png new file mode 100644 index 00000000..6985be79 Binary files /dev/null and b/assets/menu/menuBackplate.png differ diff --git a/assets/menu/play3Button.png b/assets/menu/play3Button.png new file mode 100644 index 00000000..f007ace9 Binary files /dev/null and b/assets/menu/play3Button.png differ diff --git a/assets/menu/play5Button.png b/assets/menu/play5Button.png new file mode 100644 index 00000000..b6f82cad Binary files /dev/null and b/assets/menu/play5Button.png differ diff --git a/assets/menu/tutorialButton.png b/assets/menu/tutorialButton.png new file mode 100644 index 00000000..289c3781 Binary files /dev/null and b/assets/menu/tutorialButton.png differ diff --git a/assets/tutorial/instruction1.png b/assets/tutorial/instruction1.png new file mode 100644 index 00000000..4670983a Binary files /dev/null and b/assets/tutorial/instruction1.png differ diff --git a/assets/tutorial/instruction2.png b/assets/tutorial/instruction2.png new file mode 100644 index 00000000..ac0db518 Binary files /dev/null and b/assets/tutorial/instruction2.png differ diff --git a/assets/tutorial/instruction3.png b/assets/tutorial/instruction3.png new file mode 100644 index 00000000..3cdacf6f Binary files /dev/null and b/assets/tutorial/instruction3.png differ diff --git a/assets/tutorial/instruction4.png b/assets/tutorial/instruction4.png new file mode 100644 index 00000000..cd8fd906 Binary files /dev/null and b/assets/tutorial/instruction4.png differ diff --git a/assets/tutorial/instruction5.png b/assets/tutorial/instruction5.png new file mode 100644 index 00000000..d48a3cb4 Binary files /dev/null and b/assets/tutorial/instruction5.png differ diff --git a/docs/credits.txt b/docs/credits.txt new file mode 100644 index 00000000..24d817f7 --- /dev/null +++ b/docs/credits.txt @@ -0,0 +1,8 @@ +Credits for Boolean Bonanza + +Boolean block textures: Alex Trexler +Block break sound: Cork by Blender Foundation (submitted by Lamoot) https://opengameart.org/content/cork +Gameplay background music: Contemplation by Bart https://opengameart.org/content/contemplation +Button press sound: Cloud Click by Mobeyee Sounds https://mobeyee.com +Main menu music: Puzzle Menu by caret7 https://opengameart.org/content/puzzle-menu +Block Break Animations: AlotofImpacts - 5 Frame Impacts by JoesAlotofthings https://opengameart.org/users/joesalotofthings \ No newline at end of file diff --git a/docs/egdd.md b/docs/egdd.md index 51ed6536..9e5f2811 100644 --- a/docs/egdd.md +++ b/docs/egdd.md @@ -1 +1,193 @@ -REPLACE THIS TEXT WITH YOUR EGDD MARKDOWN. +# Boolean Bonanza EGDD + +## Elevator Pitch + +In Boolean Bonanza, you are presented with a grid of squares representing boolean values and operators (True, False, OR, AND, NOT, etc.). The player is able to move the rows and columns by clicking and dragging on a square, with the goal of making a row or column evaluate to True. If a row or column evaluates to True, it will be destroyed and give the player points. The goal is to make players able to evaluate boolean expressions on sight and understand how changes will affect their evaluation. + +## Influences + +- Yoshi's Cookie + - Medium: Video Game + - Explanation: Yoshi's Cookie multiplayer mode inspired the block movement of the grid. In Yoshi's Cookie, the player can move rows and columns such that their order is maintained and the blocks wrap around the other side. This will allow for greater control than a system like Candy Crush where only two blocks can be switched at a time. +- Candy Crush + - Medium: Video Game + - Explanation: Candy Crush is a popular "match-3" style game that incorporates a pleasing visual style to draw in players. The graphics and animations of Candy Crush are considered so good as to be addictive by some, which could be mimicked to encourage players to participate in this educational game. Blocks in Candy Crush explode into satisfying particle effects when broken. This is something that should be incorporated into Boolean Bonanza. +- 2048 + - Medium: Video Game + - Explanation: 2048 is a popular online matching game where players are tasked with combining squares based on their numerical value. Squares start with low powers of 2 (2, 4, 8, etc.) and are combined with squares of the same value until the player reaches a square of number 2048. The difficulty comes from squares of different values getting in the way of making matches. This is similar to the difficulty in Boolean Bonanza, which comes from the fact that randomly ordered squares will evaluate to false or an error and thus not break. + +## Core Gameplay Mechanics + +- Players are presented with a grid of squares representing boolean values and operators. +- Players can click and drag rows or columns on the grid of blocks to rearrange them. +- When a row (left to right) or a column (top to bottom) of boolean blocks evaluates to True, it will break and give the player points. +- After blocks break, the blocks above them will fall down until they are supported from below, with new blocks being spawned on the top of the grid. +- A timer will be displayed to let the player know how much more time they have in the current level before it ends. + +# Learning Aspects + +## Learning Domains + +Logic +Discrete Math +Introductory Programming (language is irrelevant given it has boolean values) + +## Target Audiences + +Boolean Bonanza is targeted at young computer scientists in middle school or high school looking to learn the basics of boolean statements. + +## Target Contexts + +Boolean Bonanza can be used at home as a study tool in order to practice boolean concepts. It can also be used in a classroom setting as an exercise to further strengthen one's booolean knowledge. + +## Learning Objectives + +- Truthiness of Booleans: By the end of the lesson, players will be able to explain the truthiness of a boolean expression with at least two variables +- Adjustment of Booleans: By the end of the lesson, players will be able to rearrange boolean expressions in order to achieve a truthful outcome. +- Boolean Algebra: By the end of the lesson, players will be able to recognize the order of operations for boolean algebra. Students will learn the priority of operations is NOT, then AND, then OR. + +## Prerequisite Knowledge + +- Prior to the game, players need to be able to define the concept of evaluating a boolean expression. +- Prior to the game, players need to be able to evaluate a boolean expression. +- Prior to the game, players need to be able to evaluate a boolean expression with more than two variables. +- Prior to the game, players need to be able to define the concept of order of operations regarding boolean expressions. + +## Assessment Measures + +A short pre-test and matching post-test should be designed to assess student learning. +The exact format of the test will be multiple choice where the student will be tasked with identifying which boolean expression of the four options provided evaluates to true. Students will be assessed based on their accuracy and efficiency. + +- Given the following boolean expression, determine whether it evaluates to True, False, or an Error: True AND False OR NOT True (False) +- Given the following boolean expression, determine whether it evaluates to True, False, or an Error: False OR True AND True (True) +- Given the following boolean expression, determine whether it evaluates to True, False, or an Error: True AND True OR AND FALSE (Error) + +# What sets this project apart? + +- Most computer science games revolve around code-writing, this one focuses solely on sharpening the players understanding of boolean expressions. +- The visuals of the game will mimic a Nintendo/Indie game with a bright and attractive atmosphere. +- With all input involving the shifting of blocks, the controls of the game are simple and easy to learn. +- The simple nature of the game along with its flashiness will be addictive to players. +- Players will improve their speed at evaluating boolean expressions as they improve at the game. + +# Player Interaction Patterns and Modes + +## Player Interaction Pattern + +Players will use a mouse or touchscreen to press buttons and drag blocks across the screen. Players will engage with the game by themselves or competitively. One player at a time will actively play the game while trying to achieve a high score in the time allotted. After that player is finished, other players can use the same device to attempt to reach a new high score. In this way, players can compete and try to achieve their own personal high score or a score higher than their friends. + +## Player Modes + +- Main Menu: allows the player to pick between a 3 x 3 and 5 x 5 depending on the player's experience. The player will also be given the option to play the tutorial. +- Tutorial: the player will be taken through a playable tutorial in which they learn the mechanics of the game. After completing the tutorial the player will be guided back to the main menu. +- 3 x 3 Boolean Grid: the player will make valid boolean statements with only one operator. After completing this level they will be given the option to play again or move on to the next level. +- 5 x 5 Boolean Grid: the player will make valid boolean statements with two operators. After completing this level they will be given the opportunity to play again. + +# Gameplay Objectives + +- Scoring Points By Making Valid Truthiness Statements: + - Description: Each valid truthiness statement will result in gaining an amount of points adding to you current score. + - Alignment: By gaining points, this will allow the player to see how many valid statements they are able to make. +- Scoring As Many Points As You Can Before The Time Is Up: + - Description: In Boolean Bonanza there will be a time limit for each level. During this time the player must make as many valid + statements as they can. + - Alignment: Each time the player plays the game they will be able to see their learning progress each time they score higher given the same time restriction. + +# Procedures/Actions + +During menu screens such as the main menu, the player will be able to move the mouse cursor and click on buttons to enter the game or change settings. On the gameplay screen, the player will use the mouse cursor to drag blocks across the screen. Clicking and holding down the mouse button on a block of the grid will allow the player to drag that block up, down, left, or right. The other blocks in that same row or column (depending on the direction of dragging) will also move to follow that block while wrapping around the edges of the screen. This allows the player to rearrange the blocks on the grid while still providing a challenge to plan moves in advance. + +# Rules + +Time - The player will be given limited time to create as many valid boolean expressions as possible. They do not have a limit on the number of moves that they can make, so having time as a finite resource will force the player to make decisions and movements at a higher rate, leading to some mistakes that will ultimately affect their end of game results. The player starts the game with a fixed amount of time, and once that time runs out the game will be over. + +# Objects/Entities + +- Blocks - In order to populate the grid that we have acting as our game board, we will need to design blocks that each depict a different aspect of a boolean statement. We will need a block for True, False, AND, OR, and NOT. Each block will be differentiated based upon their name and color scheme. +- Grid - The grid will act as the game board which will be a fixed size but can be altered depending on the difficulty of the level/phase. The grid itself will be populated by blocks, which will be stored in a double array. The grid itself will not be a highlight and will not really stick out to the player, it only really creates the boundaries of the game board. +- Timer - A timer will be created at the top of the grid that will tick downwards, displaying to the player the amount of time they have left in the level. The timer will look analog with a black background and white text for the numbers. Once the timer is finished, the timer will display 0:00 in red numbers to indicate the level is over. +- Scoreboard - A scoreboard will be create at the top of the grid, beside the timer in order to display the players score for the current level. The scoreboard will initially be listed with the players name above a 0000 to indicate that the player has no points at the start of the level. Once a valid boolean statement is created, the row/column will break and the player will be awarded a certain amount of points that will be reflected on the scoreboard. +- Background - The background will be behind the grid, and will not serve any real purpose outside of visual appeal. Depending on the difficulty of the level, could be a solid color or something more extreme to reflect the difficulty of the challenge that the player is undertaking. + +## Core Gameplay Mechanics + +- Players are presented with a grid of squares representing boolean values and operators. - Players will either be presented with a 3 x 3 grid involving only two boolean values or a 5 x 5 grid involving at least two boolean values. Within this grid the player will find boolean values True and False represented by T and F. The player will also see operators such as NOT, AND, and OR. Using this grid the player will have to determine how they can rearrange these elements in order to make true boolean statements. +- Players can click and drag rows or columns on the grid of blocks to rearrange them. - Players interact with the grid by clicking and dragging rows or columns to rearrange the blocks. This mechanic is intuitive, mimicking familiar touch and drag gestures, making the game accessible to a wide audience. It encourages strategic thinking and planning as players must consider the effects of their rearrangements not only on the immediate row or column but on the entire grid. This mechanic enhances problem-solving skills and provides a hands-on approach to learning boolean logic. +- When a row (left to right) or a column (top to bottom) of boolean blocks evaluates to True, it will break and give the player points. - When a row or column is arranged such that it evaluates to True (following standard boolean logic rules), that row or column breaks, rewarding the player with points. This mechanic directly ties the game's puzzle-solving aspect to educational outcomes, as players must apply their knowledge of boolean logic to progress. It provides instant feedback on the correctness of their logic application, reinforcing learning through gameplay. +- After blocks break, the blocks above them will fall down until they are supported from below, with new blocks being spawned on the top of the grid. - After blocks break, the mechanics of blocks falling to fill empty spaces and new blocks spawning at the top introduce a dynamic element to the game. This mechanic requires players to adapt to a constantly changing puzzle environment, enhancing cognitive flexibility. It adds a layer of complexity and unpredictability, as players must not only plan their current moves but also anticipate how these changes will affect future moves. This simulates a more realistic problem-solving environment, where conditions can change and require flexible thinking. +- A timer will be displayed to let the player know how much more time they have in the current level before it ends. - A visible timer adds urgency to each level, challenging players to think and act quickly. This time pressure tests and improves players' ability to apply boolean logic under constraints, mirroring real-world situations where decisions must be made within limited time frames. The timer also adds a competitive edge to the game, encouraging players to improve their speed and efficiency for higher scores or better completion times. + +## Feedback + +Blocks Breaking - When a valid boolean statement is created using the blocks, the blocks themselves will "break" and this break will consist of the blocks disappearing off the screen, each block will have a shattering animation that they leave behind along with a "breaking" audio cue to commend the player on the creation of a valid boolean statement. + +Medal Acquisition - Every time a valid boolean statement is created and the statement disappears from the screen, a fixed amount of points will be added to the players scoreboard on the top of the screen. Once these scores reach a certain threshold, there will be a small animation and audio cue indicating that the player has passed this threshold. An example of this would be going from a silver medal to a gold medal. The silver medal will slide down and be replaced with a gold medal from above. While this process is happening, there will be a metallic audio cue to indicate the upgrade of score. + +High Scores - There will be a log for all of the previous scores that a player has gotten. The scores themselves will track the amount of boolean statements created in the amount of time that it took. Based on this metric, the player will be able to see how they have progressed in terms of speed and accuracy when it comes to the creation of boolean statements. + +# Story and Gameplay + +## Presentation of Rules + +The player will be guided through a playable tutorial in which they will be making valid truthiness statements. The tutorial will take the player through each mechanic of the game. The player will be shown how to move operators in position to make valid boolean statements in order to add points to their score. After making a series of statements the player will be directed to the main menu where they can choose to play the game on their own. + +## Presentation of Content + +The playable tutorial will demonstrate to the player how the game is actually meant to be played. From that point, the player will be started off playing the game on the lowest difficulty on the smallest grid. At this point, the time pressure will not be severe and the player will get the opportunity to get their bearings with smaller and easier to handle boolean statements. After completion of these easier grid levels, the player will begin getting exposed to larger grids with less time to work with, forcing them to adapt and learn how to handle larger, more complex booleans. This cycle will continue until the time pressure becomes too great and the player will not be able to get an acceptable score. Because the learning aspect is so heavily tied into the core gameplay mechanics, the player's success within the game will be directly tied to their understanding of what a valid boolean statement is and how to create one given a set amount of variables. + +## Story + +The player will be tasked with a series of boolean puzzles under a time constraint. The game will present a grid to the player involving blocks consistiing of boolean operators and true/false indicators. The player must rearrange these blocks in order to make as many valid boolean statements as they can before the time is up. After doing the puzzles many times, the player will be able to see the progress they have made in making truthiness statements. + +## Storyboarding + +Main Gameplay Storyboard: + +Story Board 1 +Story Board 1 +Story Board 1 +Story Board 1 +Story Board 1 + +# Assets Needed + +## Aethestics + +The game is designed to immerse players in a nostalgic arcade atmosphere, with soft and bright colors that create a comforting and enjoyable visual experience reminiscent of old-timey Nintendo games. Its pixelated graphics and simple animations evoke a sense of retro charm, making the world both inviting and familiar. The chiptune soundtrack and classic sound effects further enhance the nostalgic feel, transporting players back to the golden era of gaming. Together, these elements combine to offer an experience that's not only visually and auditorily captivating but also emotionally resonant with fans of early video gaming. + +## Graphical + +- Boolean Blocks + - True Block + - False Block + - And Block + - Or Block + - Not Block +- Particle Textures: + - [AlotofImpacts Red Particles by JoesAlotofthings](https://opengameart.org/content/alotofimpacts-5-frame-impacts-4-variations-red-impacts-19of20) + - [AlotofImpacts Green Particles by JoesAlotofthings](https://opengameart.org/content/alotofimpacts-5-frame-impacts-4-variations-green-impacts-15of20) + - [AlotofImpacts Blue Particles by JoesAlotofthings](https://opengameart.org/content/alotofimpacts-5-frame-impacts-4-variations-blue-impacts-14of20) + - [AlotofImpacts Purple Particles by JoesAlotofthings](https://opengameart.org/content/alotofimpacts-5-frame-impacts-4-variations-purple-impacts-18of20) + - [AlotofImpacts Yellow Particles by JoesAlotofthings](https://opengameart.org/content/alotofimpacts-5-frame-impacts-4-variations-yellow-impacts-20of20) +- Environment Art/Textures: + - Example Backplate + - Main Menu Mockup + +## Audio + +- Music List (Ambient sound) + + - Main Menu Music: [Puzzle Menu by caret7](https://opengameart.org/content/puzzle-menu), [Menu by tcarisland](https://opengameart.org/content/menu-1) + - Gameplay Music: [Contemplation by bart](https://opengameart.org/content/contemplation), [Contemplation II by bart](https://opengameart.org/content/contemplation-ii) + +- Sound List (SFX) + - Button Press: [8bit Menu Highlight by Fupi](https://opengameart.org/content/8bit-menu-highlight), [Cloud Click by Mobeyee Sounds](https://opengameart.org/content/cloud-click) + - Block Breaking: [Cork by Blender Foundation](https://opengameart.org/content/cork), [Breaking/Falling/Hit SFX by rubberduck](https://opengameart.org/content/75-cc0-breaking-falling-hit-sfx) + - Block Breaking Combo: [Positive Sounds by remaxim](https://opengameart.org/content/postive-sounds), + +# Metadata + +- Template created by Austin Cory Bart , Mark Sheriff, Alec Markarian, and Benjamin Stanley. +- Version 0.0.3 + +- Shoutout ChatGPT for helping generate the spiels on the Core Gameplay Mechanics diff --git a/docs/large.png b/docs/large.png new file mode 100644 index 00000000..23c1aa92 Binary files /dev/null and b/docs/large.png differ diff --git a/docs/small.png b/docs/small.png new file mode 100644 index 00000000..589f9397 Binary files /dev/null and b/docs/small.png differ diff --git a/package-lock.json b/package-lock.json index 279e7740..4b4d31a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "coding-3-phaser-scenes", + "name": "final-project", "version": "4.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "coding-3-phaser-scenes", + "name": "final-project", "version": "4.0.0", "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e285b2ae..fdbda97b 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "final-project", - "version": "4.0.0", - "description": "Phaser 3 Final Project", - "homepage": "https://github.com/UD-S24-CISC374/final-project-template#readme", + "name": "Boolean Bonanza", + "version": "1.0.0", + "description": "Team Cyan Phaser 3 Final Project", + "homepage": "https://github.com/UD-S24-CISC374/final-project-cyan", "main": "index.js", "scripts": { "start": "webpack serve --config webpack/webpack.dev.js", @@ -24,12 +24,12 @@ "starter" ], "author": { - "name": "Austin Cory Bart", + "name": "Alex Trexler, Michael Murphy, and Clay Wilfong", "url": "https://github.com/acbart" }, "repository": { "type": "git", - "url": "https://github.com/UD-S24-CISC374/final-project-template.git" + "url": "https://github.com/UD-S24-CISC374/final-project-cyan" }, "engines": { "node": ">=12" diff --git a/pwa/icons/icons-192.png b/pwa/icons/icons-192.png index 16bebab8..8e8bc282 100644 Binary files a/pwa/icons/icons-192.png and b/pwa/icons/icons-192.png differ diff --git a/pwa/icons/icons-512.png b/pwa/icons/icons-512.png index 0e536097..23e6f4eb 100644 Binary files a/pwa/icons/icons-512.png and b/pwa/icons/icons-512.png differ diff --git a/pwa/manifest.json b/pwa/manifest.json index fa202a58..3b39b14b 100644 --- a/pwa/manifest.json +++ b/pwa/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "Phaser Game", - "name": "My Cool Phaser 3 Game", + "short_name": "Boolean Bonanza", + "name": "Boolean Bonanza", "icons": [ { "src": "./icons/icons-192.png", diff --git a/src/config.ts b/src/config.ts index 9776bc5c..dbfcdc46 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,13 +1,18 @@ import Phaser from "phaser"; -import MainScene from "./scenes/mainScene"; import PreloadScene from "./scenes/preloadScene"; +import MenuScene from "./scenes/menuScene"; +import FiveByFiveLevel from "./scenes/fiveByFiveLevel"; +import PostLevelScene from "./scenes/postLevelScene"; +import ThreeByThreeLevel from "./scenes/threeByThreeLevel"; +import TutorialLevel from "./scenes/tutorial"; +import AdvancedTutorial from "./scenes/advancedTutorial"; const DEFAULT_WIDTH = 1280; const DEFAULT_HEIGHT = 720; export const CONFIG = { - title: "My Untitled Phaser 3 Game", - version: "0.0.1", + title: "Boolean Bonanza", + version: "0.1.1", type: Phaser.AUTO, backgroundColor: "#ffffff", scale: { @@ -17,7 +22,15 @@ export const CONFIG = { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, }, - scene: [PreloadScene, MainScene], + scene: [ + PreloadScene, + MenuScene, + FiveByFiveLevel, + PostLevelScene, + ThreeByThreeLevel, + TutorialLevel, + AdvancedTutorial, + ], physics: { default: "arcade", arcade: { diff --git a/src/objects/blockGrid.ts b/src/objects/blockGrid.ts new file mode 100644 index 00000000..2c6979c4 --- /dev/null +++ b/src/objects/blockGrid.ts @@ -0,0 +1,541 @@ +import Phaser from "phaser"; +import BooleanBlock from "./booleanBlock"; + +interface Ratios { + [index: string]: number; +} + +export default class BlockGrid extends Phaser.GameObjects.Container { + blockMatrix: Array>; + private blockSize: number = 100; + private blockSpacing: number = 10; + private includeNotBlocks: boolean; + private operatorCount: number = 0; + private trueCreated: number = 0; + private falseCreated: number = 0; + private operatorCreated: number = 0; + private notCreated: number = 0; + + private IDEAL_BLOCK_RATIOS_3: Ratios = { + true: 0.2, + false: 0.2, + and: 0.3, + or: 0.3, + not: 0, + }; + private IDEAL_BLOCK_RATIOS_5: Ratios = { + true: 0.3, + false: 0.3, + and: 0.15, + or: 0.15, + not: 0.1, + }; + + constructor( + scene: Phaser.Scene, + sideLength: number, + includeNotBlocks: boolean = true + ) { + super(scene); + this.blockMatrix = []; + this.includeNotBlocks = includeNotBlocks; + this.initBlockCounters(sideLength * sideLength); // Initialize counters based on total blocks + + for (let i = 0; i < sideLength; i++) { + this.blockMatrix.push([]); + for (let j = 0; j < sideLength; j++) { + let block = this.createNewBlock(i, j); + this.blockMatrix[i].push(block); + this.add(block); + } + } + this.recenterGrid(); + this.updateBlockPositions(); + scene.add.existing(this); + scene.sound.add("block-break"); + } + + private initBlockCounters(totalBlocks: number): void { + let trueCount = Math.ceil(totalBlocks * 0.3); // 30% true + let falseCount = trueCount; // Equal number of true and false + this.operatorCount = totalBlocks - trueCount - falseCount; // Rest are operators + this.trueCreated = 0; + this.falseCreated = 0; + this.operatorCreated = 0; + this.notCreated = 0; + } + + public createNewBlock(row: number, col: number): BooleanBlock { + // let blockType = this.determineBlockType(); + let x = col * (this.blockSize + this.blockSpacing); + let y = row * (this.blockSize + this.blockSpacing); + let block = new BooleanBlock( + this.scene, + x, + y, + this.determineBlockType(), + [row, col] + ); + + block.setInteractive(); + return block; + } + + private getCurrentRatios(): Ratios { + const totalBlocks = this.countTotalBlocks(); + return { + true: this.trueCreated / totalBlocks, + false: this.falseCreated / totalBlocks, + and: this.operatorCreated / totalBlocks, + or: this.operatorCreated / totalBlocks, + not: this.includeNotBlocks ? this.notCreated / totalBlocks : 0, + }; + } + + private determineBlockType(): string { + if (this.includeNotBlocks) { + let currentBlockRatios = this.getBlockRatios(); + let blockRatioDelta: { [blockType: string]: number } = { + true: 0, + false: 0, + and: 0, + or: 0, + not: 0, + }; + for (let blockType in currentBlockRatios) { + blockRatioDelta[blockType] = + this.IDEAL_BLOCK_RATIOS_5[blockType] - + currentBlockRatios[blockType]; + } + let maxRatioDelta: string = "true"; + for (let blockType in blockRatioDelta) { + if ( + blockRatioDelta[blockType] > blockRatioDelta[maxRatioDelta] + ) { + maxRatioDelta = blockType; + } + } + return maxRatioDelta; + } else { + let currentBlockRatios = this.getBlockRatios(); + let blockRatioDelta: { [blockType: string]: number } = { + true: 0, + false: 0, + and: 0, + or: 0, + }; + for (let blockType in currentBlockRatios) { + blockRatioDelta[blockType] = + this.IDEAL_BLOCK_RATIOS_3[blockType] - + currentBlockRatios[blockType]; + } + let maxRatioDelta: string = "true"; + for (let blockType in blockRatioDelta) { + if ( + blockRatioDelta[blockType] > blockRatioDelta[maxRatioDelta] + ) { + maxRatioDelta = blockType; + } + } + return maxRatioDelta; + } + } + + private getBlockRatios(): { [blockType: string]: number } { + if (this.includeNotBlocks) { + let blockCounts: { [blockType: string]: number } = { + true: 0, + false: 0, + and: 0, + or: 0, + not: 0, + }; + let nonNullIndexes: number = 0; + for (let i = 0; i < this.blockMatrix.length; i++) { + for (let j = 0; j < this.blockMatrix.length; j++) { + if (this.blockMatrix[i][j] != null) { + const block = this.blockMatrix[i][j] as BooleanBlock; + blockCounts[block.getBlockType()] += 1; + nonNullIndexes += 1; + } + } + } + for (let blockType in blockCounts) { + blockCounts[blockType] /= nonNullIndexes; + } + return blockCounts; + } else { + let blockCounts: { [blockType: string]: number } = { + true: 0, + false: 0, + and: 0, + or: 0, + }; + let nonNullIndexes: number = 0; + for (let i = 0; i < this.blockMatrix.length; i++) { + for (let j = 0; j < this.blockMatrix.length; j++) { + if (this.blockMatrix[i][j] != null) { + const block = this.blockMatrix[i][j] as BooleanBlock; + blockCounts[block.getBlockType()] += 1; + nonNullIndexes += 1; + } + } + } + for (let blockType in blockCounts) { + blockCounts[blockType] /= nonNullIndexes; + } + return blockCounts; + } + } + + private countTotalBlocks(): number { + return this.blockMatrix.flat().length; + } + + private randomOperator(): string { + let operators = ["and", "or"]; + if (this.includeNotBlocks && this.notCreated < this.operatorCount / 3) { + operators.push("not"); + this.notCreated++; + } + return operators[Math.floor(Math.random() * operators.length)]; + } + + public getBlockAtLocation(index: [number, number]) { + return this.blockMatrix[index[0]][index[1]]; + } + + public switchBlocks( + indexA: [number, number], + indexB: [number, number] + ): Array> { + let blockA = this.blockMatrix[indexA[0]][indexA[1]] as BooleanBlock; + let blockB = this.blockMatrix[indexB[0]][indexB[1]] as BooleanBlock; + blockA.setGridLocation(indexB); + blockB.setGridLocation(indexA); + this.blockMatrix[indexA[0]][indexA[1]] = blockB; + this.blockMatrix[indexB[0]][indexB[1]] = blockA; + // promises used to ensure the swap animation is complete before blocks are eliminated + let promise1 = new Promise((resolve: () => void) => { + this.scene.tweens.add({ + targets: blockA, + x: indexB[1] * (this.blockSize + this.blockSpacing), + y: indexB[0] * (this.blockSize + this.blockSpacing), + ease: "Linear", + duration: 300, + onComplete: resolve, + }); + }); + let promise2 = new Promise((resolve: () => void) => { + this.scene.tweens.add({ + targets: blockB, + x: indexA[1] * (this.blockSize + this.blockSpacing), + y: indexA[0] * (this.blockSize + this.blockSpacing), + ease: "Linear", + duration: 300, + onComplete: resolve, + }); + }); + // returns promises of the tweens so that the scene can check truthiness after they finish + return [promise1, promise2]; + } + + public evaluateBooleanExpression(blocks: Array): boolean { + try { + let expression = blocks + .map((block) => { + switch (block.getBlockType()) { + case "and": + return "&&"; + case "or": + return "||"; + case "not": + return "!"; + case "true": + return "true"; + case "false": + return "false"; + default: + return ""; + } + }) + .join(" "); + return eval(expression) as boolean; + } catch (error) { + return false; + } + } + + public findTruthyStatements(): Array<{ + type: "row" | "column"; + index: number; + }> { + let outArray: Array<{ type: "row" | "column"; index: number }> = []; + for (let row = 0; row < this.blockMatrix.length; row++) { + if ( + this.evaluateBooleanExpression( + this.blockMatrix[row] as Array + ) + ) { + outArray.push({ type: "row", index: row }); + } + } + + for (let col = 0; col < this.blockMatrix.length; col++) { + let columnBlocks = this.blockMatrix.map((row) => row[col]); + if ( + this.evaluateBooleanExpression( + columnBlocks as Array + ) + ) { + outArray.push({ type: "column", index: col }); + } + } + + return outArray; + } + + public checkForTruthy(): number { + let foundTruthy = this.findTruthyStatements(); + if (foundTruthy.length > 0) { + let indexesToBreak = this.getIndexesToBreak(foundTruthy); + let breakingTweenPromises: Promise[] = []; + + this.scene.sound.play("block-break", { volume: 0.5 }); + for (let i = 0; i < indexesToBreak.length; i++) { + breakingTweenPromises.push( + this.breakBlockAtIndex(indexesToBreak[i]) + ); + } + Promise.all(breakingTweenPromises).then(() => { + this.fallRemainingGridPositions(); + let fallingTweenPromises: Promise[] = + this.moveToCorrectBlockPositions(); + Promise.all(fallingTweenPromises).then(() => { + this.fillEmptyMatrixPositions(); + }); + }); + // returns number of truthy statements found + return indexesToBreak.length; + } + return 0; + } + + private async fillEmptyMatrixPositions() { + for (let i = 0; i < this.blockMatrix.length; i++) { + for (let j = 0; j < this.blockMatrix.length; j++) { + if (this.blockMatrix[i][j] == null) { + const block = this.createNewBlock(i, j); + this.blockMatrix[i][j] = block; + this.add(block); + } + } + } + } + + private async fallRemainingGridPositions() { + for (let i = this.blockMatrix.length - 2; i >= 0; i--) { + for (let j = 0; j < this.blockMatrix.length; j++) { + if ( + this.blockMatrix[i][j] != null && + this.blockMatrix[i + 1][j] == null + ) { + let block: BooleanBlock = this.blockMatrix[i][ + j + ] as BooleanBlock; + block.setGridLocation([i + 1, j]); + this.blockMatrix[i + 1][j] = block; + this.blockMatrix[i][j] = null; + } + } + } + } + + private moveToCorrectBlockPositions(): Promise[] { + let promises: Promise[] = []; + for (let i = 0; i < this.blockMatrix.length; i++) { + for (let j = 0; j < this.blockMatrix.length; j++) { + if (this.blockMatrix[i][j] != null) { + let block: BooleanBlock = this.blockMatrix[i][ + j + ] as BooleanBlock; + const correctCorrdinates = this.getCorrectCoordinates([ + i, + j, + ]); + if ( + block.x != correctCorrdinates[0] || + block.y != correctCorrdinates[1] + ) { + promises.push( + this.moveBlockToCoordinates( + block, + correctCorrdinates[0], + correctCorrdinates[1] + ) + ); + } + } + } + } + return promises; + } + + private async moveBlockToCoordinates( + block: BooleanBlock, + x: number, + y: number + ): Promise { + let promise = new Promise((resolve: () => void) => { + this.scene.tweens.add({ + targets: block, + x: x, + y: y, + ease: "Linear", + duration: 300, + onComplete: resolve, + }); + }); + return promise; + } + + private getCorrectCoordinates(ind: [number, number]): [number, number] { + return [ + ind[1] * (this.blockSize + this.blockSpacing), + ind[0] * (this.blockSize + this.blockSpacing), + ]; + } + + private replaceBlockAtIndex(ind: [number, number]) { + const newBlock: BooleanBlock = this.createNewBlock(ind[0], ind[1]); + this.blockMatrix[ind[0]][ind[1]] = newBlock; + this.add(newBlock); + this.updateBlockPositions(); + } + + private breakBlockAtIndex(ind: [number, number]): Promise { + let block: BooleanBlock = this.blockMatrix[ind[0]][ + ind[1] + ] as BooleanBlock; + const animPromise = new Promise((resolve) => { + const breakKey = this.getBreakAnimationKey(block); + const horizontalAdjustment = this.includeNotBlocks ? 370 : 480; + const verticalAdjustment = this.includeNotBlocks ? 130 : 200; + + // Create and play the animation + const anim = this.scene.add + .sprite( + block.x + this.blockSize / 2 + horizontalAdjustment, + block.y + this.blockSize / 2 + verticalAdjustment, + breakKey + ) + .setOrigin(0.5, 0.5) + .setDepth(10); + + anim.play(breakKey); + anim.on("animationcomplete", () => { + // When animation completes, destroy the sprite and resolve the promise + anim.destroy(); + resolve(); + }); + + // Destroy the block immediately (consider destroying after animation if needed) + block.destroy(); + this.blockMatrix[ind[0]][ind[1]] = null; + }); + + return animPromise; + } + + private getIndexesToBreak( + truthyStatements: Array<{ + type: "row" | "column"; + index: number; + }> + ): Array<[number, number]> { + let out: Array<[number, number]> = []; + for (let i = 0; i < truthyStatements.length; i++) { + if (truthyStatements[i].type == "row") { + for (let j = 0; j < this.blockMatrix.length; j++) { + if (!this.checkIfIn([truthyStatements[i].index, j], out)) { + out.push([truthyStatements[i].index, j]); + } + } + } else { + for (let j = 0; j < this.blockMatrix.length; j++) { + if (!this.checkIfIn([j, truthyStatements[i].index], out)) { + out.push([j, truthyStatements[i].index]); + } + } + } + } + return out; + } + + private checkIfIn( + index: [number, number], + array: Array<[number, number]> + ): boolean { + for (let i = 0; i < array.length; i++) { + if (array[i][0] == index[0] && array[i][1] == index[1]) { + return true; + } + } + return false; + } + + private decrementCounterBasedOnType(blockType: string) { + switch (blockType) { + case "true": + this.trueCreated--; + break; + case "false": + this.falseCreated--; + break; + default: + this.operatorCreated--; + if (blockType === "not") this.notCreated--; + break; + } + } + + public getBreakAnimationKey(block: BooleanBlock): string { + switch (block.getBlockType()) { + case "true": + return "greenBreak"; + case "false": + return "redBreak"; + case "and": + return "blueBreak"; + case "or": + return "purpleBreak"; + case "not": + return "yellowBreak"; + default: + throw new Error(`Unknown block type: ${block.getBlockType()}`); + } + } + + public updateBlockPositions() { + for (let row = 0; row < this.blockMatrix.length; row++) { + for (let col = 0; col < this.blockMatrix[row].length; col++) { + let block = this.blockMatrix[row][col] as BooleanBlock; + block.x = col * (this.blockSize + this.blockSpacing); + block.y = row * (this.blockSize + this.blockSpacing); + block.setGridLocation([row, col]); + } + } + this.recenterGrid(); + } + + private recenterGrid() { + let gridWidth = + this.blockMatrix[0].length * (this.blockSize + this.blockSpacing) - + this.blockSpacing; + let gridHeight = + this.blockMatrix.length * (this.blockSize + this.blockSpacing) - + this.blockSpacing; + this.x = (this.scene.scale.width - gridWidth) / 2 + 30; + this.y = (this.scene.scale.height - gridHeight) / 2 + 40; + } +} diff --git a/src/objects/booleanBlock.ts b/src/objects/booleanBlock.ts new file mode 100644 index 00000000..9a33028a --- /dev/null +++ b/src/objects/booleanBlock.ts @@ -0,0 +1,57 @@ +import Phaser from "phaser"; + +export default class BooleanBlock extends Phaser.GameObjects.Image { + /* + BooleanBlock is the GameObject that represents the blocks on the board + It is given a scene, x and y coordinates, and a texture based on its type + */ + private gridLocation: [number, number]; // gridLocation stores a block's location on the grid locally counting from the top left [row, column] + private blockType: string; + + constructor( + scene: Phaser.Scene, + x: number, + y: number, + type: string, + gridLocation: [number, number] + ) { + let img: string = ""; + switch (type) { + case "and": { + img = "and-block"; + break; + } + case "or": { + img = "or-block"; + break; + } + case "not": { + img = "not-block"; + break; + } + case "true": { + img = "true-block"; + break; + } + case "false": { + img = "false-block"; + break; + } + } + super(scene, x, y, img); + this.blockType = type; + this.gridLocation = gridLocation; + } + + public getGridLocation() { + return this.gridLocation; + } + + public setGridLocation(newGridLocation: [number, number]): void { + this.gridLocation = newGridLocation; + } + + public getBlockType(): string { + return this.blockType; + } +} diff --git a/src/objects/fpsText.ts b/src/objects/fpsText.ts deleted file mode 100644 index b1a2a3d8..00000000 --- a/src/objects/fpsText.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Phaser from "phaser"; -export default class FpsText extends Phaser.GameObjects.Text { - constructor(scene: Phaser.Scene) { - super(scene, 10, 10, "", { color: "black", fontSize: "28px" }); - scene.add.existing(this); - this.setOrigin(0); - } - - public update() { - this.setText(`fps: ${Math.floor(this.scene.game.loop.actualFps)}`); - } -} diff --git a/src/objects/pausemenu.ts b/src/objects/pausemenu.ts new file mode 100644 index 00000000..64d764d2 --- /dev/null +++ b/src/objects/pausemenu.ts @@ -0,0 +1,61 @@ +import Phaser from "phaser"; + +export default class PauseMenu extends Phaser.GameObjects.Container { + private pausedText: Phaser.GameObjects.Text; + public mainMenuButton: Phaser.GameObjects.Image; + public resumeButton: Phaser.GameObjects.Image; + private background: Phaser.GameObjects.Rectangle; + + constructor( + scene: Phaser.Scene, + resumeFunc: (button: Phaser.GameObjects.Image) => void, + mainMenuFunc: (button: Phaser.GameObjects.Image) => void + ) { + super(scene); + + this.background = new Phaser.GameObjects.Rectangle( + scene, + 640, + 360, + 1280, + 720, + 0, + 0.4 + ); + this.add(this.background); + + this.pausedText = new Phaser.GameObjects.Text( + scene, + 370, + 100, + "Game Paused", + { + fontFamily: "Arial", + color: "#000000", + fontSize: 80, + backgroundColor: "#FFFFFF", + } + ); + this.add(this.pausedText); + + this.mainMenuButton = new Phaser.GameObjects.Image( + scene, + 640, + 350, + "main-menu-button" + ) + .setInteractive() + .on("pointerdown", mainMenuFunc, this.scene); + this.add(this.mainMenuButton); + + this.resumeButton = new Phaser.GameObjects.Image( + scene, + 640, + 500, + "resume-button" + ) + .setInteractive() + .on("pointerdown", resumeFunc, this.scene); + this.add(this.resumeButton); + } +} diff --git a/src/objects/phaserLogo.ts b/src/objects/phaserLogo.ts deleted file mode 100644 index 84324120..00000000 --- a/src/objects/phaserLogo.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Phaser from "phaser"; - -export default class PhaserLogo extends Phaser.Physics.Arcade.Sprite { - constructor(scene: Phaser.Scene, x: number, y: number) { - super(scene, x, y, "phaser-logo"); - scene.add.existing(this); - scene.physics.add.existing(this); - - this.setCollideWorldBounds(true) - .setBounce(0.6) - .setInteractive() - .on("pointerdown", () => { - this.setVelocityY(-400); - }); - } -} diff --git a/src/objects/scoreDisplay.ts b/src/objects/scoreDisplay.ts new file mode 100644 index 00000000..7593c04b --- /dev/null +++ b/src/objects/scoreDisplay.ts @@ -0,0 +1,44 @@ +import Phaser from "phaser"; + +export default class ScoreDisplay extends Phaser.GameObjects.Container { + private score: number; + private scoreText: Phaser.GameObjects.Text; + private scoreBox: Phaser.GameObjects.Rectangle; + + constructor( + scene: Phaser.Scene, + x: number, + y: number, + startScore: number = 0 + ) { + super(scene, x, y); + this.scoreText = new Phaser.GameObjects.Text( + this.scene, + x - 65, + y - 10, + `${startScore}`, + { + fontFamily: "Arial", + fontSize: "45px", + align: "center", + color: "black", + } + ); + this.add(this.scoreText); + this.scene.add.existing(this.scoreText); + this.score = startScore; + } + + public changeScore(newScore: number) { + this.scoreText.setText(`${newScore}`); + this.score = newScore; + } + + public incrementScore(increment: number) { + this.changeScore(this.score + increment); + } + + public getScore(): number { + return this.score; + } +} diff --git a/src/scenes/advancedTutorial.ts b/src/scenes/advancedTutorial.ts new file mode 100644 index 00000000..9bd3afe8 --- /dev/null +++ b/src/scenes/advancedTutorial.ts @@ -0,0 +1,195 @@ +import Phaser from "phaser"; +import BlockGrid from "../objects/blockGrid"; +import BooleanBlock from "../objects/booleanBlock"; +import ScoreDisplay from "../objects/scoreDisplay"; +import PauseMenu from "../objects/pausemenu"; + +export default class AdvancedTutorial extends Phaser.Scene { + // No need for gameplay music since music will carry over from firest tutorial scene + locationBuffer: [number, number] | undefined; + blockGrid: BlockGrid; + scoreDisplay: ScoreDisplay; + instructionImage: Phaser.GameObjects.Image; + timer: Phaser.Time.TimerEvent; + timeLimitInSeconds: number; + timerText: Phaser.GameObjects.Text; + pauseButton: Phaser.GameObjects.Image; + pauseMenu: PauseMenu; + paused: boolean; + pauseLock: boolean = true; + background: Phaser.GameObjects.Image; + + constructor() { + super({ key: "AdvancedTutorial" }); + } + + create() { + this.paused = false; + this.background = new Phaser.GameObjects.Image( + this, + 640, + 360, + "5x5-backplate" + ); + this.add.existing(this.background); + this.blockGrid = new BlockGrid(this, 5, true); // Initialize a 5x5 grid + this.scoreDisplay = new ScoreDisplay(this, 900, 38); + this.timeLimitInSeconds = 60; + + this.input.on("pointerdown", this.mouseClick, this); + + this.instructionImage = this.add + .image(180, 600, "instruction-4") + .setScale(0.6); + + this.timerText = this.add + .text(550, 77, `${this.timeLimitInSeconds}`, { + fontFamily: "Arial", + color: "#000000", + fontSize: "45px", + }) + .setOrigin(1, 1); + this.timer = this.time.addEvent({ + delay: 1000, + callback: () => { + this.timeLimitInSeconds--; + if (this.timeLimitInSeconds <= 0) { + this.sound.stopAll(); + this.scene.start("PostLevelScene", { + finalScore: this.scoreDisplay.getScore(), + }); + } + }, + callbackScope: this, + loop: true, + paused: true, + }); + + this.pauseButton = new Phaser.GameObjects.Image( + this, + 50, + 50, + "pause-button" + ) + .setScale(0.1) + .setInteractive() + .on("pointerdown", this.clickPause, this); + this.add.existing(this.pauseButton); + + this.blockGrid.setX(420); + this.blockGrid.setY(180); + } + + mouseClick( + pointer: Phaser.Input.Pointer, + currentlyOver: Array + ) { + if (!this.paused && currentlyOver[0] instanceof BooleanBlock) { + const currentBlock = currentlyOver[0] as BooleanBlock; + const currentLocation = currentBlock.getGridLocation(); + + if (this.locationBuffer === undefined) { + // No block is currently selected, select this one + this.locationBuffer = currentLocation; + currentBlock.setTint(0xfff300); // Tint the selected block + } else { + // Try to retrieve the previously selected block safely + const previousBlock = this.blockGrid.getBlockAtLocation( + this.locationBuffer + ); + if (previousBlock !== null) { + previousBlock.clearTint(); // Safely clear the tint only if previousBlock is not null + } + + if ( + this.locationBuffer[0] === currentLocation[0] && + this.locationBuffer[1] === currentLocation[1] + ) { + // The same block was clicked again deselect it + this.locationBuffer = undefined; + } else if (previousBlock) { + // A different block was clicked and previousBlock is not null -> swap + let promises = this.blockGrid.switchBlocks( + currentLocation, + this.locationBuffer + ); + this.locationBuffer = undefined; + Promise.all(promises).then(() => { + const matches: number = this.blockGrid.checkForTruthy(); + this.scoreDisplay.incrementScore(matches); + this.updateTutorialState(); + }); + } + } + } + } + update() { + this.timerText.setText(`${this.timeLimitInSeconds}`); + } + + clickPause() { + if (!this.paused) { + this.paused = true; + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseButton.setScale(0.09); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.pauseButton.setScale(0.1); + this.timer.paused = true; + this.pauseMenu = new PauseMenu( + this, + this.resumeFunc, + this.mainMenuFunc + ); + this.add.existing(this.pauseMenu); + }); + } + } + + mainMenuFunc() { + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseMenu.mainMenuButton.setScale(0.9); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.pauseMenu.mainMenuButton.setScale(1); + this.sound.stopAll(); + this.scene.start("MenuScene"); + }); + } + + resumeFunc() { + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseMenu.resumeButton.setScale(0.9); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.paused = false; + this.pauseMenu.resumeButton.setScale(1); + this.pauseMenu.destroy(); + if (!this.pauseLock) { + this.timer.paused = false; + } + }); + } + + startTimer() { + this.instructionImage.setTexture("instruction-5"); + + this.pauseLock = false; + this.timer.paused = false; + } + + updateTutorialState() { + if (this.timer.paused) { + this.startTimer(); + } + } +} diff --git a/src/scenes/fiveByFiveLevel.ts b/src/scenes/fiveByFiveLevel.ts new file mode 100644 index 00000000..4beb4938 --- /dev/null +++ b/src/scenes/fiveByFiveLevel.ts @@ -0,0 +1,190 @@ +import Phaser from "phaser"; +import BlockGrid from "../objects/blockGrid"; +import BooleanBlock from "../objects/booleanBlock"; +import ScoreDisplay from "../objects/scoreDisplay"; +import PauseMenu from "../objects/pausemenu"; + +function updateHighScore(newScore: number, mode: string) { + const key = `highScore-${mode}`; + const currentHighScore = + parseInt(localStorage.getItem(key) ?? "0", 10) || 0; + if (newScore > currentHighScore) { + localStorage.setItem(key, newScore.toString()); + } +} + +export default class FiveByFiveLevel extends Phaser.Scene { + locationBuffer: [number, number] | undefined; + blockGrid: BlockGrid; + timer: Phaser.Time.TimerEvent; + timeLimitInSeconds: number; + timerText: Phaser.GameObjects.Text; + gameplayMusic: Phaser.Sound.BaseSound; + scoreDisplay: ScoreDisplay; + pauseButton: Phaser.GameObjects.Image; + pauseMenu: PauseMenu; + paused: boolean; + background: Phaser.GameObjects.Image; + + constructor() { + super({ key: "FiveByFiveLevel" }); + } + + preload() { + // Preload assets if not done in PreloadScene, otherwise this is not needed + } + + create() { + this.paused = false; + this.timeLimitInSeconds = 120; + this.background = new Phaser.GameObjects.Image( + this, + 640, + 360, + "5x5-backplate" + ); + this.add.existing(this.background); + this.blockGrid = new BlockGrid(this, 5); + this.gameplayMusic = this.sound.add("gameplay-music"); + this.gameplayMusic.play({ volume: 0.3, loop: true }); + this.scoreDisplay = new ScoreDisplay(this, 900, 38); + + this.input.on("pointerdown", this.mouseClick, this); + + this.timer = this.time.addEvent({ + delay: 1000, + callback: () => { + this.timeLimitInSeconds--; + if (this.timeLimitInSeconds <= 0) { + updateHighScore(this.scoreDisplay.getScore(), "5x5"); + this.gameplayMusic.stop(); + this.scene.start("PostLevelScene", { + finalScore: this.scoreDisplay.getScore(), + }); + } + }, + callbackScope: this, + loop: true, + }); + + this.timerText = this.add + .text(550, 77, `${this.timeLimitInSeconds}`, { + fontFamily: "Arial", + color: "#000000", + fontSize: "45px", + }) + .setOrigin(1, 1); + + this.pauseButton = new Phaser.GameObjects.Image( + this, + 50, + 50, + "pause-button" + ) + .setScale(0.1) + .setInteractive() + .on("pointerdown", this.clickPause, this); + this.add.existing(this.pauseButton); + + this.blockGrid.setX(420); + this.blockGrid.setY(180); + } + + mouseClick( + pointer: Phaser.Input.Pointer, + currentlyOver: Array + ) { + if (!this.paused && currentlyOver[0] instanceof BooleanBlock) { + const currentBlock = currentlyOver[0] as BooleanBlock; + const currentLocation = currentBlock.getGridLocation(); + + if (this.locationBuffer === undefined) { + // No block is currently selected, select this one + this.locationBuffer = currentLocation; + currentBlock.setTint(0xfff300); // Tint the selected block + } else { + // Try to retrieve the previously selected block safely + const previousBlock = this.blockGrid.getBlockAtLocation( + this.locationBuffer + ); + if (previousBlock !== null) { + previousBlock.clearTint(); // Safely clear the tint only if previousBlock is not null + } + + if ( + this.locationBuffer[0] === currentLocation[0] && + this.locationBuffer[1] === currentLocation[1] + ) { + // The same block was clicked again deselect it + this.locationBuffer = undefined; + } else if (previousBlock) { + // A different block was clicked and previousBlock is not null -> swap + let promises = this.blockGrid.switchBlocks( + currentLocation, + this.locationBuffer + ); + this.locationBuffer = undefined; + Promise.all(promises).then(() => { + const matches: number = this.blockGrid.checkForTruthy(); + this.scoreDisplay.incrementScore(matches); + }); + } + } + } + } + + clickPause() { + if (!this.paused) { + this.paused = true; + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseButton.setScale(0.09); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.pauseButton.setScale(0.1); + this.timer.paused = true; + this.pauseMenu = new PauseMenu( + this, + this.resumeFunc, + this.mainMenuFunc + ); + this.add.existing(this.pauseMenu); + }); + } + } + + mainMenuFunc() { + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseMenu.mainMenuButton.setScale(0.9); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.pauseMenu.mainMenuButton.setScale(1); + this.gameplayMusic.pause(); + this.scene.start("MenuScene"); + }); + } + + resumeFunc() { + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseMenu.resumeButton.setScale(0.9); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.paused = false; + this.pauseMenu.resumeButton.setScale(1); + this.pauseMenu.destroy(); + this.timer.paused = false; + }); + } + + update() { + this.timerText.setText(`${this.timeLimitInSeconds}`); + } +} diff --git a/src/scenes/mainScene.ts b/src/scenes/mainScene.ts deleted file mode 100644 index 1c6b6089..00000000 --- a/src/scenes/mainScene.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Phaser from "phaser"; -import PhaserLogo from "../objects/phaserLogo"; -import FpsText from "../objects/fpsText"; - -export default class MainScene extends Phaser.Scene { - fpsText: FpsText; - - constructor() { - super({ key: "MainScene" }); - } - - create() { - new PhaserLogo(this, this.cameras.main.width / 2, 0); - this.fpsText = new FpsText(this); - - const message = `Phaser v${Phaser.VERSION}`; - this.add - .text(this.cameras.main.width - 15, 15, message, { - color: "#000000", - fontSize: "24px", - }) - .setOrigin(1, 0); - } - - update() { - this.fpsText.update(); - } -} diff --git a/src/scenes/menuScene.ts b/src/scenes/menuScene.ts new file mode 100644 index 00000000..1842ec54 --- /dev/null +++ b/src/scenes/menuScene.ts @@ -0,0 +1,102 @@ +import Phaser from "phaser"; + +export default class MenuScene extends Phaser.Scene { + tutorialButton: Phaser.GameObjects.Image; + play5Button: Phaser.GameObjects.Image; + play3Button: Phaser.GameObjects.Image; + menuMusic: Phaser.Sound.BaseSound; + + constructor() { + super({ key: "MenuScene" }); + } + + create() { + this.add.image(640, 360, "menu-backplate"); // backplate image for title and background + + this.add + .text(800, 250, "Start Interactive Tutorial", { + font: "38px Arial", + color: "#fff", + }) + .setOrigin(0.5); + + // main menu music + this.menuMusic = this.sound.add("menu-music", { loop: true }); + this.menuMusic.play(); + + this.tutorialButton = new Phaser.GameObjects.Image( + this, + 440, + 250, + "tutorial-button" + ); + this.tutorialButton + .setScale(0.6) + .setInteractive() + .on("pointerdown", () => { + this.clickPlay(this.tutorialButton, "TutorialLevel"); + }); + this.add.existing(this.tutorialButton); + + // play button for 5x5 mode + this.play5Button = new Phaser.GameObjects.Image( + this, + 440, + 550, + "play-5-button" + ); + this.play5Button + .setScale(0.6) + .setInteractive() + .on("pointerdown", () => { + this.clickPlay(this.play5Button, "FiveByFiveLevel"); + }); + this.add.existing(this.play5Button); + + // play button for 3x3 mode + this.play3Button = new Phaser.GameObjects.Image( + this, + 440, + 400, + "play-3-button" + ); + this.play3Button + .setScale(0.6) + .setInteractive() + .on("pointerdown", () => { + this.clickPlay(this.play3Button, "ThreeByThreeLevel"); + }); + this.add.existing(this.play3Button); + + this.displayHighScores(); + } + + displayHighScores() { + const highScore3x3 = localStorage.getItem("highScore-3x3") ?? "0"; + const highScore5x5 = localStorage.getItem("highScore-5x5") ?? "0"; + + const style = { font: "38px Arial", fill: "#fff" }; + this.add + .text(800, 400, `3x3 High Score: ${highScore3x3}`, style) + .setOrigin(0.5); + this.add + .text(800, 550, `5x5 High Score: ${highScore5x5}`, style) + .setOrigin(0.5); + } + + // run when play button is pressed + //modified so that it accepts scenekey as a paramaeter + clickPlay(button: Phaser.GameObjects.Image, sceneKey: string) { + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + button.setScale(0.55); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + button.setScale(1); + this.menuMusic.stop(); + this.scene.start(sceneKey); + }); + } +} diff --git a/src/scenes/postLevelScene.ts b/src/scenes/postLevelScene.ts new file mode 100644 index 00000000..1bf186cb --- /dev/null +++ b/src/scenes/postLevelScene.ts @@ -0,0 +1,45 @@ +import Phaser from "phaser"; + +export default class PostLevelScene extends Phaser.Scene { + finalScore: number; + playAgainButton: Phaser.GameObjects.Image; + menuMusic: Phaser.Sound.BaseSound; + + init(data: { finalScore: number }) { + this.finalScore = data.finalScore; + } + + constructor() { + super({ key: "PostLevelScene" }); + } + + create() { + this.add.text(420, 200, `Final Score: ${this.finalScore}`, { + color: "black", + fontFamily: "Arial", + fontSize: "70px", + align: "center", + }); + + this.menuMusic = this.sound.add("menu-music", { loop: true }); + this.menuMusic.play(); + + this.playAgainButton = new Phaser.GameObjects.Image( + this, + 640, + 400, + "play-again-button" + ); + this.playAgainButton + .setInteractive() + .on("pointerdown", this.clickPlayAgain, this); + this.add.existing(this.playAgainButton); + } + + clickPlayAgain() { + this.sound.play("button-press", { volume: 0.4 }); + this.menuMusic.stop(); + + this.scene.start("MenuScene"); + } +} diff --git a/src/scenes/preloadScene.ts b/src/scenes/preloadScene.ts index c17b81ba..391734a2 100644 --- a/src/scenes/preloadScene.ts +++ b/src/scenes/preloadScene.ts @@ -7,9 +7,126 @@ export default class PreloadScene extends Phaser.Scene { preload() { this.load.image("phaser-logo", "assets/img/phaser-logo.png"); + this.load.image("and-block", "assets/blocks/And.png"); + this.load.image("or-block", "assets/blocks/Or.png"); + this.load.image("not-block", "assets/blocks/Not.png"); + this.load.image("true-block", "assets/blocks/True.png"); + this.load.image("false-block", "assets/blocks/False.png"); + this.load.image("menu-backplate", "assets/menu/menuBackplate.png"); + this.load.image("play-3-button", "assets/menu/play3Button.png"); + this.load.image("play-5-button", "assets/menu/play5Button.png"); + this.load.image("tutorial-button", "assets/menu/tutorialButton.png"); + this.load.image("instruction-1", "assets/tutorial/instruction1.png"); + this.load.image("instruction-2", "assets/tutorial/instruction2.png"); + this.load.image("instruction-3", "assets/tutorial/instruction3.png"); + this.load.image("instruction-4", "assets/tutorial/instruction4.png"); + this.load.image("instruction-5", "assets/tutorial/instruction5.png"); + this.load.image("play-again-button", "assets/menu/PlayAgainButton.png"); + this.load.image("main-menu-button", "assets/menu/MainMenuButton.png"); + this.load.image("resume-button", "assets/menu/ResumeButton.png"); + this.load.image("pause-button", "assets/menu/PauseButton.png"); + this.load.image("5x5-backplate", "assets/backgrounds/5x5backplate.png"); + this.load.image("3x3-backplate", "assets/backgrounds/3x3backplate.png"); + this.load.audio("button-press", "assets/audio/effects/click.mp3"); + this.load.audio("menu-music", "assets/audio/music/puzzlemenu.ogg"); + this.load.audio("block-break", "assets/audio/effects/cork.mp3"); + this.load.audio( + "gameplay-music", + "assets/audio/music/contemplation.mp3" + ); + this.load.spritesheet( + "green-break", + "assets/effects/effects/green.png", + { + frameWidth: 192, // width of each frame + frameHeight: 192, // height of each frame + endFrame: 5, // number of frames in the spritesheet + } + ); + this.load.spritesheet("red-break", "assets/effects/effects/red.png", { + frameWidth: 192, // width of each frame + frameHeight: 192, // height of each frame + endFrame: 5, // number of frames in the spritesheet + }); + this.load.spritesheet( + "yellow-break", + "assets/effects/effects/yellow.png", + { + frameWidth: 192, // width of each frame + frameHeight: 192, // height of each frame + endFrame: 5, // number of frames in the spritesheet + } + ); + this.load.spritesheet("blue-break", "assets/effects/effects/blue.png", { + frameWidth: 192, // width of each frame + frameHeight: 192, // height of each frame + endFrame: 5, // number of frames in the spritesheet + }); + this.load.spritesheet( + "purple-break", + "assets/effects/effects/purple.png", + { + frameWidth: 192, // width of each frame + frameHeight: 192, // height of each frame + endFrame: 5, // number of frames in the spritesheet + } + ); + } + + // Function to create break animations + private createBreakAnimations() { + const breakConfig = { + frameRate: 10, + repeat: 0, + hideOnComplete: true, + }; + + // Animation creation for each color + this.anims.create({ + key: "greenBreak", + frames: this.anims.generateFrameNumbers("green-break", { + start: 0, + end: 4, + }), + ...breakConfig, + }); + // Repeat for other colors + this.anims.create({ + key: "redBreak", + frames: this.anims.generateFrameNumbers("red-break", { + start: 0, + end: 4, + }), + ...breakConfig, + }); + this.anims.create({ + key: "yellowBreak", + frames: this.anims.generateFrameNumbers("yellow-break", { + start: 0, + end: 4, + }), + ...breakConfig, + }); + this.anims.create({ + key: "blueBreak", + frames: this.anims.generateFrameNumbers("blue-break", { + start: 0, + end: 4, + }), + ...breakConfig, + }); + this.anims.create({ + key: "purpleBreak", + frames: this.anims.generateFrameNumbers("purple-break", { + start: 0, + end: 4, + }), + ...breakConfig, + }); } create() { - this.scene.start("MainScene"); + this.createBreakAnimations(); + this.scene.start("MenuScene"); } } diff --git a/src/scenes/threeByThreeLevel.ts b/src/scenes/threeByThreeLevel.ts new file mode 100644 index 00000000..7ca62a31 --- /dev/null +++ b/src/scenes/threeByThreeLevel.ts @@ -0,0 +1,186 @@ +import Phaser from "phaser"; +import BlockGrid from "../objects/blockGrid"; +import BooleanBlock from "../objects/booleanBlock"; +import ScoreDisplay from "../objects/scoreDisplay"; +import PauseMenu from "../objects/pausemenu"; + +function updateHighScore(newScore: number, mode: string) { + const key = `highScore-${mode}`; + const currentHighScore = + parseInt(localStorage.getItem(key) ?? "0", 10) || 0; + if (newScore > currentHighScore) { + localStorage.setItem(key, newScore.toString()); + } +} + +export default class ThreeByThreeLevel extends Phaser.Scene { + locationBuffer: [number, number] | undefined; + blockGrid: BlockGrid; + timer: Phaser.Time.TimerEvent; + timeLimitInSeconds: number; + timerText: Phaser.GameObjects.Text; + gameplayMusic: Phaser.Sound.BaseSound; + scoreDisplay: ScoreDisplay; + pauseButton: Phaser.GameObjects.Image; + pauseMenu: PauseMenu; + paused: boolean; + background: Phaser.GameObjects.Image; + + constructor() { + super({ key: "ThreeByThreeLevel" }); + } + + create() { + this.paused = false; + this.timeLimitInSeconds = 120; + this.background = new Phaser.GameObjects.Image( + this, + 640, + 360, + "3x3-backplate" + ); + this.add.existing(this.background); + this.blockGrid = new BlockGrid(this, 3, false); // Initialize a 3x3 grid + this.gameplayMusic = this.sound.add("gameplay-music"); + this.gameplayMusic.play({ volume: 0.3, loop: true }); + this.scoreDisplay = new ScoreDisplay(this, 900, 38); + + this.input.on("pointerdown", this.mouseClick, this); + + this.timer = this.time.addEvent({ + delay: 1000, + callback: () => { + this.timeLimitInSeconds--; + if (this.timeLimitInSeconds <= 0) { + updateHighScore(this.scoreDisplay.getScore(), "3x3"); + this.gameplayMusic.stop(); + this.scene.start("PostLevelScene", { + finalScore: this.scoreDisplay.getScore(), + }); + } + }, + callbackScope: this, + loop: true, + }); + + this.timerText = this.add + .text(550, 77, `${this.timeLimitInSeconds}`, { + fontFamily: "Arial", + color: "#000000", + fontSize: "45px", + }) + .setOrigin(1, 1); + + this.pauseButton = new Phaser.GameObjects.Image( + this, + 50, + 50, + "pause-button" + ) + .setScale(0.1) + .setInteractive() + .on("pointerdown", this.clickPause, this); + this.add.existing(this.pauseButton); + + this.blockGrid.setX(528); + this.blockGrid.setY(250); + } + + mouseClick( + pointer: Phaser.Input.Pointer, + currentlyOver: Array + ) { + if (!this.paused && currentlyOver[0] instanceof BooleanBlock) { + const currentBlock = currentlyOver[0] as BooleanBlock; + const currentLocation = currentBlock.getGridLocation(); + + if (this.locationBuffer === undefined) { + // No block is currently selected, select this one + this.locationBuffer = currentLocation; + currentBlock.setTint(0xfff300); // Tint the selected block + } else { + // Try to retrieve the previously selected block safely + const previousBlock = this.blockGrid.getBlockAtLocation( + this.locationBuffer + ); + if (previousBlock !== null) { + previousBlock.clearTint(); // Safely clear the tint only if previousBlock is not null + } + + if ( + this.locationBuffer[0] === currentLocation[0] && + this.locationBuffer[1] === currentLocation[1] + ) { + // The same block was clicked again deselect it + this.locationBuffer = undefined; + } else if (previousBlock) { + // A different block was clicked and previousBlock is not null -> swap + let promises = this.blockGrid.switchBlocks( + currentLocation, + this.locationBuffer + ); + this.locationBuffer = undefined; + Promise.all(promises).then(() => { + const matches: number = this.blockGrid.checkForTruthy(); + this.scoreDisplay.incrementScore(matches); + }); + } + } + } + } + + clickPause() { + if (!this.paused) { + this.paused = true; + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseButton.setScale(0.09); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.pauseButton.setScale(0.1); + this.timer.paused = true; + this.pauseMenu = new PauseMenu( + this, + this.resumeFunc, + this.mainMenuFunc + ); + this.add.existing(this.pauseMenu); + }); + } + } + + mainMenuFunc() { + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseMenu.mainMenuButton.setScale(0.9); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.pauseMenu.mainMenuButton.setScale(1); + this.gameplayMusic.pause(); + this.scene.start("MenuScene"); + }); + } + + resumeFunc() { + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseMenu.resumeButton.setScale(0.9); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.pauseMenu.resumeButton.setScale(1); + this.pauseMenu.destroy(); + this.timer.paused = false; + this.paused = false; + }); + } + + update() { + this.timerText.setText(`${this.timeLimitInSeconds}`); + } +} diff --git a/src/scenes/tutorial.ts b/src/scenes/tutorial.ts new file mode 100644 index 00000000..0d458561 --- /dev/null +++ b/src/scenes/tutorial.ts @@ -0,0 +1,185 @@ +import Phaser from "phaser"; +import BlockGrid from "../objects/blockGrid"; +import BooleanBlock from "../objects/booleanBlock"; +import ScoreDisplay from "../objects/scoreDisplay"; +import PauseMenu from "../objects/pausemenu"; + +export default class TutorialLevel extends Phaser.Scene { + locationBuffer: [number, number] | undefined; + blockGrid: BlockGrid; + gameplayMusic: Phaser.Sound.BaseSound; + scoreDisplay: ScoreDisplay; + instructionImage: Phaser.GameObjects.Image; + hasMovedBlock: boolean; + pauseButton: Phaser.GameObjects.Image; + pauseMenu: PauseMenu; + paused: boolean; + currentTutorialStep: number; + background: Phaser.GameObjects.Image; + + constructor() { + super({ key: "TutorialLevel" }); + this.currentTutorialStep = 1; + } + + create() { + this.background = new Phaser.GameObjects.Image( + this, + 640, + 360, + "3x3-backplate" + ); + this.add.existing(this.background); + this.blockGrid = new BlockGrid(this, 3, false); // Initialize a 3x3 grid + this.gameplayMusic = this.sound.add("gameplay-music"); + this.gameplayMusic.play({ volume: 0.3, loop: true }); + this.scoreDisplay = new ScoreDisplay(this, 900, 38); + this.currentTutorialStep = 1; + + this.input.on("pointerdown", this.mouseClick, this); + + this.instructionImage = this.add + .image(180, 600, "instruction-1") + .setScale(0.7); + + this.hasMovedBlock = false; + this.pauseButton = new Phaser.GameObjects.Image( + this, + 50, + 50, + "pause-button" + ) + .setScale(0.1) + .setInteractive() + .on("pointerdown", this.clickPause, this); + this.add.existing(this.pauseButton); + + this.blockGrid.setX(528); + this.blockGrid.setY(250); + } + + updateInstructionImage() { + switch (this.currentTutorialStep) { + case 1: + this.instructionImage.setTexture("instruction-1"); + break; + case 2: + this.instructionImage.setTexture("instruction-2"); + break; + case 3: + this.instructionImage.setTexture("instruction-3"); + break; + } + } + + clickPause() { + if (!this.paused) { + this.paused = true; + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseButton.setScale(0.09); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.pauseButton.setScale(0.1); + this.pauseMenu = new PauseMenu( + this, + this.resumeFunc, + this.mainMenuFunc + ); + this.add.existing(this.pauseMenu); + }); + } + } + + mainMenuFunc() { + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseMenu.mainMenuButton.setScale(0.9); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.pauseMenu.mainMenuButton.setScale(1); + this.gameplayMusic.pause(); + this.scene.start("MenuScene"); + }); + } + + resumeFunc() { + let promise = new Promise((resolve: () => void) => { + this.sound.play("button-press", { volume: 0.4 }); + this.pauseMenu.resumeButton.setScale(0.9); + setTimeout(resolve, 200); + }); + + Promise.resolve(promise).then(() => { + this.paused = false; + this.pauseMenu.resumeButton.setScale(1); + this.pauseMenu.destroy(); + }); + } + + mouseClick( + pointer: Phaser.Input.Pointer, + currentlyOver: Array + ) { + if (!this.paused && currentlyOver[0] instanceof BooleanBlock) { + const currentBlock = currentlyOver[0] as BooleanBlock; + const currentLocation = currentBlock.getGridLocation(); + + if (this.locationBuffer === undefined) { + // No block is currently selected, select this one + this.locationBuffer = currentLocation; + currentBlock.setTint(0xfff300); // Tint the selected block + } else { + // Try to retrieve the previously selected block safely + const previousBlock = this.blockGrid.getBlockAtLocation( + this.locationBuffer + ); + if (previousBlock !== null) { + previousBlock.clearTint(); // Safely clear the tint only if previousBlock is not null + } + + if ( + this.locationBuffer[0] === currentLocation[0] && + this.locationBuffer[1] === currentLocation[1] + ) { + // The same block was clicked again deselect it + this.locationBuffer = undefined; + } else if (previousBlock) { + // A different block was clicked and previousBlock is not null -> swap + let promises = this.blockGrid.switchBlocks( + currentLocation, + this.locationBuffer + ); + this.locationBuffer = undefined; + Promise.all(promises).then(() => { + const matches: number = this.blockGrid.checkForTruthy(); + this.scoreDisplay.incrementScore(matches); + + if (this.currentTutorialStep === 1) { + // Player has moved a block for the first time + this.currentTutorialStep = 2; // Advance to next step regardless of making a True + this.updateInstructionImage(); + } else if ( + this.currentTutorialStep === 2 && + matches > 0 + ) { + // Player has successfully created a True statement in the correct step + this.currentTutorialStep = 3; + this.updateInstructionImage(); + } + }); + } + } + } + } + + update() { + if (this.scoreDisplay.getScore() >= 12) { + this.scene.start("AdvancedTutorial"); + } + } +}