diff --git a/Metro/Metro_RP2350_Memory/memory_game/btn_exit.bmp b/Metro/Metro_RP2350_Memory/memory_game/btn_exit.bmp new file mode 100644 index 000000000..2c3347e6b Binary files /dev/null and b/Metro/Metro_RP2350_Memory/memory_game/btn_exit.bmp differ diff --git a/Metro/Metro_RP2350_Memory/memory_game/btn_play_again.bmp b/Metro/Metro_RP2350_Memory/memory_game/btn_play_again.bmp new file mode 100644 index 000000000..ce4b8fed9 Binary files /dev/null and b/Metro/Metro_RP2350_Memory/memory_game/btn_play_again.bmp differ diff --git a/Metro/Metro_RP2350_Memory/memory_game/code.py b/Metro/Metro_RP2350_Memory/memory_game/code.py new file mode 100644 index 000000000..9db9f919b --- /dev/null +++ b/Metro/Metro_RP2350_Memory/memory_game/code.py @@ -0,0 +1,504 @@ +# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT +""" +An implementation of the card game memory. Players take turns flipping +over two cards trying to find pairs. After the turn any non-pairs are +flipped face down so the players must try to remember where they are. + +Players trade off using the USB mouse to play their turns. +""" +import array +import random +import time +from displayio import Group, OnDiskBitmap, TileGrid +from adafruit_display_text.bitmap_label import Label +from adafruit_display_text.text_box import TextBox +from adafruit_displayio_layout.layouts.grid_layout import GridLayout +from adafruit_ticks import ticks_ms +import supervisor +import terminalio +import usb.core + + +def random_selection(lst, count): + """ + Select items randomly from a list of items. + + returns a list of length count containing the selected items. + """ + if len(lst) <= count: + raise ValueError("Count must be less than or equal to length of list") + iter_copy = list(lst) + selection = set() + while len(selection) < count: + selection.add(iter_copy.pop(random.randrange(len(iter_copy)))) + return list(selection) + + +def update_score_text(): + """ + Update the score text on the display for each player + """ + for _ in range(2): + out_str = f"p{_+1} score: {player_scores[_]}" + score_lbls[_].text = out_str + + +# state machine constants + +# title state, shows title screen waits for click +STATE_TITLE = 0 + +# playing state alternates players flipping cards to play the game +STATE_PLAYING = 1 + +# shows the game over message and waits for a button to be clicked +STATE_GAMEOVER = 2 + +# initial state is title screen +CUR_STATE = STATE_TITLE + +# pylint: disable=ungrouped-imports +if hasattr(supervisor.runtime, "display") and supervisor.runtime.display is not None: + # use the built-in HSTX display for Metro RP2350 + display = supervisor.runtime.display +else: + # pylint: disable=ungrouped-imports + from displayio import release_displays + import picodvi + import board + import framebufferio + + # initialize display + release_displays() + + fb = picodvi.Framebuffer( + 320, + 240, + clk_dp=board.CKP, + clk_dn=board.CKN, + red_dp=board.D0P, + red_dn=board.D0N, + green_dp=board.D1P, + green_dn=board.D1N, + blue_dp=board.D2P, + blue_dn=board.D2N, + color_depth=16, + ) + display = framebufferio.FramebufferDisplay(fb) + +# main group will hold all of our visual elements +main_group = Group() + +# make main group visible on the display +display.root_group = main_group + +# list of Label instances for player scores +score_lbls = [] + +# list of colors, one representing each player +colors = [0xFF00FF, 0x00FF00] + +# randomly choose the first player +current_turn_index = random.randrange(0, 2) + +# list that holds up to 2 cards that have been flipped over +# on the current turn +cards_flipped_this_turn = [] + +# list that holds the scores of each player +player_scores = [0, 0] + +# size of the grid of cards to layout +grid_size = (6, 4) + +# create a grid layout to help place cards neatly +# into a grid on the display +card_grid = GridLayout(x=10, y=10, width=260, height=200, grid_size=grid_size) + +# these indexes within the spritesheet contain the +# card front sprites, there are 8 different cards total. +CARD_FRONT_SPRITE_INDEXES = {1, 2, 3, 4, 5, 6, 7, 9} + +# pool of cards to deal them onto the board from +# starts with 2 copies of each of 8 different cards +pool = list(CARD_FRONT_SPRITE_INDEXES) + list(CARD_FRONT_SPRITE_INDEXES) + +# select 4 cards at random that will be duplicated +duplicates = random_selection(CARD_FRONT_SPRITE_INDEXES, 4) + +# add 2 copies each of the 4 selected duplicate cards +# this brings the pool to 24 cards total +pool += duplicates + duplicates + +# list that represents the order the cards are randomly +# dealt out into. The board is a two-dimensional grid, +# but this list is one dimension where +# index in the list = y * width + x in the grid. +card_locations = [] + +# load the spritesheet for the cards +sprites = OnDiskBitmap("memory_game_sprites.bmp") + +# list to hold TileGrid instances for each card +card_tgs = [] + +# loop over 4 rows +for y in range(4): + # loop over 6 columns + for x in range(6): + # i = y * 6 + x + + # create a TileGrid + new_tg = TileGrid( + bitmap=sprites, + default_tile=10, + tile_height=32, + tile_width=32, + height=1, + width=1, + pixel_shader=sprites.pixel_shader, + ) + + # add it to the list of card tilegrids + card_tgs.append(new_tg) + + # add it to the grid layout at the current x,y position + card_grid.add_content(new_tg, grid_position=(x, y), cell_size=(1, 1)) + + # choose a random index of a card in the pool + random_choice = random.randrange(0, len(pool) - 1) if len(pool) > 1 else 0 + + # remove the chosen card from the pool, and add it + # to the card locations list at the current location + card_locations.append(pool.pop(random_choice)) + +# center the card grid layout horizontally +card_grid.x = display.width // 2 - card_grid.width // 2 + +# move the card grid layout towards the bottom of the screen +card_grid.y = display.height - card_grid.height + +# add the card grid to the main group +main_group.append(card_grid) + +# create a group to hold the game over elements +game_over_group = Group() + +# create a TextBox to hold the game over message +game_over_label = TextBox( + terminalio.FONT, + text="", + color=0xFFFFFF, + background_color=0x222222, + width=display.width // 2, + height=80, + align=TextBox.ALIGN_CENTER, +) +# move it to the center top of the display +game_over_label.anchor_point = (0, 0) +game_over_label.anchored_position = ( + display.width // 2 - game_over_label.width // 2, + 40, +) + +# make it hidden, we'll show it when the game is over. +game_over_group.hidden = True + +# add the game over lable to the game over group +game_over_group.append(game_over_label) + +# load the play again, and exit button bitmaps +play_again_btn_bmp = OnDiskBitmap("btn_play_again.bmp") +exit_btn_bmp = OnDiskBitmap("btn_exit.bmp") + +# create TileGrid for the play again button +play_again_btn = TileGrid( + bitmap=play_again_btn_bmp, pixel_shader=play_again_btn_bmp.pixel_shader +) + +# transparent pixels in the corners for the rounded corner effect +play_again_btn_bmp.pixel_shader.make_transparent(0) + +# centered within the display, offset to the left +play_again_btn.x = display.width // 2 - play_again_btn_bmp.width // 2 - 30 + +# inside the bounds of the game over label, so it looks like a dialog visually +play_again_btn.y = 80 + +# create TileGrid for the exit button +exit_btn = TileGrid(bitmap=exit_btn_bmp, pixel_shader=exit_btn_bmp.pixel_shader) + +# transparent pixels in the corners for the rounded corner effect +exit_btn_bmp.pixel_shader.make_transparent(0) + +# centered within the display, offset to the right +exit_btn.x = display.width // 2 - exit_btn_bmp.width // 2 + 30 + +# inside the bounds of the game over label, so it looks like a dialog visually +exit_btn.y = 80 + +# add the play again and exit buttons to the game over group +game_over_group.append(play_again_btn) +game_over_group.append(exit_btn) + +# add the game over group to the main group +main_group.append(game_over_group) + +# create score label for each player +for i in range(2): + # create a new label to hold score + score_lbl = Label(terminalio.FONT, text="", color=colors[i], scale=1) + if i == 0: + # if it's player 1 put it in the top left + score_lbl.anchor_point = (0, 0) + score_lbl.anchored_position = (4, 1) + else: + # if it's player 2 put it tin the top right + score_lbl.anchor_point = (1.0, 0) + score_lbl.anchored_position = (display.width - 4, 1) + + # add the label to list of score labels + score_lbls.append(score_lbl) + + # add the label to the main group + main_group.append(score_lbl) + +# initialize the text in the score labels to show 0 +update_score_text() + +# create a label to indicate which player's turn it is +current_player_lbl = Label( + terminalio.FONT, text="Current Player", color=colors[current_turn_index], scale=1 +) + +# place it centered horizontally at the top of the screen +current_player_lbl.anchor_point = (0.5, 0) +current_player_lbl.anchored_position = (display.width // 2, 1) + +# add the score label to the main group +main_group.append(current_player_lbl) + +# load the title screen bitmap +title_screen_bmp = OnDiskBitmap("memory_title.bmp") + +# create a TileGrid for the title screen +title_screen_tg = TileGrid( + bitmap=title_screen_bmp, pixel_shader=title_screen_bmp.pixel_shader +) + +# add it to the main group +main_group.append(title_screen_tg) + +# load the mouse bitmap +mouse_bmp = OnDiskBitmap("mouse_cursor.bmp") + +# make the background pink pixels transparent +mouse_bmp.pixel_shader.make_transparent(0) + +# create a TileGrid for the mouse +mouse_tg = TileGrid(mouse_bmp, pixel_shader=mouse_bmp.pixel_shader) + +# place it in the center of the display +mouse_tg.x = display.width // 2 +mouse_tg.y = display.height // 2 + +# add the mouse to the main group +main_group.append(mouse_tg) + +# variable for the mouse USB device instance +mouse = None + +# wait a second for USB devices to be ready +time.sleep(1) + +# scan for connected USB devices +for device in usb.core.find(find_all=True): + # print information about the found devices + print(f"{device.idVendor:04x}:{device.idProduct:04x}") + print(device.manufacturer, device.product) + print(device.serial_number) + + # assume this device is the mouse + mouse = device + + # detach from kernel driver if active + if mouse.is_kernel_driver_active(0): + mouse.detach_kernel_driver(0) + + # set the mouse configuration so it can be used + mouse.set_configuration() + + +# Buffer to hold data read from the mouse +# Boot mice have 4 byte reports +buf = array.array("b", [0] * 4) + +# timestamp in the future to wait until before +# awarding points for a pair, or flipping cards +# back over and changing turns +WAIT_UNTIL = 0 + +# bool indicating whether we are waiting to reset flipped +# cards and change turns or award points and remove +# cards. Will be True if we are waiting to take action, +# False otherwise. +waiting_to_reset = False + +# main loop +while True: + # timestamp of the current time + now = ticks_ms() + + # attempt mouse read + try: + # read data from the mouse, small timeout so we move on + # quickly if there is no data + data_len = mouse.read(0x81, buf, timeout=10) + + # if we got data, then update the mouse cursor on the display + # using min and max to keep it within the bounds of the display + mouse_tg.x = max(0, min(display.width - 1, mouse_tg.x + buf[1] // 2)) + mouse_tg.y = max(0, min(display.height - 1, mouse_tg.y + buf[2] // 2)) + + # timeout error is raised if no data was read within the alotted timeout + except usb.core.USBTimeoutError: + # no problem, just go on + pass + + # if the current state is title screen + if CUR_STATE == STATE_TITLE: + # if the left mouse button was clicked + if buf[0] & (1 << 0) != 0: + # change the current state to playing + CUR_STATE = STATE_PLAYING + # hide the title screen + title_screen_tg.hidden = True + # change the mouse cursor color to match the current player + mouse_bmp.pixel_shader[2] = colors[current_turn_index] + + # if the current state is playing + elif CUR_STATE == STATE_PLAYING: + + # if we are waiting to reset, and it's time to take action + if waiting_to_reset and now >= WAIT_UNTIL: + # this means that there are already 2 cards flipped face up. + # we need to either award points, or flip them back over and + # change to the next players turn. + + # change variable to indicate we're no longer waiting to take action + waiting_to_reset = False + + # if both cards were the same i.e. they found a match + if ( + card_tgs[cards_flipped_this_turn[0]][0] + == card_tgs[cards_flipped_this_turn[1]][0] + ): + + # set the cards tile index to show a blank spot instead of a card + card_tgs[cards_flipped_this_turn[0]][0] = 8 + card_tgs[cards_flipped_this_turn[1]][0] = 8 + + # award a point to the player + player_scores[current_turn_index] += 1 + + # refresh the score texts to show the new score + update_score_text() + + # if the total of both players scores is equal to half the amount + # of cards then we know the game is over because each pair is worth 1 + # point + if ( + player_scores[0] + player_scores[1] + >= (grid_size[0] * grid_size[1]) // 2 + ): + + # if the player's scores are equal + if player_scores[0] == player_scores[1]: + # set the game over message to tie game + game_over_label.text = "Game Over\nTie Game" + + else: # player scores are not equal + + # if player 2 score is larger than player 1 + if player_scores[0] < player_scores[1]: + # set the game over message to indicate player 2 victory + game_over_label.text = "Game Over\nPlayer 2 Wins" + game_over_label.color = colors[1] + + else: # player 1 score is larger than player 2 + # set the game over message to indicate player 1 victory + game_over_label.text = "Game Over\nPlayer 1 Wins" + game_over_label.color = colors[0] + + # set the game over group to visible + game_over_group.hidden = False + + # change the state to gameover + CUR_STATE = STATE_GAMEOVER + + else: # the two cards were different i.e. they did not find a match + # set both cards tile index to the card back sprite to flip it back over + card_tgs[cards_flipped_this_turn[0]][0] = 10 + card_tgs[cards_flipped_this_turn[1]][0] = 10 + + # go to the next players turn + current_turn_index = (current_turn_index + 1) % 2 + + # update the color of the current player indicator + current_player_lbl.color = colors[current_turn_index] + + # update the color of the mouse cursor + mouse_bmp.pixel_shader[2] = colors[current_turn_index] + + # empty out the cards flipped this turn list + cards_flipped_this_turn = [] + + # ignore any clicks while we're waiting to take reset cards + if now >= WAIT_UNTIL: + # left btn pressed + if buf[0] & (1 << 0) != 0: + + # loop over all cards + for card_index, card in enumerate(card_tgs): + # coordinates of the mouse taking into account + # the offset from the card_grid position + coords = (mouse_tg.x - card_grid.x, mouse_tg.y - card_grid.y, 0) + + # if this is a face down card, and the mouse coordinates + # are within its bounding box + if card[0] == 10 and card.contains(coords): + # flip the card face up by setting its tile index + # to the appropriate value from the card_locations list + card[0] = card_locations[card_tgs.index(card)] + + # add this card index to the cards flipped this turn list + cards_flipped_this_turn.append(card_index) + + # if 2 cards have been flipped this turn + if len(cards_flipped_this_turn) == 2: + # set the wait until time to a little bit in the future + WAIT_UNTIL = ticks_ms() + 1500 + # set the waiting to reset flag to True + waiting_to_reset = True + + # if the current state is gameover + elif CUR_STATE == STATE_GAMEOVER: + # left btn pressed + if buf[0] & (1 << 0) != 0: + # get the coordinates of the mouse cursor point + coords = (mouse_tg.x, mouse_tg.y, 0) + + # if the mouse point is within the play again + # button bounding box + if play_again_btn.contains(coords): + # set next code file to this one + supervisor.set_next_code_file(__file__) + # reload + supervisor.reload() + + # if the mouse pint is within the exit + # button bounding box + if exit_btn.contains(coords): + # break to exit out of this script + break diff --git a/Metro/Metro_RP2350_Memory/memory_game/memory_game_sprites.bmp b/Metro/Metro_RP2350_Memory/memory_game/memory_game_sprites.bmp new file mode 100644 index 000000000..939717fcd Binary files /dev/null and b/Metro/Metro_RP2350_Memory/memory_game/memory_game_sprites.bmp differ diff --git a/Metro/Metro_RP2350_Memory/memory_game/memory_title.bmp b/Metro/Metro_RP2350_Memory/memory_game/memory_title.bmp new file mode 100644 index 000000000..54ae2b30f Binary files /dev/null and b/Metro/Metro_RP2350_Memory/memory_game/memory_title.bmp differ diff --git a/Metro/Metro_RP2350_Memory/memory_game/mouse_cursor.bmp b/Metro/Metro_RP2350_Memory/memory_game/mouse_cursor.bmp new file mode 100644 index 000000000..94ec32889 Binary files /dev/null and b/Metro/Metro_RP2350_Memory/memory_game/mouse_cursor.bmp differ diff --git a/Metro/Metro_RP2350_Memory/mouse_demo/code.py b/Metro/Metro_RP2350_Memory/mouse_demo/code.py new file mode 100644 index 000000000..4518e8efa --- /dev/null +++ b/Metro/Metro_RP2350_Memory/mouse_demo/code.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT +""" +This example is made for a basic Microsoft optical mouse with +two buttons and a wheel that can be pressed. + +It assumes there is a single mouse connected to USB Host, +and no other devices connected. +""" +import array +from displayio import Group, OnDiskBitmap, TileGrid +from adafruit_display_text.bitmap_label import Label +import supervisor +import terminalio +import usb.core + +# pylint: disable=ungrouped-imports +if hasattr(supervisor.runtime, "display") and supervisor.runtime.display is not None: + # use the built-in HSTX display for Metro RP2350 + display = supervisor.runtime.display +else: + # pylint: disable=ungrouped-imports + from displayio import release_displays + import picodvi + import board + import framebufferio + + # initialize display + release_displays() + + fb = picodvi.Framebuffer( + 320, + 240, + clk_dp=board.CKP, + clk_dn=board.CKN, + red_dp=board.D0P, + red_dn=board.D0N, + green_dp=board.D1P, + green_dn=board.D1N, + blue_dp=board.D2P, + blue_dn=board.D2N, + color_depth=16, + ) + display = framebufferio.FramebufferDisplay(fb) + +# group to hold visual elements +main_group = Group() + +# make the group visible on the display +display.root_group = main_group + +# load the mouse cursor bitmap +mouse_bmp = OnDiskBitmap("mouse_cursor.bmp") + +# make the background pink pixels transparent +mouse_bmp.pixel_shader.make_transparent(0) + +# create a TileGrid for the mouse, using its bitmap and pixel_shader +mouse_tg = TileGrid(mouse_bmp, pixel_shader=mouse_bmp.pixel_shader) + +# move it to the center of the display +mouse_tg.x = display.width // 2 +mouse_tg.y = display.height // 2 + +# text label to show the x, y coordinates on the screen +output_lbl = Label( + terminalio.FONT, text=f"{mouse_tg.x},{mouse_tg.y}", color=0xFFFFFF, scale=1 +) + +# move it to the upper left corner +output_lbl.anchor_point = (0, 0) +output_lbl.anchored_position = (1, 1) + +# add it to the main group +main_group.append(output_lbl) + +# add the mouse tile grid to the main group +main_group.append(mouse_tg) + +# button names +# This is ordered by bit position. +BUTTONS = ["left", "right", "middle"] + +# scan for connected USB device and loop over any found +for device in usb.core.find(find_all=True): + # print device info + print(f"{device.idVendor:04x}:{device.idProduct:04x}") + print(device.manufacturer, device.product) + print(device.serial_number) + # assume the device is the mouse + mouse = device + +# detach the kernel driver if needed +if mouse.is_kernel_driver_active(0): + mouse.detach_kernel_driver(0) + +# set configuration on the mouse so we can use it +mouse.set_configuration() + +# buffer to hold mouse data +# Boot mice have 4 byte reports +buf = array.array("b", [0] * 4) + +# main loop +while True: + try: + # attempt to read data from the mouse + # 10ms timeout, so we don't block long if there + # is no data + count = mouse.read(0x81, buf, timeout=10) + except usb.core.USBTimeoutError: + # skip the rest of the loop if there is no data + continue + + # update the mouse tilegrid x and y coordinates + # based on the delta values read from the mouse + mouse_tg.x = max(0, min(display.width - 1, mouse_tg.x + buf[1])) + mouse_tg.y = max(0, min(display.height - 1, mouse_tg.y + buf[2])) + + # string with updated coordinates for the text label + out_str = f"{mouse_tg.x},{mouse_tg.y}" + + # loop over the button names + for i, button in enumerate(BUTTONS): + # check if each button is pressed using bitwise AND shifted + # to the appropriate index for this button + if buf[0] & (1 << i) != 0: + # append the button name to the string to show if + # it is being clicked. + out_str += f" {button}" + + # update the text label with the new coordinates + # and buttons being pressed + output_lbl.text = out_str diff --git a/Metro/Metro_RP2350_Memory/mouse_demo/mouse_cursor.bmp b/Metro/Metro_RP2350_Memory/mouse_demo/mouse_cursor.bmp new file mode 100644 index 000000000..94ec32889 Binary files /dev/null and b/Metro/Metro_RP2350_Memory/mouse_demo/mouse_cursor.bmp differ diff --git a/Metro/Metro_RP2350_Memory/tictactoe_demo/code.py b/Metro/Metro_RP2350_Memory/tictactoe_demo/code.py new file mode 100644 index 000000000..886b896c9 --- /dev/null +++ b/Metro/Metro_RP2350_Memory/tictactoe_demo/code.py @@ -0,0 +1,253 @@ +# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT +""" +This example is made for a basic Microsoft optical mouse with +two buttons and a wheel that can be pressed. + +It assumes there is a single mouse connected to USB Host, +and no other devices connected. + +It illustrates multi-player turn based logic with a very +basic implementation of tic-tac-toe. +""" +import array +import random +from displayio import Group, OnDiskBitmap, TileGrid +from adafruit_display_text.bitmap_label import Label +from adafruit_displayio_layout.layouts.grid_layout import GridLayout +import supervisor +import terminalio +import usb.core + +# pylint: disable=ungrouped-imports +if hasattr(supervisor.runtime, "display") and supervisor.runtime.display is not None: + # use the built-in HSTX display for Metro RP2350 + display = supervisor.runtime.display +else: + from displayio import release_displays + import picodvi + import board + import framebufferio + + # initialize display + release_displays() + + fb = picodvi.Framebuffer( + 320, + 240, + clk_dp=board.CKP, + clk_dn=board.CKN, + red_dp=board.D0P, + red_dn=board.D0N, + green_dp=board.D1P, + green_dn=board.D1N, + blue_dp=board.D2P, + blue_dn=board.D2N, + color_depth=16, + ) + display = framebufferio.FramebufferDisplay(fb) + +# group to hold visual elements +main_group = Group() + +# make the group visible on the display +display.root_group = main_group + +# load the mouse cursor bitmap +mouse_bmp = OnDiskBitmap("mouse_cursor.bmp") + +# make the background pink pixels transparent +mouse_bmp.pixel_shader.make_transparent(0) + +# create a TileGrid for the mouse, using its bitmap and pixel_shader +mouse_tg = TileGrid(mouse_bmp, pixel_shader=mouse_bmp.pixel_shader) + +# move it to the center of the display +mouse_tg.x = display.width // 2 +mouse_tg.y = display.height // 2 + +# text label to show the x, y coordinates on the screen +output_lbl = Label(terminalio.FONT, text="", color=0xFFFFFF, scale=1) + +# move it to the right side of the screen +output_lbl.anchor_point = (0, 0) +output_lbl.anchored_position = (180, 40) + +# add it to the main group +main_group.append(output_lbl) + +# scan for connected USB device and loop over any found +for device in usb.core.find(find_all=True): + # print device info + print(f"{device.idVendor:04x}:{device.idProduct:04x}") + print(device.manufacturer, device.product) + print(device.serial_number) + # assume the device is the mouse + mouse = device + +# detach the kernel driver if needed +if mouse.is_kernel_driver_active(0): + mouse.detach_kernel_driver(0) + +# set configuration on the mouse so we can use it +mouse.set_configuration() + +# buffer to hold mouse data +# Boot mice have 4 byte reports +buf = array.array("b", [0] * 4) + +# set up a 3x3 grid for the tic-tac-toe board +board_grid = GridLayout(x=40, y=40, width=128, height=128, grid_size=(3, 3)) + +# load the tic-tac-toe spritesheet +tictactoe_spritesheet = OnDiskBitmap("tictactoe_spritesheet.bmp") + +# X is index 1 in the spritesheet, O is index 2 in the spritesheet +player_icon_indexes = [1, 2] + +# current player variable. +# When this equlas 0 its X's turn, +# when it equals 1 it is O's turn. +current_player_index = random.randint(0, 1) # randomize the initial player + +# loop over rows +for y in range(3): + # loop over columns + for x in range(3): + # create a TileGrid for this cell + new_tg = TileGrid( + bitmap=tictactoe_spritesheet, + default_tile=0, + tile_height=32, + tile_width=32, + height=1, + width=1, + pixel_shader=tictactoe_spritesheet.pixel_shader, + ) + + # add the new TileGrid to the board grid at the current position + board_grid.add_content(new_tg, grid_position=(x, y), cell_size=(1, 1)) + +# add the board grid to the main group +main_group.append(board_grid) + +# add the mouse tile grid to the main group +main_group.append(mouse_tg) + + +def check_for_winner(): + """ + check if a player has won + + :return: the player icon index of the winning player, + None if no winner and game continues, -1 if game ended in a tie. + """ + found_empty = False + + # check rows + for row_idx in range(3): + # if the 3 cells in this row match + if ( + board_grid[0 + (row_idx * 3)][0] != 0 + and board_grid[0 + (row_idx * 3)][0] + == board_grid[1 + (row_idx * 3)][0] + == board_grid[2 + (row_idx * 3)][0] + ): + return board_grid[0 + (row_idx * 3)][0] + + # if any of the cells in this row are empty + if 0 in ( + board_grid[0 + (row_idx * 3)][0], + board_grid[1 + (row_idx * 3)][0], + board_grid[2 + (row_idx * 3)][0], + ): + found_empty = True + + # check columns + for col_idx in range(3): + # if the 3 cells in this column match + if ( + board_grid[0 + col_idx][0] != 0 + and board_grid[0 + col_idx][0] + == board_grid[3 + col_idx][0] + == board_grid[6 + col_idx][0] + ): + return board_grid[0 + col_idx][0] + + # if any of the cells in this column are empty + if 0 in ( + board_grid[0 + col_idx][0], + board_grid[3 + col_idx][0], + board_grid[6 + col_idx][0], + ): + found_empty = True + + # check diagonals + if ( + board_grid[0][0] != 0 + and board_grid[0][0] == board_grid[4][0] == board_grid[8][0] + ): + return board_grid[0][0] + + if ( + board_grid[2][0] != 0 + and board_grid[2][0] == board_grid[4][0] == board_grid[6][0] + ): + return board_grid[2][0] + + if found_empty: + # return None if there is no winner and the game continues + return None + else: + # return -1 if it's a tie game with no winner + return -1 + + +# main loop +while True: + try: + # attempt to read data from the mouse + # 10ms timeout, so we don't block long if there + # is no data + count = mouse.read(0x81, buf, timeout=10) + except usb.core.USBTimeoutError: + # skip the rest of the loop if there is no data + continue + + # update the mouse tilegrid x and y coordinates + # based on the delta values read from the mouse + mouse_tg.x = max(0, min(display.width - 1, mouse_tg.x + buf[1])) + mouse_tg.y = max(0, min(display.height - 1, mouse_tg.y + buf[2])) + + # if left button clicked + if buf[0] & (1 << 0) != 0: + # get the mouse pointer coordinates accounting for the offset of + # the board grid location + coords = (mouse_tg.x - board_grid.x, mouse_tg.y - board_grid.y, 0) + + # loop over all cells in the board + for cell_tg in board_grid: + + # if the current cell is blank, and contains the clicked coordinates + if cell_tg[0] == 0 and cell_tg.contains(coords): + # set the current cell tile index to the current player's icon + cell_tg[0] = player_icon_indexes[current_player_index] + + # change to the next player + current_player_index = (current_player_index + 1) % 2 + + # print out which player's turn it is + print(f"It is now {'X' if current_player_index == 0 else 'O'}'s turn") + + # check for a winner + result = check_for_winner() + + # if Xs or Os have won + if result == 1: + output_lbl.text = "X is the winner!" + elif result == 2: + output_lbl.text = "O is the winner!" + + # if it was a tie game + elif result == -1: + output_lbl.text = "Tie game, no winner." diff --git a/Metro/Metro_RP2350_Memory/tictactoe_demo/mouse_cursor.bmp b/Metro/Metro_RP2350_Memory/tictactoe_demo/mouse_cursor.bmp new file mode 100644 index 000000000..94ec32889 Binary files /dev/null and b/Metro/Metro_RP2350_Memory/tictactoe_demo/mouse_cursor.bmp differ diff --git a/Metro/Metro_RP2350_Memory/tictactoe_demo/tictactoe_spritesheet.bmp b/Metro/Metro_RP2350_Memory/tictactoe_demo/tictactoe_spritesheet.bmp new file mode 100644 index 000000000..a210c9ab2 Binary files /dev/null and b/Metro/Metro_RP2350_Memory/tictactoe_demo/tictactoe_spritesheet.bmp differ