├── .gitignore ├── AI_player.py ├── LICENSE ├── README.md ├── clock.py ├── events.py ├── functions.py ├── interface.py ├── main.py ├── requirements.py ├── screens.py ├── settings.py └── squares.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | *.xml 4 | *.iml 5 | *.sqlite 6 | *.sqlite-journal 7 | ProjectSettings.json 8 | .suo 9 | .vscode -------------------------------------------------------------------------------- /AI_player.py: -------------------------------------------------------------------------------- 1 | """ 2 | This controls games key and aims to get a high score 3 | """ 4 | from copy import copy, deepcopy 5 | from numpy import array, mean 6 | from random import choice 7 | 8 | class AI: 9 | def __init__(self): 10 | self.direction = None 11 | 12 | def control(self, sqs_given, status): 13 | if sqs_given.curr_sq == sqs_given.st.new or self.direction is None: 14 | self.direction = make_choice(sqs_given) 15 | else: 16 | move(sqs_given, self.direction, status) 17 | 18 | def move(sqs_given, direction, status): 19 | # rotation 20 | if sqs_given.rotate_curr != direction['rotate']: 21 | status.rotate = True 22 | else: 23 | status.rotate = False 24 | # horizontal left 25 | if sqs_given.curr_sq[1] > direction['center'][1]: 26 | status.left = True 27 | # horizontal right 28 | elif sqs_given.curr_sq[1] < direction['center'][1]: 29 | status.right = True 30 | # horizontal stop 31 | else: 32 | status.left = False 33 | status.right = False 34 | # vertical drop 35 | if sqs_given.curr_sq[0] != direction['center'][0]: 36 | status.down = True 37 | else: 38 | status.down = False 39 | 40 | def make_choice(sqs_given): 41 | '''return one direction to go''' 42 | sqs = copy_sqs(sqs_given) 43 | pos_data = get_all_possible_pos(sqs) 44 | evaluate_full_situation(sqs, pos_data) 45 | all_highest = get_all_highest(pos_data) 46 | return choice(all_highest) 47 | 48 | def get_all_highest(pos_data): 49 | '''highest marks might not be distinct, so return all of them''' 50 | # find highest mark 51 | highest_key = lambda dict:dict['mark'] 52 | max_data = max(pos_data, key=highest_key) 53 | max_mark = max_data['mark'] 54 | # get all data with this mark 55 | all_highest = [] 56 | for data in pos_data: 57 | if data['mark'] == max_mark: 58 | all_highest.append(data) 59 | return all_highest 60 | 61 | 62 | def get_all_possible_pos(sqs_given): 63 | # copy given sqs for safety 64 | sqs_origin = copy_sqs(sqs_given) 65 | # reset rotation 66 | sqs_origin.curr_shape = sqs_origin.origin_shape 67 | sqs_origin.rotate_curr = 1 68 | # generate pos 69 | pos = [] 70 | for rotate in range(sqs_origin.rotate_limit): 71 | sqs = copy_sqs(sqs_origin) 72 | sqs_origin.rotate(sqs_origin) 73 | get_end_pos_with_rotate(pos, sqs) 74 | return pos 75 | 76 | def get_end_pos_with_rotate(pos, sqs): 77 | move_sq_to_left(sqs) 78 | old_sq = None 79 | # move to right and record each position with drop to the end 80 | while old_sq != sqs.curr_sq: 81 | sqs_curr = copy_sqs(sqs) 82 | sqs_curr.drop_straight(sqs_curr) 83 | record_curr_pos(pos, sqs_curr) 84 | old_sq = sqs.curr_sq 85 | sqs.right(sqs) 86 | 87 | def copy_sqs(sqs): 88 | '''this copies sqs safely''' 89 | sqs_copy = copy(sqs) 90 | sqs_copy.squares = deepcopy(sqs.squares) 91 | sqs_copy.curr_sq = deepcopy(sqs.curr_sq) 92 | sqs_copy.curr_shape = deepcopy(sqs.curr_shape) 93 | return sqs_copy 94 | 95 | def move_sq_to_left(sqs): 96 | old_sq = None 97 | while old_sq != sqs.curr_sq: 98 | old_sq = sqs.curr_sq 99 | sqs.left(sqs) 100 | 101 | def record_curr_pos(pos, sqs): 102 | '''record all active squares''' 103 | all_pos = [] 104 | y = sqs.curr_sq[0] 105 | x = sqs.curr_sq[1] 106 | all_pos.append([y, x]) 107 | for sq in sqs.curr_shape: 108 | all_pos.append([y+sq[0], x+sq[1]]) 109 | pos.append({'all_pos':all_pos, 'center':sqs.curr_sq, 'rotate':sqs.rotate_curr}) 110 | 111 | def evaluate_full_situation(sqs, positions): 112 | for pos_data in positions: 113 | pos = pos_data['all_pos'] 114 | sqs_curr = copy_sqs(sqs) 115 | map_pos_to_sqs(sqs_curr, pos) 116 | pos_data['mark'] = evaluate_situation(sqs_curr) 117 | 118 | def evaluate_situation(sqs): 119 | full_lines = evaluate_full_lines(sqs) 120 | sqs.clean_full_lines(sqs) 121 | squares = array(sqs.squares).T # convert rows to colomns 122 | hidden_squares = evaluate_hidden_squares(squares) 123 | lowest_column, average_column, absolute_diff = evaluate_column(squares) 124 | return evaluate_mark(full_lines, hidden_squares, lowest_column, average_column, absolute_diff) 125 | 126 | def evaluate_full_lines(sqs_given): 127 | sqs = copy_sqs(sqs_given) 128 | full_lines = 0 129 | for line in sqs.squares: 130 | if line.count('none') == 0: 131 | full_lines += 1 132 | return full_lines 133 | 134 | def evaluate_hidden_squares(squares): 135 | '''find the number of non-squares under squares''' 136 | hidden_squares = 0 137 | for colomn in squares: 138 | found_first_sq = False 139 | for sq in colomn: 140 | # find first square 141 | if not found_first_sq: 142 | if sq != 'none': 143 | found_first_sq = True 144 | else: 145 | continue 146 | # find hidden squares 147 | if sq == 'none': 148 | hidden_squares += 1 149 | return hidden_squares 150 | 151 | def evaluate_column(squares): 152 | '''count lowest and average space left in every column''' 153 | space_left = [] 154 | for column in squares: 155 | appended = False 156 | for index, sq in enumerate(column): 157 | # check every square 158 | if sq != 'none': 159 | space_left.append(index) 160 | appended = True 161 | break 162 | if not appended: 163 | space_left.append(len(column)) 164 | return (min(space_left), mean(space_left), max(space_left)-min(space_left)) 165 | 166 | def evaluate_mark(full_lines, hidden_squares, lowest_column, average_column, absolute_diff): 167 | # weights, set manually 168 | full_line_weight = 20 169 | hidden_squares_weight = -2 170 | lowest_column_weight = 0.3 171 | average_column_weight = 0.15 172 | absolute_diff_weight = -1 173 | mark = 0 174 | mark += full_lines * full_line_weight 175 | mark += hidden_squares * hidden_squares_weight 176 | mark += lowest_column * lowest_column_weight 177 | mark += average_column * average_column_weight 178 | mark += absolute_diff * absolute_diff_weight 179 | return mark 180 | 181 | def map_pos_to_sqs(sqs, positions): 182 | for pos in positions: 183 | sqs.squares[pos[0]][pos[1]] = 'map' 184 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tetris in Python 2 | This is a simple Tetris game based on python3 with pygame. 3 | 4 | ## How to play? 5 | 1. Download all python files and store them to one folder. 6 | 2. Open main.py to start play. 7 | 8 | ## How to control? 9 | Left and right to move, up to rotate, down to quick drop, space to hard drop. 10 | 11 | ## Contribution 12 | Everyone is welcome to contribute on this. 13 | 14 | ## Test 15 | Tested with python 3.6.3 and pygame 1.9.3, and auto-pygame-download part is tested under pip 9. 16 | 17 | ## Related Projects 18 | - https://github.com/tholman/tetris-pieces JS Tetris with AI 19 | -------------------------------------------------------------------------------- /clock.py: -------------------------------------------------------------------------------- 1 | from time import process_time 2 | 3 | class Clock: 4 | """set up a timer to record what is time to do""" 5 | 6 | def __init__(self, st): 7 | self.st = st 8 | self.last_drop = process_time() 9 | self.last_move = process_time() 10 | self.last_rotate = process_time() 11 | self.last_left_down = process_time() 12 | self.last_right_down = process_time() 13 | self.last_quick_drop = process_time() 14 | self.last_stop = process_time() 15 | self.last_should_stop = None # for detection of the stop at the very bottom 16 | self.stop_detection_started = False 17 | self.last_straight_drop = process_time() 18 | 19 | def update_drop(self): 20 | self.last_drop = process_time() 21 | 22 | def update_move(self): 23 | self.last_move = process_time() 24 | 25 | def update_rotate(self): 26 | self.last_rotate = process_time() 27 | 28 | def update_left_down(self): 29 | self.last_left_down = process_time() 30 | 31 | def update_right_down(self): 32 | self.last_right_down = process_time() 33 | 34 | def update_quick_drop(self): 35 | self.last_quick_drop = process_time() 36 | 37 | def update_stop(self): 38 | self.last_stop = process_time() 39 | 40 | def update_should_stop(self, mode): 41 | if mode is True and self.stop_detection_started is False: 42 | self.last_should_stop = process_time() 43 | self.stop_detection_started = True 44 | elif mode is None: 45 | self.stop_detection_started = False 46 | 47 | 48 | def update_straight_drop(self): 49 | self.last_straight_drop = process_time() 50 | 51 | def is_time_to_drop(self): 52 | return ((process_time() - self.last_drop) > self.st.time_drop) 53 | 54 | def is_time_to_quick_drop(self): 55 | return ((process_time() - self.last_quick_drop) > self.st.time_quick_drop) and\ 56 | ((process_time() - self.last_stop) > self.st.time_before_drop) 57 | 58 | def is_time_to_move(self): 59 | return (process_time() - self.last_move) > self.st.time_move 60 | 61 | def is_time_to_rotate(self): 62 | return (process_time() - self.last_rotate) > self.st.time_rotate 63 | 64 | def is_time_to_quick_left(self): 65 | return (process_time() - self.last_left_down) > self.st.time_to_quick 66 | 67 | def is_time_to_quick_right(self): 68 | return (process_time() - self.last_right_down) > self.st.time_to_quick 69 | 70 | def is_time_to_straight_drop(self): 71 | return (process_time() - self.last_straight_drop) > self.st.time_to_straight_drop 72 | 73 | def is_time_to_stop(self): 74 | return self.stop_detection_started and\ 75 | ((process_time() - self.last_should_stop) > self.st.time_stop) 76 | -------------------------------------------------------------------------------- /events.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from sys import exit 3 | 4 | # listen to every event and respond 5 | def check_events(sqs, status, AI): 6 | for event in pygame.event.get(): 7 | if event.type == pygame.QUIT: 8 | exit() 9 | if event.type == pygame.KEYDOWN: 10 | key_down(sqs, event.key, status) 11 | if event.type == pygame.KEYUP: 12 | key_up(event.key, status) 13 | if status.is_AI(): 14 | AI.control(sqs, status) 15 | 16 | # deal with keys that are pressed down 17 | def key_down(sqs, key, status): 18 | if status.is_game_new(): 19 | status.game_status = status.ACTIVE 20 | elif status.is_game_over(): 21 | status.game_status = status.RENEW 22 | status.new_AI = False 23 | if key == pygame.K_q: # q stands for quit 24 | exit() 25 | if key == pygame.K_DOWN: 26 | status.down = True 27 | elif key == pygame.K_LEFT: 28 | status.left = True 29 | sqs.clock.update_left_down() 30 | elif key == pygame.K_RIGHT: 31 | status.right = True 32 | sqs.clock.update_right_down() 33 | elif key == pygame.K_UP: 34 | status.rotate = True 35 | elif key == pygame.K_SPACE: 36 | status.straight_drop = True 37 | if key == pygame.K_a: 38 | status.AI = True 39 | status.new_AI = True 40 | sqs.st.adjust_for_AI() 41 | 42 | # deal with keys that are released 43 | def key_up(key, status): 44 | if key == pygame.K_q: 45 | exit() 46 | if key == pygame.K_DOWN: 47 | status.down = False 48 | elif key == pygame.K_LEFT: 49 | status.left = False 50 | elif key == pygame.K_RIGHT: 51 | status.right = False 52 | elif key == pygame.K_UP: 53 | status.rotate = False 54 | elif key == pygame.K_SPACE: 55 | status.straight_drop = False 56 | -------------------------------------------------------------------------------- /functions.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | class Fucntions: 4 | def __init__(self, st, screen): 5 | self.st = st 6 | self.screen = screen 7 | 8 | def show_score(self, score): 9 | surface = None 10 | adjust = True # at least calculate surface once 11 | while adjust: 12 | text = self.st.score + str(score) 13 | font = pygame.font.SysFont(self.st.score_font, self.st.score_size) 14 | surface = font.render(text, True, self.st.score_color) 15 | # adjust score font when it is too big 16 | adjust = ((surface.get_width() + 2 * self.st.score_pos[0]) > self.st.func_size[0]) 17 | if adjust: 18 | self.st.score_size -= self.st.score_font_adjust 19 | self.screen.blit(surface, self.st.score_pos) 20 | 21 | class Status: 22 | def __init__(self): 23 | # some numbers 24 | self.GAMEOVER = 0x0 25 | self.NEWSTART = 0x1 26 | self.ACTIVE = 0x2 27 | self.RENEW = 0x3 28 | 29 | # game status 30 | self.game_status = self.NEWSTART 31 | self.refresh() 32 | 33 | def is_game_active(self): 34 | return self.game_status == self.ACTIVE 35 | 36 | def is_game_over(self): 37 | return self.game_status == self.GAMEOVER 38 | 39 | def is_game_new(self): 40 | return self.game_status == self.NEWSTART 41 | 42 | def is_game_renew(self): 43 | return self.game_status == self.RENEW 44 | 45 | def is_AI(self): 46 | return self.AI 47 | 48 | def refresh(self): 49 | self.left = False 50 | self.right = False 51 | self.down = False 52 | self.rotate = False 53 | self.straight_drop = False 54 | self.AI = False 55 | 56 | # score status 57 | self.score = 0 58 | -------------------------------------------------------------------------------- /interface.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | def start(screen, st): 4 | if st.start_pos == "center": 5 | st.start_pos = get_center_pos(screen, st.start_surface) 6 | screen.blit(st.start_surface, st.start_pos) 7 | 8 | def game_over(screen, st): 9 | if st.game_over_pos == "center": 10 | st.game_over_pos = get_center_pos(screen, st.game_over_surface) 11 | 12 | screen.blit(st.game_over_surface, st.game_over_pos) 13 | 14 | def get_center_pos(screen, text): 15 | screen_rect = screen.get_rect() 16 | text_rect = text.get_rect() 17 | text_rect.centerx = screen_rect.centerx 18 | text_rect.centery = screen_rect.centery 19 | return (text_rect.x, text_rect.y) 20 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import requirements 2 | import pygame 3 | import screens, events, functions, interface 4 | from settings import Settings 5 | from squares import Squares 6 | import AI_player 7 | 8 | def game_start(): 9 | # initialisations 10 | pygame.init() 11 | status = functions.Status() 12 | st = Settings() 13 | 14 | screen = pygame.display.set_mode(st.screen_size) 15 | pygame.display.set_caption(st.screen_name) 16 | 17 | func = functions.Fucntions(st, screens.get_func_surface(screen, st)) 18 | sqs = Squares(st, status, screens.get_sqs_surface(screen, st)) 19 | 20 | AI = AI_player.AI() 21 | # main loop 22 | while True: 23 | pygame.display.flip() 24 | events.check_events(sqs, status, AI) 25 | if status.is_game_active(): 26 | if sqs.update(): 27 | screens.update_screen(screen, sqs, func, status, st) 28 | elif status.is_game_over(): 29 | interface.game_over(screen, st) 30 | elif status.is_game_new(): 31 | interface.start(screen, st) 32 | elif status.is_game_renew(): 33 | AI_mode = status.new_AI 34 | status.refresh() 35 | status.game_status = status.ACTIVE 36 | sqs = Squares(st, status, screens.get_sqs_surface(screen, st)) 37 | st = Settings() 38 | if AI_mode: 39 | status.AI = True 40 | sqs.st.adjust_for_AI() 41 | else: 42 | raise RuntimeError # this should never happen 43 | 44 | 45 | if __name__ == "__main__": 46 | requirements.check() 47 | game_start() 48 | -------------------------------------------------------------------------------- /requirements.py: -------------------------------------------------------------------------------- 1 | """ 2 | this part checks external packages and install if not exist 3 | """ 4 | 5 | requirements = ['pygame', 'numpy'] 6 | import sys 7 | 8 | # find and install any missing external packages 9 | def check(): 10 | # find missing packages 11 | from importlib.util import find_spec 12 | missing = [requirement for requirement in requirements if not(find_spec(requirement))] 13 | if not missing: 14 | return 15 | # install missing packages 16 | sys.stdout.write("Installing" + ','.join(missing) + ".\n") 17 | # redirect out to nothing so no installing messages will be seen. 18 | sys_stdout = sys.stdout 19 | sys_stderr = sys.stderr 20 | sys.stdout = None 21 | sys.stderr = None 22 | from pip.commands.install import InstallCommand 23 | from pip.status_codes import SUCCESS 24 | cmd = InstallCommand() 25 | for requirement in requirements: 26 | try: 27 | if cmd.main([requirement]) is not SUCCESS: 28 | sys_stderr.write("Can not install " + requirement + ", program aborts.\n") 29 | sys.exit() 30 | # this might occur because of redirection of stdout and stderr 31 | except AttributeError: 32 | pass 33 | # direct out back to normal 34 | sys.stdout = sys_stdout 35 | sys.stderr = sys_stderr 36 | sys.stdout.write("All packages are installed, starting game...") 37 | sys.stdout.flush() -------------------------------------------------------------------------------- /screens.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | def update_screen(screen, sqs, func, status, st): 4 | """draw one screen""" 5 | screen.fill(st.bg_color) 6 | sqs.draw_squares() 7 | func.show_score(status.score) 8 | 9 | def get_sqs_surface(screen, st): 10 | sqs_rect = pygame.Rect(0, 0, st.game_size[0], st.game_size[1]) 11 | return screen.subsurface(sqs_rect) 12 | 13 | def get_func_surface(screen, st): 14 | func_surface = pygame.Rect(st.game_size[0], 0, st.func_size[0], st.func_size[1]) 15 | return screen.subsurface(func_surface) 16 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | class Settings: 4 | def __init__(self): 5 | # times or speed, in seconds, you can adjust this if youre not satisfied by the default 6 | self.time_drop = 0.8 # period to force drop 7 | self.time_drop_adjust = 0.99 # every score up, decrease drop time by this factor 8 | self.time_stop = 0.5 # time player can adjust pos at bottom 9 | self.time_move = 0.05 # minimum time interval to move 10 | self.time_rotate = 0.2 # minimum time interval to rotate 11 | self.time_to_quick = 0.15 # time interval to activate quick move mode 12 | self.time_before_drop = 0.3 # time to wait from one stop to drop 13 | self.time_quick_drop = 0.01 # minimum time interval to drop in quick mode 14 | self.time_move_quick = 0.015 # minimum time interval to move in quick mode 15 | self.time_to_straight_drop = 0.3 # time to do another down straight 16 | 17 | # colors, you can change it to be an artist 18 | self.colors = { 19 | 'black': (0,0,0), 20 | 'white': (255, 255, 255), 21 | 'red' : (255, 0, 0), 22 | 'green': (0, 255, 0), 23 | 'blue' : (0, 0, 255), 24 | 'yellow': (255, 255, 0), 25 | 'purple': (255, 0, 255), 26 | 'cyan' : (0, 255, 255), 27 | 28 | 'none' : (45, 45, 45), # dark grey 29 | 'tip' : (100, 100, 100) # grey 30 | } 31 | 32 | self.bg_color = (30, 30, 30) # black 33 | self.square_color = (245, 245, 245) # white 34 | self.space_color = (35, 35, 35) # slightly lighter than bg 35 | 36 | # shapes, dont touch this if you are not clear what this dose 37 | self.shapes = ( 38 | {'pos':([-1, 0], [0, -1], [0, 1]), 'color':'red', 'rotate':4}, # _|_ 39 | {'pos':([-1, 0], [0, -1], [-1, 1]), 'color':'green', 'rotate':2}, # _|- 40 | {'pos':([-1, 0], [-1, -1], [0, 1]), 'color':'blue', 'rotate':2}, #-|_ 41 | {'pos':([-1, 0], [-1, 1], [0, 1]), 'color':'yellow', 'rotate':1}, # :: 42 | {'pos':([-1, 0], [-2, 0], [1, 0]), 'color':'purple', 'rotate':2}, # | 43 | {'pos':([-1, -1], [0, -1], [0, 1]), 'color':'cyan', 'rotate':4}, # |__ 44 | {'pos':([-1, 1], [0, -1], [0, 1]), 'color':'white', 'rotate':4} # --| 45 | ) 46 | self.shape_num = len(self.shapes) 47 | 48 | # positions 49 | self.square_length = 30 50 | self.square_num_x = 12 51 | self.square_num_y = 20 52 | self.square_space = 5 53 | self.new = [1, int(self.square_num_x/2)] # upper center 54 | 55 | # surfaces 56 | self.func_width = 300 57 | self.game_size = self.get_game_size(self) 58 | self.func_size = self.get_func_size(self) 59 | self.screen_size = self.get_screen_size(self) 60 | self.screen_name = "Tetris by Bofei Wang" 61 | 62 | # texts 63 | self.text_margin = 10 64 | self.text_adjust_factor = 5 65 | self.score = "Score: " 66 | self.score_font = "Comic Sans MS" 67 | self.score_size = 120 68 | self.score_font_adjust = 5 69 | self.score_color = (255, 255, 255) # white 70 | self.score_pos = (10, 10) 71 | 72 | self.start = "Press any key to start, press A to watch AI play" 73 | self.start_font = "Comic Sans MS" 74 | self.start_size = 200 75 | self.start_color = (0, 255, 0) # green 76 | self.start_pos = "center" 77 | self.start_surface = self.adjust_start_size(self) 78 | 79 | self.game_over = "Press any key to play again, press A to watch AI play" 80 | self.game_over_font = self.start_font 81 | self.game_over_size = self.start_size 82 | self.game_over_color = (255, 0, 0) # red 83 | self.game_over_pos = "center" 84 | self.game_over_surface = self.adjust_game_over_size(self) 85 | 86 | def adjust_for_AI(self): 87 | self.time_drop = 0 # period to force drop 88 | self.time_drop_adjust = 0 # every score up, decrease drop time by this factor 89 | self.time_stop = 0 # time player can adjust pos at bottom 90 | self.time_move = 0 # minimum time interval to move 91 | self.time_rotate = 0 # minimum time interval to rotate 92 | self.time_before_drop = 0 # time to wait from one stop to drop 93 | self.time_quick_drop = 0 # minimum time interval to drop in quick mode 94 | self.time_move_quick = 0 # minimum time interval to move in quick mode 95 | self.screen_name = 'Tetris by Bofei Wang, AI playing...' 96 | 97 | 98 | @staticmethod 99 | def get_game_size(self): 100 | x = ((self.square_length + self.square_space)\ 101 | * self.square_num_x) + self.square_space 102 | y = ((self.square_length + self.square_space)\ 103 | * self.square_num_y) + self.square_space 104 | return (x, y) 105 | 106 | @staticmethod 107 | def get_func_size(self): 108 | x = self.func_width 109 | y = self.game_size[1] 110 | return (x, y) 111 | 112 | @staticmethod 113 | def get_screen_size(self): 114 | x = self.game_size[0] + self.func_size[0] 115 | y = self.game_size[1] 116 | return (x, y) 117 | 118 | @staticmethod 119 | def adjust_start_size(self): 120 | adjust = True # at least calculate surface once 121 | while adjust: 122 | font = pygame.font.SysFont(self.start_font, self.start_size) 123 | surface = font.render(self.start, True, self.start_color) 124 | # adjust font if it is too big 125 | adjust = ((surface.get_width() + 2 * self.text_margin) > self.screen_size[0]) 126 | if adjust: 127 | self.start_size -= self.text_adjust_factor 128 | else: 129 | return surface 130 | 131 | @staticmethod 132 | def adjust_game_over_size(self): 133 | adjust = True # at least calculate surface once 134 | while adjust: 135 | font = pygame.font.SysFont(self.game_over_font, self.game_over_size) 136 | surface = font.render(self.game_over, True, self.game_over_color) 137 | # adjust font if it is too big 138 | adjust = ((surface.get_width() + 2 * self.text_margin) > self.screen_size[0]) 139 | if adjust: 140 | self.game_over_size -= self.text_adjust_factor 141 | else: 142 | return surface 143 | 144 | -------------------------------------------------------------------------------- /squares.py: -------------------------------------------------------------------------------- 1 | from random import randrange 2 | from pygame import Rect, draw 3 | from clock import Clock 4 | 5 | class Squares: 6 | """method for malipulating squares in the game""" 7 | def __init__(self, st, status, screen): 8 | self.st = st 9 | self.status = status 10 | self.screen = screen 11 | self.empty_line = ['none' for i in range(st.square_num_x)] 12 | self.squares = [self.empty_line.copy() for i in range(st.square_num_y)] 13 | self.new_sq(self) 14 | self.clock = Clock(st) 15 | 16 | # draw all squares 17 | def draw_squares(self): 18 | self.screen.fill(self.st.space_color) 19 | self.draw_tip(self) 20 | self.draw_exist_sq(self) 21 | self.draw_curr_sq(self) 22 | 23 | # update squares' information 24 | def update(self): 25 | updated = False # for update screen 26 | # vertical drop, straight drop 27 | if self.status.straight_drop and self.clock.is_time_to_straight_drop(): 28 | updated = True 29 | self.drop_straight(self) 30 | self.clock.update_straight_drop() 31 | # vertical drop, force drop 32 | elif self.clock.is_time_to_drop(): 33 | updated = True 34 | self.drop(self) 35 | self.clock.update_drop() 36 | # vertical drop, quick drop 37 | elif self.status.down and self.clock.is_time_to_quick_drop(): 38 | updated = True 39 | self.drop(self) 40 | self.clock.update_quick_drop() 41 | # rotation 42 | if self.status.rotate and self.clock.is_time_to_rotate(): 43 | updated = True 44 | self.rotate(self) 45 | self.clock.update_rotate() 46 | # horizontal move 47 | if self.status.right: 48 | updated = True 49 | if self.clock.is_time_to_move() or self.clock.is_time_to_quick_right(): 50 | self.right(self) 51 | self.clock.update_move() 52 | if self.status.left: 53 | updated = True 54 | if self.clock.is_time_to_move() or self.clock.is_time_to_quick_left(): 55 | self.left(self) 56 | self.clock.update_move() 57 | # crash detection 58 | if self.should_stop(self): 59 | updated = True 60 | self.stop(self) 61 | return updated 62 | 63 | # renew current square 64 | @staticmethod 65 | def new_sq(self): 66 | self.curr_sq = self.st.new.copy() 67 | shape = self.get_shape(self) 68 | self.origin_shape = shape['pos'] 69 | self.curr_shape = shape['pos'] 70 | self.curr_color = shape['color'] 71 | self.rotate_limit = shape['rotate'] 72 | self.rotate_curr = 1 73 | # if new squares are crashed, game over. 74 | if not self.valid(self, self.curr_sq, self.curr_shape): 75 | self.status.game_status = self.status.GAMEOVER 76 | 77 | # return a random shape dictionary 78 | @staticmethod 79 | def get_shape(self): 80 | shape_index = randrange(0, self.st.shape_num) 81 | return self.st.shapes[shape_index].copy() 82 | 83 | @staticmethod 84 | def drop_straight(self): 85 | while not self.should_stop(self): 86 | self.curr_sq[0] += 1 87 | 88 | @staticmethod 89 | def drop(self): 90 | new_sq = self.curr_sq.copy() 91 | new_sq[0] += 1 92 | if self.valid(self, new_sq, self.curr_shape): 93 | self.curr_sq = new_sq 94 | 95 | @staticmethod 96 | def rotate(self): 97 | new_shape = self.get_rotated_shape(self) 98 | # regular check 99 | if self.valid(self, self.curr_sq, new_shape): 100 | self.curr_shape = new_shape 101 | # move horizontally if not valid 102 | else: 103 | tolerance = 2 104 | for i in range(tolerance): 105 | # left 106 | new_sq_left = self.curr_sq.copy() 107 | new_sq_left[1] -= 1 108 | if self.valid(self, new_sq_left, new_shape): 109 | self.curr_sq = new_sq_left 110 | self.curr_shape = new_shape 111 | return 112 | # right 113 | new_sq_right = self.curr_sq.copy() 114 | new_sq_right[1] += 1 115 | if self.valid(self, new_sq_right, new_shape): 116 | self.curr_sq = new_sq_right 117 | self.curr_shape = new_shape 118 | return 119 | 120 | 121 | @staticmethod 122 | def get_rotated_shape(self): 123 | # rotation limit must not exceed, if exceed, reset it 124 | if self.rotate_curr >= self.rotate_limit: 125 | self.rotate_curr = 1 126 | new_shape = self.origin_shape 127 | else: 128 | self.rotate_curr += 1 129 | new_shape = [] 130 | for sq in self.curr_shape: 131 | new_shape.append([sq[1], -sq[0]]) 132 | return new_shape 133 | 134 | @staticmethod 135 | def right(self): 136 | new_sq = self.curr_sq.copy() 137 | new_sq[1] += 1 138 | if self.valid(self, new_sq, self.curr_shape): 139 | self.curr_sq = new_sq 140 | 141 | @staticmethod 142 | def left(self): 143 | new_sq = self.curr_sq.copy() 144 | new_sq[1] -= 1 145 | if self.valid(self, new_sq, self.curr_shape): 146 | self.curr_sq = new_sq 147 | 148 | @staticmethod 149 | def stop(self): 150 | # wait for a moment before stop, give player time to adjust 151 | if not self.clock.is_time_to_stop(): 152 | self.clock.update_should_stop(True) 153 | return 154 | else: 155 | self.clock.update_should_stop(None) 156 | self.clock.update_stop() 157 | # copy squares to map 158 | for sq in self.curr_shape: 159 | x = sq[1] + self.curr_sq[1] 160 | y = sq[0] + self.curr_sq[0] 161 | if y >= 0: 162 | self.squares[y][x] = self.curr_color 163 | x = self.curr_sq[1] 164 | y = self.curr_sq[0] 165 | if y >= 0: 166 | self.squares[y][x] = self.curr_color 167 | full_lines = self.clean_full_lines(self) 168 | self.status.score += full_lines # add score 169 | self.new_sq(self) 170 | 171 | # delete full lines and insert empty lines at the front 172 | @staticmethod 173 | def clean_full_lines(self): 174 | full_lines = 0 175 | for index, line in enumerate(self.squares): 176 | if line.count('none') == 0: 177 | full_lines += 1 178 | self.st.time_drop *= self.st.time_drop_adjust # adjust time 179 | self.squares.pop(index) 180 | self.squares.insert(0, self.empty_line.copy()) 181 | return full_lines 182 | 183 | # validate current squares of shapes relative to center with with one drop vertically 184 | @staticmethod 185 | def should_stop(self): 186 | # check shape squares 187 | for sq in self.curr_shape: 188 | x = sq[1] + self.curr_sq[1] 189 | y = sq[0] + self.curr_sq[0] + 1 190 | if y - 1 >= 0 and not self.valid_sq(self, [y, x]): 191 | return True 192 | # check center square 193 | x = self.curr_sq[1] 194 | y = self.curr_sq[0] + 1 195 | return not (self.valid_sq(self, [y, x])) 196 | 197 | # validate the given center square and shape squires relative to center square 198 | @staticmethod 199 | def valid(self, square, shape): 200 | # check shape squares 201 | for sq in shape: 202 | x = sq[1] + square[1] 203 | y = sq[0] + square[0] 204 | if y >= 0 and not (self.valid_sq(self, [y, x])): 205 | return False 206 | # check center square 207 | return self.valid_sq(self, square) 208 | 209 | @staticmethod 210 | def valid_sq(self, sq): 211 | # check border 212 | if sq[0] >= self.st.square_num_y or \ 213 | sq[1] >= self.st.square_num_x or \ 214 | sq[1] < 0: 215 | return False 216 | # check crash 217 | return self.squares[sq[0]][sq[1]] == 'none' 218 | 219 | @staticmethod 220 | def draw_exist_sq(self): 221 | for y, row in enumerate(self.squares): 222 | for x, square in enumerate(row): 223 | color = self.st.colors[self.squares[y][x]] 224 | self.draw_square(self, y, x, color) 225 | 226 | @staticmethod 227 | def draw_tip(self): 228 | # find the lowrest position 229 | curr_sq = self.curr_sq.copy() 230 | while not self.should_stop(self): 231 | self.curr_sq[0] += 1 232 | curr_sq, self.curr_sq = self.curr_sq, curr_sq 233 | 234 | # draw their tips 235 | color = self.st.colors['tip'] 236 | self.draw_square(self, curr_sq[0], curr_sq[1], color, True) 237 | self.draw_square(self, curr_sq[0], curr_sq[1], self.st.colors['none']) 238 | for y, x in self.curr_shape: 239 | curr_y, curr_x = curr_sq[0], curr_sq[1] 240 | self.draw_square(self, y + curr_y, x + curr_x, color, True) 241 | self.draw_square(self, y + curr_y, x + curr_x, self.st.colors['none']) 242 | 243 | @staticmethod 244 | def draw_curr_sq(self): 245 | # draw center 246 | color = self.st.colors[self.curr_color] 247 | self.draw_square(self, self.curr_sq[0], self.curr_sq[1], color) 248 | # draw shapes 249 | curr_y, curr_x = self.curr_sq[0], self.curr_sq[1] 250 | for y, x in self.curr_shape: 251 | self.draw_square(self, y + curr_y, x + curr_x, color) 252 | 253 | # draw one single square with given information 254 | @staticmethod 255 | def draw_square(self, y, x, color, border=False): 256 | x_pos = x * (self.st.square_space + self.st.square_length) 257 | y_pos = y * (self.st.square_space + self.st.square_length) 258 | length = self.st.square_length 259 | # adding borders borders 260 | if border: 261 | y_pos -= self.st.square_space 262 | x_pos -= self.st.square_space 263 | length += 2 * self.st.square_space 264 | rect = Rect(x_pos + self.st.square_space, y_pos + self.st.square_space, length, length) 265 | draw.rect(self.screen, color, rect) --------------------------------------------------------------------------------