├── .gitignore ├── LICENSE.md ├── README.md ├── knifehit ├── assets │ ├── images │ │ ├── background │ │ │ ├── bg1.png │ │ │ └── bg2.png │ │ ├── boss │ │ │ ├── apple-pie.png │ │ │ └── moon.png │ │ ├── collectible │ │ │ └── full_apple.png │ │ ├── knife-count │ │ │ ├── background.png │ │ │ └── foreground.png │ │ ├── knife │ │ │ └── knife1.png │ │ ├── logo.png │ │ ├── logo.psd │ │ └── target │ │ │ ├── wood1-debris1.png │ │ │ ├── wood1-debris2.png │ │ │ ├── wood1-debris3.png │ │ │ └── wood1.png │ └── sounds │ │ ├── boss1_destroyed.mp3 │ │ ├── boss1_hitted.mp3 │ │ ├── knife_propelled.mp3 │ │ ├── target_destroyed.mp3 │ │ └── target_hitted.mp3 ├── boss.py ├── config.yaml ├── gamemanager.py ├── knife.py ├── knifecount.py ├── knifehit.py ├── obstacle.py └── target.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.vscode 4 | *__pycache__ 5 | /venv/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Akmal Hakimi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Knife Hit 2 | 3 | A Python clone of the popular hyper-casual mobile game called "Knife Hit" powered by [Arcade](https://github.com/pvcraven/arcade) 4 | 5 | ![alt text](https://i.imgur.com/cDU2nNj.jpg) 6 | 7 | Knife Hit is a popular hyper-casual mobile game published by [Ketchapp](http://www.ketchappgames.com/) and developed by [Estoty](http://estoty.com/). The game has over 100 million downloads in both Apple App Store and Google Play Store. This project is an attempt to recreate the game using Python. 8 | 9 | ## Features 10 | * Challenging but fun mechanics 11 | * Unique boss fights 12 | * Randomly generated obstacle 13 | * No Ads or In App Purchase!! 14 | 15 | ## Pending Features 16 | * Add more boss 17 | * Add target entry animation 18 | * Add target destroyed animation 19 | * Add apple/coin mechanics 20 | * Add more rotation mode 21 | 22 | ## Dependencies Installation 23 | 24 | To install the dependencies, navigate to the project directory and execute this command: 25 | 26 | ```bash 27 | python3 -m venv venv 28 | source venv/bin/activate 29 | pip install -r requirements.txt 30 | cd knifehit 31 | python knifehit.py 32 | ``` 33 | 34 | ## Screenshot 35 | ![alt text](https://i.imgur.com/A2nQjqv.png) 36 | 37 | ## Contributing 38 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 39 | 40 | ## License 41 | This project is licensed under the [MIT License](https://choosealicense.com/licenses/mit/) -------------------------------------------------------------------------------- /knifehit/assets/images/background/bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/background/bg1.png -------------------------------------------------------------------------------- /knifehit/assets/images/background/bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/background/bg2.png -------------------------------------------------------------------------------- /knifehit/assets/images/boss/apple-pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/boss/apple-pie.png -------------------------------------------------------------------------------- /knifehit/assets/images/boss/moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/boss/moon.png -------------------------------------------------------------------------------- /knifehit/assets/images/collectible/full_apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/collectible/full_apple.png -------------------------------------------------------------------------------- /knifehit/assets/images/knife-count/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/knife-count/background.png -------------------------------------------------------------------------------- /knifehit/assets/images/knife-count/foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/knife-count/foreground.png -------------------------------------------------------------------------------- /knifehit/assets/images/knife/knife1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/knife/knife1.png -------------------------------------------------------------------------------- /knifehit/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/logo.png -------------------------------------------------------------------------------- /knifehit/assets/images/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/logo.psd -------------------------------------------------------------------------------- /knifehit/assets/images/target/wood1-debris1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/target/wood1-debris1.png -------------------------------------------------------------------------------- /knifehit/assets/images/target/wood1-debris2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/target/wood1-debris2.png -------------------------------------------------------------------------------- /knifehit/assets/images/target/wood1-debris3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/target/wood1-debris3.png -------------------------------------------------------------------------------- /knifehit/assets/images/target/wood1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/images/target/wood1.png -------------------------------------------------------------------------------- /knifehit/assets/sounds/boss1_destroyed.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/sounds/boss1_destroyed.mp3 -------------------------------------------------------------------------------- /knifehit/assets/sounds/boss1_hitted.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/sounds/boss1_hitted.mp3 -------------------------------------------------------------------------------- /knifehit/assets/sounds/knife_propelled.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/sounds/knife_propelled.mp3 -------------------------------------------------------------------------------- /knifehit/assets/sounds/target_destroyed.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/sounds/target_destroyed.mp3 -------------------------------------------------------------------------------- /knifehit/assets/sounds/target_hitted.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akmalmzamri/python-knife-hit/44c04aa0c471c64a21231ed14aa2722a61dc11d8/knifehit/assets/sounds/target_hitted.mp3 -------------------------------------------------------------------------------- /knifehit/boss.py: -------------------------------------------------------------------------------- 1 | import arcade 2 | import random 3 | from enum import Enum 4 | 5 | class RotationMode(Enum): 6 | """ Store rotation mode in enum """ 7 | NORMAL = 1 8 | REVERSE = 2 9 | BACK_AND_FORTH = 3 10 | FAST_AND_SLOW = 4 11 | 12 | class Boss(arcade.Sprite): 13 | """ 14 | Boss class 15 | """ 16 | 17 | def __init__(self, GAME_CONFIG, scale_ratio=1, rotation_mode=None): 18 | """ Initialize target """ 19 | 20 | self.SCREEN_WIDTH = GAME_CONFIG["general_settings"]["screen_width"] 21 | self.SCREEN_HEIGHT = GAME_CONFIG["general_settings"]["screen_height"] 22 | self.SPRITE_SCALING = GAME_CONFIG["general_settings"]["sprite_scaling"] 23 | self.TARGET_POSITION = (self.SCREEN_WIDTH//2, self.SCREEN_HEIGHT*(0.7)) 24 | self.TARGET_ROTATION_SPEED = GAME_CONFIG["target_settings"]["rotation_speed"] 25 | self.TARGET_IMAGE = GAME_CONFIG["assets_path"]["images"]["boss"]["boss1"] 26 | 27 | super().__init__(self.TARGET_IMAGE, self.SPRITE_SCALING/scale_ratio) 28 | 29 | self.TARGET_HITTED_SOUND = GAME_CONFIG["assets_path"]["sounds"]["boss1_hitted"] 30 | self.TARGET_HITTED_SOUND = arcade.load_sound(self.TARGET_HITTED_SOUND) 31 | 32 | self.center_x = self.TARGET_POSITION[0] 33 | self.center_y = self.TARGET_POSITION[1] 34 | # self.angle = random.randrange(360) 35 | # self.change_angle = self.TARGET_ROTATION_SPEED 36 | 37 | self.hit_impact_animation = False 38 | self.original_position = self.center_y 39 | 40 | self.impact_animation_counter = 0 41 | self.rotation_animation_counter = 0 42 | self.rotation_state = None 43 | 44 | if rotation_mode is None: 45 | self.rotation_mode = random.choice(list(RotationMode)) 46 | else: 47 | self.rotation_mode = rotation_mode 48 | 49 | # Override the rotation mode here for testing purposed 50 | self.rotation_mode = RotationMode.BACK_AND_FORTH 51 | 52 | if self.rotation_mode == RotationMode.NORMAL: 53 | pass 54 | elif self.rotation_mode == RotationMode.REVERSE: 55 | self.TARGET_ROTATION_SPEED = -self.TARGET_ROTATION_SPEED 56 | elif self.rotation_mode == RotationMode.FAST_AND_SLOW: 57 | self.rotation_state = "Decreasing" 58 | elif self.rotation_mode == RotationMode.BACK_AND_FORTH: 59 | self.rotation_state = "Back" 60 | self.rotation_direction = 1 61 | elif self.rotation_mode == "STATIC": 62 | self.TARGET_ROTATION_SPEED = 0 63 | else: 64 | pass 65 | 66 | def hit_impact(self): 67 | """ Initialize the "hit impact" state """ 68 | 69 | self.hit_impact_animation = True 70 | self.impact_animation_counter = 3 71 | 72 | def update(self): 73 | """ Movement and game logic """ 74 | 75 | # Rotate the target. 76 | # The arcade.Sprite class has an "angle" attribute that controls 77 | # the sprite rotation. Change this, and the sprite rotates. 78 | 79 | if self.rotation_mode == RotationMode.FAST_AND_SLOW: 80 | if self.rotation_state is "Decreasing": 81 | self.TARGET_ROTATION_SPEED -= 0.005 82 | if self.rotation_state is "Decreasing" and self.TARGET_ROTATION_SPEED <= 0: 83 | self.rotation_state = "Increasing" 84 | if self.rotation_state is "Increasing": 85 | self.TARGET_ROTATION_SPEED += 0.01 86 | if self.rotation_state is "Increasing" and self.TARGET_ROTATION_SPEED >= 3: 87 | self.rotation_state = "Decreasing" 88 | 89 | if self.rotation_mode == RotationMode.BACK_AND_FORTH: 90 | if self.rotation_state is "Back": 91 | self.TARGET_ROTATION_SPEED -= 0.01 92 | if self.rotation_state is "Back" and self.TARGET_ROTATION_SPEED <= -4: 93 | self.rotation_state = "Forth" 94 | if self.rotation_state is "Forth": 95 | self.TARGET_ROTATION_SPEED += 0.05 96 | if self.rotation_state is "Forth" and self.TARGET_ROTATION_SPEED >= 4: 97 | self.rotation_state = "Back" 98 | 99 | 100 | self.angle += self.TARGET_ROTATION_SPEED 101 | 102 | # Play the hit impact animation 103 | if self.hit_impact_animation and self.impact_animation_counter > 0: 104 | self.center_y += 3 105 | self.color = (255, 179, 179) 106 | self.impact_animation_counter -= 1 107 | else: 108 | self.color = (255, 255, 255) 109 | self.hit_impact_animation = False 110 | self.impact_animation_counter = 0 111 | self.center_y = self.original_position 112 | 113 | -------------------------------------------------------------------------------- /knifehit/config.yaml: -------------------------------------------------------------------------------- 1 | assets_path: 2 | sounds: 3 | knife_propelled: "assets/sounds/knife_propelled.mp3" 4 | target_hitted: "assets/sounds/target_destroyed.mp3" 5 | target_destroyed: "assets/sounds/target_hitted.mp3" 6 | boss1_hitted: "assets/sounds/boss1_hitted.mp3" 7 | boss2_destroyed: "assets/sounds/boss2_destroyed.mp3" 8 | images: 9 | logo: "assets/images/logo.png" 10 | background_ingame: "assets/images/background/bg1.png" 11 | background_gameover: "assets/images/background/bg2.png" 12 | knife: "assets/images/knife/knife1.png" 13 | target: "assets/images/target/wood1.png" 14 | knife_count: 15 | foreground: "assets/images/knife-count/foreground.png" 16 | background: "assets/images/knife-count/background.png" 17 | boss: 18 | boss1: "assets/images/boss/apple-pie.png" 19 | boss2: "assets/images/boss/moon.png" 20 | general_settings: 21 | sprite_scaling: 0.5 22 | screen_width: 540 23 | screen_height: 960 24 | screen_title: "Python Knife Hit" 25 | game_over_delay: 1 26 | max_knife_count: 10 27 | min_knife_count: 5 28 | knife_settings: 29 | speed: 70 30 | target_settings: 31 | rotation_speed: 2 32 | -------------------------------------------------------------------------------- /knifehit/gamemanager.py: -------------------------------------------------------------------------------- 1 | import arcade 2 | import math 3 | import os 4 | import random 5 | import threading 6 | import time 7 | from enum import Enum 8 | 9 | from boss import Boss 10 | from knife import Knife 11 | from knifecount import KnifeCount 12 | from target import Target 13 | from obstacle import Obstacle 14 | 15 | class GameState(Enum): 16 | """ Store game state in enum """ 17 | 18 | MENU = 1 19 | GAME_RUNNING = 2 20 | TARGET_DEFEATED = 3 21 | GAME_OVER = 4 22 | 23 | 24 | class GameManager(arcade.Window): 25 | """ 26 | Main application class. 27 | """ 28 | 29 | def __init__(self, GAME_CONFIG): 30 | """ Initializer """ 31 | 32 | # Get config data 33 | self.GAME_CONFIG = GAME_CONFIG 34 | self.SCREEN_WIDTH = GAME_CONFIG["general_settings"]["screen_width"] 35 | self.SCREEN_HEIGHT = GAME_CONFIG["general_settings"]["screen_height"] 36 | self.SCREEN_TITLE = GAME_CONFIG["general_settings"]["screen_title"] 37 | self.SPRITE_SCALING = GAME_CONFIG["general_settings"]["sprite_scaling"] 38 | self.GAME_OVER_DELAY = GAME_CONFIG["general_settings"]["game_over_delay"] 39 | self.BACKGROUND_INGAME = GAME_CONFIG["assets_path"]["images"]["background_ingame"] 40 | self.BACKGROUND_GAMEOVER = GAME_CONFIG["assets_path"]["images"]["background_gameover"] 41 | 42 | self.MAX_KNIFE_COUNT = GAME_CONFIG["general_settings"]["max_knife_count"] 43 | self.MIN_KNIFE_COUNT = GAME_CONFIG["general_settings"]["min_knife_count"] 44 | # self.MAX_OBSTACLE_COUNT = GAME_CONFIG["general_settings"]["max_obstacle_count"] 45 | 46 | # Call the parent class initializer 47 | super().__init__(self.SCREEN_WIDTH, self.SCREEN_HEIGHT, self.SCREEN_TITLE) 48 | 49 | # Set the working directory (where we expect to find files) to the same 50 | # directory this .py file is in. You can leave this out of your own 51 | # code, but it is needed to easily run the examples using "python -m" 52 | file_path = os.path.dirname(os.path.abspath(__file__)) 53 | os.chdir(file_path) 54 | 55 | # Initialize the game state 56 | self.current_state = GameState.MENU 57 | 58 | # Background image will be stored in this variable 59 | self.background = None 60 | 61 | # Variables that hold individual sprites 62 | self.knife = None 63 | self.target = None 64 | self.target_collider = None 65 | self.knife_count_display = None 66 | self.obstacle = None 67 | 68 | # Variables that will hold sprite lists 69 | self.knife_list = None 70 | self.target_list = None 71 | self.target_collider_list = None 72 | self.knife_count_display_list = None 73 | self.obstacle_list = None 74 | 75 | # Set up the game score 76 | self.score = 0 77 | self.stage = 1 78 | self.score_text = None 79 | self.initial_knife_count = None 80 | self.knife_count = None 81 | 82 | # Don't show the mouse cursor 83 | # self.set_mouse_visible(False) 84 | 85 | # Set the background color 86 | arcade.set_background_color(arcade.color.AMAZON) 87 | 88 | def setup(self): 89 | """ Set up the game and initialize the variables. """ 90 | 91 | # Load the background image. Do this in the setup so we don't keep reloading it all the time. 92 | self.background_ingame = arcade.load_texture(self.BACKGROUND_INGAME) 93 | self.background_gameover = arcade.load_texture(self.BACKGROUND_GAMEOVER) 94 | 95 | # Sprite lists 96 | self.knife_list = arcade.SpriteList() 97 | self.target_list = arcade.SpriteList() 98 | self.target_collider_list = arcade.SpriteList() 99 | self.knife_count_display_list = arcade.SpriteList() 100 | self.obstacle_list = arcade.SpriteList() 101 | 102 | # Set up the game score and knife count 103 | self.knife_count = random.randrange(self.MIN_KNIFE_COUNT, self.MAX_KNIFE_COUNT) 104 | self.initial_knife_count = self.knife_count 105 | 106 | # Set up the knife 107 | self.create_knife() 108 | 109 | # Set up knife count display 110 | self.create_knife_count_display() 111 | 112 | # Set up the target 113 | if self.stage % 5 == 0: 114 | self.create_boss() 115 | else: 116 | self.create_target() 117 | 118 | # Set up the target collider 119 | self.create_target_collider() 120 | 121 | # Set up the obstacle 122 | self.create_obstacle() 123 | 124 | def draw_menu(self): 125 | """ Draw main menu across the screen. """ 126 | 127 | # Draw the background texture 128 | arcade.draw_texture_rectangle( 129 | self.SCREEN_WIDTH // 2, 130 | self.SCREEN_HEIGHT // 2, 131 | self.SCREEN_WIDTH, 132 | self.SCREEN_HEIGHT, 133 | self.background_gameover) 134 | 135 | # Draw logo 136 | self.logo_list = arcade.SpriteList() 137 | self.logo = arcade.Sprite(self.GAME_CONFIG["assets_path"]["images"]["logo"], 0.5) 138 | self.logo.center_x = self.SCREEN_WIDTH*0.5 139 | self.logo.center_y = self.SCREEN_HEIGHT*0.6 140 | self.logo_list.append(self.logo) 141 | self.logo_list.draw() 142 | 143 | # Display "Game Over" text 144 | # output = "Python Knife Hit" 145 | # arcade.draw_text(output, self.SCREEN_WIDTH*0.5, self.SCREEN_HEIGHT*0.6, arcade.color.WHITE, 54, align="center", anchor_x="center") 146 | 147 | # Display restart instruction 148 | output = "Press To Start" 149 | arcade.draw_text(output, self.SCREEN_WIDTH*0.5, self.SCREEN_HEIGHT*0.35, arcade.color.WHITE, 24, align="center", anchor_x="center") 150 | 151 | def draw_game_over(self): 152 | """ Draw game over menu across the screen. """ 153 | 154 | # Reset the score and stage number 155 | self.score = 0 156 | self.stage = 1 157 | 158 | # Draw the background texture 159 | arcade.draw_texture_rectangle( 160 | self.SCREEN_WIDTH // 2, 161 | self.SCREEN_HEIGHT // 2, 162 | self.SCREEN_WIDTH, 163 | self.SCREEN_HEIGHT, 164 | self.background_gameover) 165 | 166 | # Display "Game Over" text 167 | output = "Game Over" 168 | arcade.draw_text(output, self.SCREEN_WIDTH*0.5, self.SCREEN_HEIGHT*0.6, arcade.color.WHITE, 54, align="center", anchor_x="center") 169 | 170 | # Display restart instruction 171 | output = "Press To Restart" 172 | arcade.draw_text(output, self.SCREEN_WIDTH*0.5, self.SCREEN_HEIGHT*0.55, arcade.color.WHITE, 24, align="center", anchor_x="center") 173 | 174 | def draw_game(self): 175 | """ Draw all the sprites, along with the score. """ 176 | 177 | # Draw the background texture 178 | arcade.draw_texture_rectangle( 179 | self.SCREEN_WIDTH // 2, 180 | self.SCREEN_HEIGHT // 2, 181 | self.SCREEN_WIDTH, 182 | self.SCREEN_HEIGHT, 183 | self.background_ingame 184 | ) 185 | 186 | # Draw all the sprites. 187 | self.knife_list.draw() 188 | self.obstacle_list.draw() 189 | self.target_list.draw() 190 | self.knife_count_display_list.draw() 191 | 192 | # Display score 193 | output = f"Press to shoot" 194 | arcade.draw_text( 195 | output, self.SCREEN_WIDTH*0.5, self.SCREEN_HEIGHT*0.05, (255,255,255), 12, 196 | align="center", anchor_x="center", anchor_y="center", 197 | ) 198 | 199 | # Display score 200 | output = f"{self.score}" 201 | arcade.draw_text( 202 | output, self.SCREEN_WIDTH*0.1, self.SCREEN_HEIGHT*0.95, (239, 182, 90), 28, 203 | align="center", anchor_x="center", anchor_y="center", 204 | ) 205 | 206 | # Display stage number 207 | if self.stage % 5 == 0: 208 | output = f"BOSS FIGHT!!" 209 | arcade.draw_text( 210 | output, self.SCREEN_WIDTH*0.5, self.SCREEN_HEIGHT*0.95, (223, 87, 84), 32, 211 | align="center", anchor_x="center", anchor_y="center" 212 | ) 213 | 214 | else: 215 | output = f"STAGE {self.stage}" 216 | arcade.draw_text( 217 | output, self.SCREEN_WIDTH*0.5, self.SCREEN_HEIGHT*0.95, arcade.color.WHITE, 28, 218 | align="center", anchor_x="center", anchor_y="center" 219 | ) 220 | 221 | def on_draw(self): 222 | """ Render the screen. """ 223 | 224 | # This command has to happen before we start drawing 225 | arcade.start_render() 226 | 227 | # Redirect to main menu 228 | if self.current_state == GameState.MENU: 229 | self.draw_menu() 230 | 231 | # Redirect to in game screen 232 | elif self.current_state == GameState.GAME_RUNNING: 233 | self.draw_game() 234 | 235 | # Redirect to game over screen 236 | else: 237 | # self.draw_game() 238 | self.draw_game_over() 239 | 240 | def update(self, delta_time): 241 | """ Movement and game logic """ 242 | 243 | # Run update function of every object 244 | self.target_list.update() 245 | self.knife_list.update() 246 | # self.target_collider_list.update() 247 | self.obstacle_list.update() 248 | 249 | # Check if knife collided with the knifes stucked in the target. 250 | obstacle_hit_list = arcade.check_for_collision_with_list(self.knife, self.obstacle_list) 251 | if not self.knife.target_hitted: 252 | for collided_object in obstacle_hit_list: 253 | # Show "knife propelled" animation 254 | self.knife.propel_knife(self.target) 255 | 256 | # Put the game over trigger in a thread so we can show the "knife propelled" animation 257 | game_over_trigger_thread = threading.Thread(target=self.trigger_game_over, args=(self.GAME_OVER_DELAY,)) 258 | game_over_trigger_thread.start() 259 | 260 | 261 | # Check if knife collided with the knifes stucked in the target. 262 | knife_hit_list = arcade.check_for_collision_with_list(self.knife, self.knife_list) 263 | if not self.knife.target_hitted: 264 | for collided_object in knife_hit_list: 265 | # Show "knife propelled" animation 266 | self.knife.propel_knife(self.target) 267 | 268 | # Put the game over trigger in a thread so we can show the "knife propelled" animation 269 | game_over_trigger_thread = threading.Thread(target=self.trigger_game_over, args=(self.GAME_OVER_DELAY,)) 270 | game_over_trigger_thread.start() 271 | 272 | # Check if knife collided with the target. 273 | target_hit_list = arcade.check_for_collision_with_list(self.knife, self.target_collider_list) 274 | if not self.knife.target_hitted and not self.knife.knife_hitted: 275 | for collided_object in target_hit_list: 276 | # Add 1 to the score when knife successfully hit the target 277 | self.score += 1 278 | 279 | # Play the target hit impact animation 280 | self.target.hit_impact() 281 | 282 | # Calculate the knife angular rotation radius based on collider height 283 | rotation_radius = (self.target_collider.height/2)+30 284 | self.knife.hit_target(self.target) 285 | 286 | # Spawn new knife 287 | if self.knife_count > 0: 288 | self.create_knife() 289 | else: 290 | # self.current_state = GameState.TARGET_DEFEATED 291 | self.create_new_stage() 292 | 293 | def on_key_press(self, key, modifiers): 294 | """ Called whenever a key is pressed. """ 295 | 296 | # Shoot knife 297 | if key == arcade.key.SPACE and self.knife_count > 0 and self.current_state == GameState.GAME_RUNNING: 298 | self.knife_count -= 1 299 | 300 | # Play knife shooting animation 301 | self.knife.shoot_knife() 302 | 303 | # Remove the foreground image of knife count 304 | # to indicates how many knife we had used 305 | 306 | # This is the ideal way to do this but will cause error 307 | # self.knife_count_display_list.pop() 308 | 309 | # Instead of removing the sprite from sprite list, 310 | # we simply set the opacity to 0 311 | knife_used = self.initial_knife_count - self.knife_count 312 | self.knife_count_display_list[-knife_used].alpha = (0) 313 | 314 | # Restart game 315 | if ( 316 | key == arcade.key.ENTER and 317 | ( 318 | self.current_state == GameState.GAME_OVER or 319 | self.current_state == GameState.MENU 320 | ) 321 | ): 322 | self.setup() 323 | self.current_state = GameState.GAME_RUNNING 324 | 325 | def create_new_stage(self): 326 | """ Create new stage """ 327 | # Reset these variables 328 | # Variables that hold individual sprites 329 | self.knife = None 330 | self.target = None 331 | self.target_collider = None 332 | self.knife_count_display = None 333 | self.obstacle = None 334 | 335 | # Variables that will hold sprite lists 336 | self.knife_list = None 337 | self.target_list = None 338 | self.target_collider_list = None 339 | self.knife_count_display_list = None 340 | self.obstacle_list = None 341 | 342 | self.stage += 1 343 | 344 | self.setup() 345 | self.current_state = GameState.GAME_RUNNING 346 | 347 | def create_target(self): 348 | """ Create new target """ 349 | self.target = Target(self.GAME_CONFIG, scale_ratio=1.2) 350 | self.target_list.append(self.target) 351 | 352 | def create_boss(self): 353 | """ Create new target """ 354 | self.target = Boss(self.GAME_CONFIG, scale_ratio=1.2) 355 | self.target_list.append(self.target) 356 | 357 | def create_target_collider(self): 358 | """ Create new target collider """ 359 | self.target_collider = Target(self.GAME_CONFIG, scale_ratio=1.5, rotation_mode="STATIC") 360 | self.target_collider_list.append(self.target_collider) 361 | 362 | def create_knife(self): 363 | """ Create new knife """ 364 | self.knife = Knife(self.GAME_CONFIG) 365 | self.knife_list.append(self.knife) 366 | 367 | def create_knife_count_display(self): 368 | """ Create knife count display """ 369 | # Create the background display 370 | for i in range(self.knife_count): 371 | self.knife_count_display = KnifeCount(self.GAME_CONFIG, "background", i) 372 | self.knife_count_display_list.append(self.knife_count_display) 373 | 374 | # Create the foreground display 375 | for i in range(self.knife_count): 376 | self.knife_count_display = KnifeCount(self.GAME_CONFIG, "foregroud", i) 377 | self.knife_count_display_list.append(self.knife_count_display) 378 | 379 | def create_obstacle(self): 380 | """ Create obstacle if stage is more than 1 """ 381 | 382 | rotation_speed = self.target.TARGET_ROTATION_SPEED 383 | rotation_radius = (self.target_collider.height/2)+30 384 | rotation_center = self.target.TARGET_POSITION 385 | 386 | if self.stage > 1: 387 | # Max obstacle count are inversely proportional to stage knife count 388 | max_obstacle_count = (self.MAX_KNIFE_COUNT - self.initial_knife_count)+2 389 | obstacle_count = random.randrange(0, max_obstacle_count) 390 | 391 | current_rotation = [] 392 | for i in range(obstacle_count): 393 | while True: 394 | # Randomize the obstacle position 395 | initial_rotation_position = random.randrange(0,359) 396 | if not current_rotation: 397 | break 398 | 399 | # Make sure the obstacle are not too close with each other 400 | closest_rotation = min(current_rotation, key=lambda x:abs(x-initial_rotation_position)) 401 | if abs(closest_rotation-initial_rotation_position) > 10: 402 | break 403 | 404 | # Create the obstacle 405 | current_rotation.append(initial_rotation_position) 406 | self.obstacle = Obstacle(self.GAME_CONFIG, self.target, initial_rotation_position) 407 | self.obstacle_list.append(self.obstacle) 408 | 409 | def trigger_game_over(self, delay): 410 | """ Trigger game over after certain delay """ 411 | time.sleep(delay) 412 | self.current_state = GameState.GAME_OVER 413 | 414 | def trigger_next_stage(self, delay): 415 | """ Trigger game over after certain delay """ 416 | time.sleep(delay) 417 | self.current_state = GameState.GAME_OVER 418 | 419 | -------------------------------------------------------------------------------- /knifehit/knife.py: -------------------------------------------------------------------------------- 1 | import arcade 2 | import math 3 | 4 | class Knife(arcade.Sprite): 5 | """ Knife class """ 6 | 7 | def __init__(self, GAME_CONFIG): 8 | """ Initialize knife """ 9 | 10 | self.SCREEN_WIDTH = GAME_CONFIG["general_settings"]["screen_width"] 11 | self.SCREEN_HEIGHT = GAME_CONFIG["general_settings"]["screen_height"] 12 | self.SPRITE_SCALING = GAME_CONFIG["general_settings"]["sprite_scaling"] 13 | 14 | self.KNIFE_POSITION = (self.SCREEN_WIDTH//2, self.SCREEN_HEIGHT*(0.2)) 15 | self.KNIFE_IMAGE = GAME_CONFIG["assets_path"]["images"]["knife"] 16 | self.KNIFE_MOVEMENT_SPEED = GAME_CONFIG["knife_settings"]["speed"] 17 | 18 | self.KNIFE_PROPELLED_SOUND = GAME_CONFIG["assets_path"]["sounds"]["knife_propelled"] 19 | self.KNIFE_PROPELLED_SOUND = arcade.load_sound(self.KNIFE_PROPELLED_SOUND) 20 | 21 | super().__init__(self.KNIFE_IMAGE, self.SPRITE_SCALING/2) 22 | 23 | self.center_x = self.KNIFE_POSITION[0] 24 | self.center_y = self.KNIFE_POSITION[1] 25 | self.rotation = 0 26 | self.target_hitted = False 27 | self.knife_hitted = False 28 | 29 | self.stucked_in_target = None 30 | 31 | # Reshape the collision 32 | self.points = ( 33 | (-self.width // 3, self.height // 2), 34 | (self.width // 3, -self.height // 2), 35 | (-self.width // 3, -self.height // 2), 36 | ) 37 | 38 | def shoot_knife(self): 39 | """ Initialize the "knife shot" state """ 40 | 41 | self.change_y = self.KNIFE_MOVEMENT_SPEED 42 | 43 | def hit_target(self, target): 44 | """ 45 | Initialize the "stuck in target" state 46 | Copy the target's rotation speed and set the rotation radius and center 47 | """ 48 | self.stucked_in_target = target 49 | 50 | arcade.play_sound(self.stucked_in_target.TARGET_HITTED_SOUND) 51 | self.target_hitted = True 52 | self.change_y = 0 53 | # self.rotation_speed = rotation_speed 54 | self.rotation_radius = (self.stucked_in_target.height/2) 55 | self.rotation_center = self.stucked_in_target.TARGET_POSITION 56 | 57 | def propel_knife(self, target): 58 | """ 59 | Initialise the "knife propelled" state 60 | The propel direction is based on the rotation direction and speed 61 | """ 62 | arcade.play_sound(self.KNIFE_PROPELLED_SOUND) 63 | self.stucked_in_target = target 64 | self.knife_hitted = True 65 | 66 | def update(self): 67 | """ Movement and game logic """ 68 | 69 | # Reshape the collision 70 | # self.set_points((self.points)) 71 | 72 | # Play the knife propelled animation 73 | if self.knife_hitted: 74 | self.rotation_speed = self.stucked_in_target.TARGET_ROTATION_SPEED 75 | self.center_y += -30 76 | self.center_x += self.rotation_speed*20 77 | self.angle += self.rotation_speed*20 78 | # Perform shooting animation when "shoot" button is press 79 | else: 80 | self.center_y += self.change_y 81 | 82 | # Stuck the knife into the target and have it spin together 83 | # We added 270 to the rotation because the knife must start from the bottom 84 | if self.target_hitted: 85 | self.rotation_speed = self.stucked_in_target.TARGET_ROTATION_SPEED 86 | self.rotation += self.rotation_speed 87 | self.angle = self.rotation 88 | self.center_x = (self.rotation_radius * math.cos(math.radians(self.rotation+270))) + self.rotation_center[0] 89 | self.center_y = (self.rotation_radius * math.sin(math.radians(self.rotation+270))) + self.rotation_center[1] 90 | 91 | 92 | -------------------------------------------------------------------------------- /knifehit/knifecount.py: -------------------------------------------------------------------------------- 1 | import arcade 2 | import math 3 | 4 | class KnifeCount(arcade.Sprite): 5 | """ 6 | Knife count display class 7 | """ 8 | 9 | def __init__(self, GAME_CONFIG, image_name, knife_count=0): 10 | """ Initialize knife count display """ 11 | 12 | self.SCREEN_WIDTH = GAME_CONFIG["general_settings"]["screen_width"] 13 | self.SCREEN_HEIGHT = GAME_CONFIG["general_settings"]["screen_height"] 14 | self.SPRITE_SCALING = GAME_CONFIG["general_settings"]["sprite_scaling"] 15 | self.KNIFE_COUNT_FG = GAME_CONFIG["assets_path"]["images"]["knife_count"]["foreground"] 16 | self.KNIFE_COUNT_BG = GAME_CONFIG["assets_path"]["images"]["knife_count"]["background"] 17 | 18 | # Check whether the display is for background or foreground 19 | self.image_name = self.KNIFE_COUNT_FG 20 | if(image_name=="background"): 21 | self.image_name = self.KNIFE_COUNT_BG 22 | 23 | super().__init__(self.image_name, self.SPRITE_SCALING/1.2) 24 | 25 | # Automatically position the knife count image based on the number of knife count 26 | self.knife_count = knife_count 27 | self.center_x = self.SCREEN_WIDTH * 0.1 28 | self.center_y = (self.SCREEN_HEIGHT * 0.1 ) + (self.knife_count*self.SCREEN_HEIGHT*0.03) 29 | -------------------------------------------------------------------------------- /knifehit/knifehit.py: -------------------------------------------------------------------------------- 1 | import arcade 2 | import yaml 3 | 4 | from gamemanager import GameManager 5 | 6 | def load_config(): 7 | """ Load config data from config.yaml """ 8 | 9 | with open("config.yaml", 'r') as stream: 10 | try: 11 | return yaml.safe_load(stream) 12 | except yaml.YAMLError as exc: 13 | print(exc) 14 | 15 | def main(): 16 | """ Main method """ 17 | 18 | # load config 19 | GAME_CONFIG = load_config() 20 | 21 | # load game 22 | window = GameManager(GAME_CONFIG) 23 | window.setup() 24 | arcade.run() 25 | 26 | if __name__ == "__main__": 27 | main() 28 | -------------------------------------------------------------------------------- /knifehit/obstacle.py: -------------------------------------------------------------------------------- 1 | import arcade 2 | import math 3 | 4 | class Obstacle(arcade.Sprite): 5 | """ Obstacle class """ 6 | 7 | def __init__(self, GAME_CONFIG, target, initial_rotation_position): 8 | """ Initialize obstacle """ 9 | 10 | self.SPRITE_SCALING = GAME_CONFIG["general_settings"]["sprite_scaling"] 11 | self.KNIFE_IMAGE = GAME_CONFIG["assets_path"]["images"]["knife"] 12 | 13 | super().__init__(self.KNIFE_IMAGE, self.SPRITE_SCALING/2) 14 | 15 | self.center_x = 0 16 | self.center_y = 0 17 | self.rotation = 0 18 | self.target = target 19 | self.rotation_speed = self.target.TARGET_ROTATION_SPEED 20 | self.rotation_radius = self.target.height/2 21 | self.rotation_center = self.target.TARGET_POSITION 22 | self.initial_rotation_position = initial_rotation_position 23 | 24 | # Reshape the collision 25 | self.points = ( 26 | (-self.width // 3, self.height // 2), 27 | (self.width // 3, -self.height // 2), 28 | (-self.width // 3, -self.height // 2), 29 | ) 30 | 31 | def update(self): 32 | """ Movement and game logic """ 33 | 34 | # Rotation angle must be added with the initial rotation and 90 degress 35 | self.rotation += self.target.TARGET_ROTATION_SPEED 36 | self.angle = self.rotation+self.initial_rotation_position+90 37 | self.center_x = (self.rotation_radius * math.cos(math.radians(self.rotation+self.initial_rotation_position))) + self.rotation_center[0] 38 | self.center_y = (self.rotation_radius * math.sin(math.radians(self.rotation+self.initial_rotation_position))) + self.rotation_center[1] 39 | -------------------------------------------------------------------------------- /knifehit/target.py: -------------------------------------------------------------------------------- 1 | import arcade 2 | import random 3 | from enum import Enum 4 | 5 | class RotationMode(Enum): 6 | """ Store rotation mode in enum """ 7 | NORMAL = 1 8 | REVERSE = 2 9 | BACK_AND_FORTH = 3 10 | FAST_AND_SLOW = 4 11 | 12 | class Target(arcade.Sprite): 13 | """ 14 | Target class 15 | """ 16 | 17 | def __init__(self, GAME_CONFIG, scale_ratio=1, rotation_mode=None): 18 | """ Initialize target """ 19 | 20 | self.SCREEN_WIDTH = GAME_CONFIG["general_settings"]["screen_width"] 21 | self.SCREEN_HEIGHT = GAME_CONFIG["general_settings"]["screen_height"] 22 | self.SPRITE_SCALING = GAME_CONFIG["general_settings"]["sprite_scaling"] 23 | self.TARGET_POSITION = (self.SCREEN_WIDTH//2, self.SCREEN_HEIGHT*(0.7)) 24 | self.TARGET_ROTATION_SPEED = GAME_CONFIG["target_settings"]["rotation_speed"] 25 | self.TARGET_IMAGE = GAME_CONFIG["assets_path"]["images"]["target"] 26 | 27 | super().__init__(self.TARGET_IMAGE, self.SPRITE_SCALING/scale_ratio) 28 | 29 | self.TARGET_HITTED_SOUND = GAME_CONFIG["assets_path"]["sounds"]["target_hitted"] 30 | self.TARGET_HITTED_SOUND = arcade.load_sound(self.TARGET_HITTED_SOUND) 31 | 32 | self.center_x = self.TARGET_POSITION[0] 33 | self.center_y = self.TARGET_POSITION[1] 34 | # self.angle = random.randrange(360) 35 | # self.change_angle = self.TARGET_ROTATION_SPEED 36 | 37 | self.hit_impact_animation = False 38 | self.original_position = self.center_y 39 | 40 | self.impact_animation_counter = 0 41 | self.rotation_animation_counter = 0 42 | self.rotation_state = None 43 | 44 | if rotation_mode is None: 45 | self.rotation_mode = random.choice(list(RotationMode)) 46 | else: 47 | self.rotation_mode = rotation_mode 48 | 49 | # Override the rotation mode here for testing purposed 50 | # self.rotation_mode = RotationMode.BACK_AND_FORTH 51 | 52 | if self.rotation_mode == RotationMode.NORMAL: 53 | pass 54 | elif self.rotation_mode == RotationMode.REVERSE: 55 | self.TARGET_ROTATION_SPEED = -self.TARGET_ROTATION_SPEED 56 | elif self.rotation_mode == RotationMode.FAST_AND_SLOW: 57 | self.rotation_state = "Decreasing" 58 | elif self.rotation_mode == RotationMode.BACK_AND_FORTH: 59 | self.rotation_state = "Back" 60 | self.rotation_direction = 1 61 | elif self.rotation_mode == "STATIC": 62 | self.TARGET_ROTATION_SPEED = 0 63 | else: 64 | pass 65 | 66 | def hit_impact(self): 67 | """ Initialize the "hit impact" state """ 68 | 69 | self.hit_impact_animation = True 70 | self.impact_animation_counter = 3 71 | 72 | def update(self): 73 | """ Movement and game logic """ 74 | 75 | # Rotate the target. 76 | # The arcade.Sprite class has an "angle" attribute that controls 77 | # the sprite rotation. Change this, and the sprite rotates. 78 | 79 | if self.rotation_mode == RotationMode.FAST_AND_SLOW: 80 | if self.rotation_state is "Decreasing": 81 | self.TARGET_ROTATION_SPEED -= 0.005 82 | if self.rotation_state is "Decreasing" and self.TARGET_ROTATION_SPEED <= 0: 83 | self.rotation_state = "Increasing" 84 | if self.rotation_state is "Increasing": 85 | self.TARGET_ROTATION_SPEED += 0.01 86 | if self.rotation_state is "Increasing" and self.TARGET_ROTATION_SPEED >= 3: 87 | self.rotation_state = "Decreasing" 88 | 89 | if self.rotation_mode == RotationMode.BACK_AND_FORTH: 90 | if self.rotation_state is "Back": 91 | self.TARGET_ROTATION_SPEED -= 0.01 92 | if self.rotation_state is "Back" and self.TARGET_ROTATION_SPEED <= -4: 93 | self.rotation_state = "Forth" 94 | if self.rotation_state is "Forth": 95 | self.TARGET_ROTATION_SPEED += 0.05 96 | if self.rotation_state is "Forth" and self.TARGET_ROTATION_SPEED >= 4: 97 | self.rotation_state = "Back" 98 | 99 | 100 | self.angle += self.TARGET_ROTATION_SPEED 101 | 102 | # Play the hit impact animation 103 | if self.hit_impact_animation and self.impact_animation_counter > 0: 104 | self.center_y += 3 105 | self.color = (255, 179, 179) 106 | self.impact_animation_counter -= 1 107 | else: 108 | self.color = (255, 255, 255) 109 | self.hit_impact_animation = False 110 | self.impact_animation_counter = 0 111 | self.center_y = self.original_position 112 | 113 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | arcade==2.0.9 2 | future==0.17.1 3 | numpy==1.16.4 4 | Pillow==6.2.0 5 | pyglet==1.4.0b1 6 | pyglet-ffmpeg2==0.1.12 7 | PyYAML==5.1.1 8 | --------------------------------------------------------------------------------