├── README.md └── tetris_bot.py /README.md: -------------------------------------------------------------------------------- 1 | # Tetris-Discord-Bot 2 | Full of spaghetti code and impossible to understand, but here's the code for my video 'Making Tetris Using Discords Bot API'. Watch it here lol: https://youtu.be/UqQz1qkBMkE 3 | -------------------------------------------------------------------------------- /tetris_bot.py: -------------------------------------------------------------------------------- 1 | #Known bugs: 2 | #After first round of playing, some of the shapes will always start higher 3 | #ROTATING SHAPE WHEN AT TOP OF GAME BOARD 4 | #ROTATING SHAPES TOO QUICK MAKES THEM DO WALL BOUNCE 5 | #PRESSING DOWN AND LEFT/RIGHT AT SAME TIME DOESNT MOVE IT LEFT/RIGHT 6 | 7 | import discord 8 | from discord.ext import commands 9 | import random 10 | import asyncio 11 | 12 | board = [] 13 | num_of_rows = 18 14 | num_of_cols = 10 15 | empty_square = ':black_large_square:' 16 | blue_square = ':blue_square:' 17 | brown_square = ':brown_square:' 18 | orange_square = ':orange_square:' 19 | yellow_square = ':yellow_square:' 20 | green_square = ':green_square:' 21 | purple_square = ':purple_square:' 22 | red_square = ':red_square:' 23 | embed_colour = 0x077ff7 #colour of line on embeds 24 | points = 0 25 | lines = 0 #how many lines cleared 26 | down_pressed = False #if down button has been pressed 27 | rotate_clockwise = False 28 | rotation_pos = 0 29 | h_movement = 0 #amount to move left or right 30 | is_new_shape = False 31 | start_higher = False #for when near top of board 32 | game_over = False 33 | index = 0 34 | 35 | 36 | class Tetronimo: #Tetris pieces 37 | def __init__(self, starting_pos, colour, rotation_points): 38 | self.starting_pos = starting_pos #list 39 | self.colour = colour 40 | self.rotation_points = rotation_points #list 41 | 42 | main_wall_kicks = [ #for J, L, T, S, Z tetronimos 43 | [[0, 0], [0, -1], [-1, -1], [2, 0], [2, -1]], 44 | [[0, 0], [0, 1], [1, 1], [-2, 0], [-2, 1]], 45 | [[0, 0], [0, 1], [-1, 1], [2, 0], [2, 1]], 46 | [[0, 0], [0, -1], [1, -1], [-2, 0], [-2, -1]] 47 | ] 48 | 49 | i_wall_kicks = [ #for I tetronimo 50 | [[0, 0], [0, -2], [0, 1], [1, -2], [-2, 1]], 51 | [[0, 0], [0, -1], [0, 2], [-2, -1], [1, 2]], 52 | [[0, 0], [0, 2], [0, -1], [-1, 2], [2, -1]], 53 | [[0, 0], [0, 1], [0, -2], [2, 1], [-1, -2]] 54 | ] 55 | 56 | rot_adjustments = { #to move when rotations are slightly off 57 | #blue: not sure if needs any rn 58 | ':blue_square:': [[0, 1], [-1, -1], [0, 0], [-1, 0]], #[[0, 0], [0, 0], [0, 0], [0, 0]] 59 | #brown: left 1, right 1, right 1, left 1, 60 | ':brown_square:': [[0, 0], [0, 1], [0, 0], [0, -1]], #[[0, -1], [0, 1], [0, 1], [0, -1]]' 61 | #orange: left 1, nothing, right 1, nothing 62 | ':orange_square:': [[0, -1], [0, 0], [-1, 1], [0, 0]], #[[0, -1], [0, 0], [0, 1], [0, 0]] 63 | #none for yellow 64 | ':yellow_square:': [[0, 0], [0, 0], [0, 0], [0, 0]], 65 | #green: right 1, nothing, right 1, nothing 66 | ':green_square:': [[0, 0], [0, 0], [0, 0], [0, 0]], #[[0, 1], [0, 0], [0, 1], [0, 0]] 67 | #purple: nothing, right 1, left 1 (possibly up too), right 1 68 | ':purple_square:': [[0, 0], [1, 1], [0, -1], [0, 1]], #[[0, 0], [0, 1], [0, -1], [0, 1]] 69 | #red: left 1, up 1, right 1, up 1 70 | ':red_square:': [[1, -1], [-1, -1], [0, 2], [-1, -1]] #[[0, -1], [-1, 0], [0, 1], [-1, 0]] 71 | } 72 | 73 | #starting spots, right above the board ready to be lowered. Col is 3/4 to start in middle 74 | shape_I = Tetronimo([[0, 3], [0, 4], [0, 5], [0, 6]], blue_square, [1, 1, 1, 1]) 75 | shape_J = Tetronimo([[0, 3], [0, 4], [0, 5], [-1, 3]], brown_square, [1, 1, 2, 2]) 76 | shape_L = Tetronimo([[0, 3], [0, 4], [0, 5], [-1, 5]], orange_square, [1, 2, 2, 1]) 77 | shape_O = Tetronimo([[0, 4], [0, 5], [-1, 4], [-1, 5]], yellow_square, [1, 1, 1, 1]) 78 | shape_S = Tetronimo([[0, 3], [0, 4], [-1, 4], [-1, 5]], green_square, [2, 2, 2, 2]) 79 | shape_T = Tetronimo([[0, 3], [0, 4], [0, 5], [-1, 4]], purple_square, [1, 1, 3, 0]) 80 | shape_Z = Tetronimo([[0, 4], [0, 5], [-1, 3], [-1, 4]], red_square, [0, 1, 0, 2]) 81 | 82 | 83 | #fill board with empty squares 84 | def make_empty_board(): 85 | for row in range(num_of_rows): 86 | board.append([]) 87 | for col in range(num_of_cols): 88 | board[row].append(empty_square) 89 | 90 | def fill_board(emoji): 91 | for row in range(num_of_rows): 92 | for col in range(num_of_cols): 93 | if board[row][col] != emoji: 94 | board[row][col] = emoji 95 | 96 | 97 | def format_board_as_str(): 98 | board_as_str = '' 99 | for row in range(num_of_rows): 100 | for col in range(num_of_cols): 101 | board_as_str += (board[row][col]) # + " " possibly 102 | if col == num_of_cols - 1: 103 | board_as_str += "\n " 104 | return board_as_str 105 | 106 | def get_random_shape(): 107 | global index 108 | # ordered_shapes = [shape_J, shape_T, shape_L, shape_O, shape_S, shape_Z, shape_S, shape_T, shape_J, shape_Z, shape_S, shape_I, shape_Z, shape_O, shape_T, shape_J, shape_L, shape_Z, shape_I] 109 | # random_shape = ordered_shapes[index] 110 | shapes = [shape_I, shape_J, shape_L, shape_O, shape_S, shape_T, shape_Z] 111 | random_shape = shapes[random.randint(0, 6)] #0, 6 112 | index += 1 113 | if start_higher == True: 114 | for s in random_shape.starting_pos[:]: #for each square 115 | s[0] = s[0] - 1 #make row 1 above 116 | else: 117 | starting_pos = random_shape.starting_pos[:] 118 | random_shape = [random_shape.starting_pos[:], random_shape.colour, random_shape.rotation_points] #gets starting point of shapes and copies, doesn't change them 119 | global is_new_shape 120 | is_new_shape = True 121 | return random_shape #returns array with starting pos and colour 122 | 123 | def do_wall_kicks(shape, old_shape_pos, shape_colour, attempt_kick_num): 124 | new_shape_pos = [] 125 | 126 | if shape_colour == blue_square: 127 | kick_set = main_wall_kicks[rotation_pos] 128 | else: 129 | kick_set = i_wall_kicks[rotation_pos] 130 | 131 | print('Kick set: ' + str(kick_set)) 132 | for kick in kick_set: 133 | print('Kick: ' + str(kick)) 134 | for square in shape: 135 | square_row = square[0] 136 | square_col = square[1] 137 | new_square_row = square_row + kick[0] 138 | new_square_col = square_col + kick[1] 139 | if (0 <= new_square_col < num_of_cols) and (0 <= new_square_row < num_of_rows): #if square checking is on board 140 | square_checking = board[new_square_row][new_square_col] #get the square to check if empty 141 | if (square_checking != empty_square) and ([new_square_row, new_square_col] not in old_shape_pos): #if square is not empty / won't be when other parts of shape have moved 142 | #shape doesn't fit 143 | new_shape_pos = [] #reset new_shape 144 | break 145 | else: #shape does fit 146 | new_shape_pos.append([new_square_row, new_square_col]) #store pos 147 | print('New shape: ' + str(new_shape_pos)) 148 | if len(new_shape_pos) == 4: 149 | print('Returned new shape after doing kicks') 150 | return new_shape_pos #return shape with kicks added 151 | else: 152 | #shape doesn't fit 153 | new_shape_pos = [] #reset new_shape 154 | break 155 | 156 | print('Returned old, unrotated shape') 157 | return old_shape_pos #return shape without rotation 158 | 159 | 160 | def rotate_shape(shape, direction, rotation_point_index, shape_colour): 161 | rotation_point = shape[rotation_point_index] #coords of rotation point 162 | new_shape = [] #to store coords of rotated shape 163 | 164 | #Rotate shape 165 | for square in shape: 166 | square_row = square[0] 167 | square_col = square[1] 168 | if direction == 'clockwise': 169 | new_square_row = (square_col - rotation_point[1]) + rotation_point[0] + rot_adjustments.get(shape_colour)[rotation_pos-1][0] 170 | print('Adjustment made: ' + str(rot_adjustments.get(shape_colour)[rotation_pos-1][0])) 171 | new_square_col = -(square_row - rotation_point[0]) + rotation_point[1] + rot_adjustments.get(shape_colour)[rotation_pos-1][1] 172 | print('Adjustment made: ' + str(rot_adjustments.get(shape_colour)[rotation_pos-1][1])) 173 | elif direction == 'anticlockwise': #currently not a thing 174 | new_square_row = -(square_col - rotation_point[1]) + rotation_point[0] 175 | new_square_col = (square_row - rotation_point[0]) + rotation_point[1] 176 | new_shape.append([new_square_row, new_square_col]) #store pos of rotated square 177 | if (0 <= square_col < num_of_cols) and (0 <= square_row < num_of_rows): #if on board 178 | board[square_row][square_col] = empty_square #make empty old square pos 179 | 180 | new_shape = do_wall_kicks(new_shape, shape, shape_colour, 0) #offset shape 181 | 182 | new_shape = sorted(new_shape, key=lambda l:l[0], reverse=True) #sort so that bottom squares are first in list 183 | print('Rotated shape: ' + str(new_shape)) 184 | 185 | #Place rotated shape (in case can't move down) 186 | if new_shape != shape: #if not same as old unrotated shape (in case places at start pos) 187 | for square in new_shape: 188 | square_row = square[0] 189 | square_col = square[1] 190 | board[square_row][square_col] = shape_colour 191 | 192 | return new_shape 193 | 194 | def clear_lines(): 195 | global board 196 | global points 197 | global lines 198 | lines_to_clear = 0 199 | for row in range(num_of_rows): 200 | row_full = True #assume line is full 201 | for col in range(num_of_cols): 202 | if board[row][col] == empty_square: 203 | row_full = False 204 | break #don't clear this row 205 | if row_full: #if line to clear 206 | lines_to_clear += 1 207 | #bring all lines above down 208 | board2 = board[:] #clone board 209 | for r in range(row, 0, -1): #for every row above row 210 | if r == 0: #if top row 211 | for c in range(num_of_cols): 212 | board2[r][c] = empty_square #make each spot empty 213 | else: 214 | for c in range(num_of_cols): 215 | board2[r][c] = board[r - 1][c] #make each spot the one above 216 | board = board2[:] 217 | if lines_to_clear == 1: 218 | points += 100 219 | lines += 1 220 | elif lines_to_clear == 2: 221 | points += 300 222 | lines += 2 223 | elif lines_to_clear == 3: 224 | points += 500 225 | lines += 3 226 | elif lines_to_clear == 4: 227 | points += 800 228 | lines += 4 229 | 230 | 231 | def get_next_pos(cur_shape_pos): 232 | global h_movement 233 | global start_higher 234 | global game_over 235 | 236 | #Check if new pos for whole shape is available 237 | movement_amnt = 1 238 | 239 | if down_pressed == False: 240 | amnt_to_check = 1 #check space one below 241 | else: 242 | amnt_to_check = num_of_rows #check all rows until furthest available space 243 | 244 | for i in range(amnt_to_check): 245 | square_num_in_shape = -1 246 | for square in cur_shape_pos: 247 | next_space_free = True 248 | square_num_in_shape += 1 249 | square_row = square[0] 250 | square_col = square[1] 251 | if (0 <= square_col < num_of_cols): #if current column spot will fit 252 | if not (0 <= square_col + h_movement < num_of_cols): #if spot with column position changed won't fit 253 | h_movement = 0 #just change row position 254 | if (0 <= square_row + movement_amnt < num_of_rows): #if new square row pos is on board 255 | square_checking = board[square_row + movement_amnt][square_col + h_movement] #get the square to check if empty 256 | if (square_checking != empty_square) and ([square_row + movement_amnt, square_col + h_movement] not in cur_shape_pos): #if square is not empty / won't be when other parts of shape have moved 257 | #check if space free if not moving horizontally (in case going into wall) but still going down 258 | h_movement = 0 259 | square_checking = board[square_row + movement_amnt][square_col + h_movement] 260 | if (square_checking != empty_square) and ([square_row + movement_amnt, square_col + h_movement] not in cur_shape_pos): 261 | if movement_amnt == 1: 262 | next_space_free = False #can't put shape there 263 | print('Detected a space that isnt free') 264 | print('Square checking: ' + str(square_row + movement_amnt) + ', ' + str(square_col + h_movement)) 265 | if is_new_shape: #if can't place new shape 266 | if start_higher == True: 267 | game_over = True 268 | else: 269 | start_higher = True 270 | elif movement_amnt > 1: #if sending down 271 | movement_amnt -= 1 #accomodate for extra 1 added to check if its free 272 | return [movement_amnt, next_space_free] #stop checking 273 | elif down_pressed == True: 274 | if square_num_in_shape == 3: #only on last square in shape 275 | movement_amnt += 1 #increase amount to move shape by 276 | elif square_row + movement_amnt >= num_of_rows: #new square row isn't on board 277 | if movement_amnt == 1: 278 | next_space_free = False #can't put shape there 279 | print('Detected a space that isnt free') 280 | elif movement_amnt > 1: #if sending down 281 | movement_amnt -= 1 #accomodate for extra 1 added to check if its free 282 | return [movement_amnt, next_space_free] #stop checking 283 | elif down_pressed == True: 284 | if square_num_in_shape == 3: #only on last square in shape 285 | movement_amnt += 1 #increase amount to move shape by 286 | 287 | return [movement_amnt, next_space_free] 288 | 289 | 290 | async def run_game(msg, cur_shape): 291 | global is_new_shape 292 | global h_movement 293 | global rotate_clockwise 294 | global rotation_pos 295 | 296 | cur_shape_pos = cur_shape[0] 297 | cur_shape_colour = cur_shape[1] 298 | 299 | if rotate_clockwise == True and cur_shape_colour != yellow_square: 300 | cur_shape_pos = rotate_shape(cur_shape_pos, 'clockwise', cur_shape[2][rotation_pos], cur_shape_colour) #rotate shape 301 | cur_shape = [cur_shape_pos, cur_shape_colour, cur_shape[2]] #update shape 302 | 303 | next_pos = get_next_pos(cur_shape_pos)[:] 304 | movement_amnt = next_pos[0] 305 | next_space_free = next_pos[1] 306 | 307 | #move/place shape if pos is available 308 | square_num_in_shape = -1 309 | if next_space_free: 310 | for square in cur_shape_pos: 311 | square_num_in_shape += 1 312 | square_row = square[0] 313 | square_col = square[1] 314 | if (0 <= square_row + movement_amnt < num_of_rows): #if new square row pos is on board 315 | square_changing = board[square_row + movement_amnt][square_col + h_movement] #get square to change 316 | board[square_row + movement_amnt][square_col + h_movement] = cur_shape_colour #changes square colour to colour of shape 317 | if is_new_shape == True: 318 | is_new_shape = False #has been placed, so not new anymore 319 | if square_row > -1: #stops from wrapping around list and changing colour of bottom rows. 320 | board[square_row][square_col] = empty_square #make old square empty again 321 | cur_shape_pos[square_num_in_shape] = [square_row + movement_amnt, square_col + h_movement] #store new pos of shape square 322 | else: #if new square row pos is not on board 323 | cur_shape_pos[square_num_in_shape] = [square_row + movement_amnt, square_col + h_movement] #store new pos of shape square 324 | else: 325 | global down_pressed 326 | down_pressed = False #reset it 327 | clear_lines() #check for full lines and clear them 328 | cur_shape = get_random_shape() #change shape 329 | rotation_pos = 0 #reset rotation 330 | print('Changed shape.') 331 | 332 | if not game_over: 333 | #Update board 334 | embed = discord.Embed(description=format_board_as_str(), color=embed_colour) 335 | h_movement = 0 #reset horizontal movement 336 | rotate_clockwise = False #reset clockwise rotation 337 | await msg.edit(embed=embed) 338 | if not is_new_shape: 339 | await asyncio.sleep(1) #to keep under api rate limit 340 | await run_game(msg, cur_shape) 341 | else: 342 | print('GAME OVER') 343 | desc = 'Score: {} \n Lines: {} \n \n Press ▶ to play again.'.format(points, lines) 344 | embed = discord.Embed(title='GAME OVER', description=desc, color=embed_colour) 345 | await msg.edit(embed=embed) 346 | await msg.remove_reaction("⬅", client.user) #Left 347 | await msg.remove_reaction("⬇", client.user) #Down 348 | await msg.remove_reaction("➡", client.user) #Right 349 | await msg.remove_reaction("🔃", client.user) #Rotate 350 | await msg.add_reaction("▶") #Play 351 | 352 | 353 | async def reset_game(): 354 | global down_pressed 355 | global rotate_clockwise 356 | global rotation_pos 357 | global h_movement 358 | global is_new_shape 359 | global start_higher 360 | global game_over 361 | global points 362 | global lines 363 | fill_board(empty_square) 364 | down_pressed = False 365 | rotate_clockwise = False 366 | rotation_pos = 0 367 | h_movement = 0 #amount to move left or right 368 | is_new_shape = False 369 | start_higher = False 370 | game_over = False 371 | next_space_free = True 372 | points = 0 373 | lines = 0 374 | 375 | make_empty_board() 376 | 377 | 378 | #------------------------------------------------------------------------------- 379 | 380 | client = commands.Bot(command_prefix = 't!') 381 | 382 | @client.event 383 | async def on_ready(): 384 | print("tetris bot started poggies") 385 | 386 | @client.command() 387 | async def test(ctx): 388 | await ctx.send('test working poggies pogchamp') 389 | 390 | @client.command() 391 | async def start(ctx): #Starts embed 392 | await reset_game() 393 | embed = discord.Embed(title='Tetris in Discord', description=format_board_as_str(), color=embed_colour) 394 | embed.add_field(name='How to Play:', value='Use ⬅ ⬇ ➡ to move left, down, and right respectively. \n \n Use 🔃 to rotate the shape clockwise. \n \n Press ▶ to Play.', inline=False) 395 | 396 | msg = await ctx.send(embed=embed) 397 | 398 | #Add button choices / reactions 399 | await msg.add_reaction("▶") #Play 400 | 401 | #On new reaction: 402 | #Update board and board_as_str 403 | #await msg.edit(embed=embed) 404 | 405 | @client.event 406 | async def on_reaction_add(reaction, user): 407 | global h_movement 408 | global rotation_pos 409 | if user != client.user: 410 | msg = reaction.message 411 | if str(reaction.emoji) == "▶": #Play button pressed 412 | print('User pressed play') 413 | await reset_game() 414 | await msg.remove_reaction("❌", client.user) #Remove delete 415 | embed = discord.Embed(description=format_board_as_str(), color=embed_colour) 416 | await msg.remove_reaction("▶", user) 417 | await msg.remove_reaction("▶", client.user) 418 | await msg.edit(embed=embed) 419 | await msg.add_reaction("⬅") #Left 420 | await msg.add_reaction("⬇") #Down 421 | await msg.add_reaction("➡") #Right 422 | await msg.add_reaction("🔃") #Rotate 423 | await msg.add_reaction("❌") #Stop game 424 | starting_shape = get_random_shape() 425 | await run_game(msg, starting_shape) 426 | 427 | if str(reaction.emoji) == "⬅": #Left button pressed 428 | print('Left button pressed') 429 | h_movement = -1 #move 1 left 430 | await msg.remove_reaction("⬅", user) 431 | if str(reaction.emoji) == "➡": #Right button pressed 432 | print('Right button pressed') 433 | h_movement = 1 #move +1 right 434 | await msg.remove_reaction("➡", user) 435 | if str(reaction.emoji) == "⬇": #Down button pressed 436 | print('Down button pressed') 437 | global down_pressed 438 | down_pressed = True 439 | await msg.remove_reaction("⬇", user) 440 | if str(reaction.emoji) == "🔃": #Rotate clockwise button pressed 441 | print('Rotate clockwise button pressed') 442 | global rotate_clockwise 443 | rotate_clockwise = True 444 | if rotation_pos < 3: 445 | rotation_pos += 1 446 | else: 447 | rotation_pos = 0 #go back to original pos 448 | await msg.remove_reaction("🔃", user) 449 | if str(reaction.emoji) == "❌": #Stop game button pressed 450 | #In future maybe put score screen here or a message saying stopping. 451 | await reset_game() 452 | await msg.delete() 453 | if str(reaction.emoji) == "🔴": 454 | await message.edit(content="") 455 | 456 | 457 | client.run('Your token here') 458 | --------------------------------------------------------------------------------