We thought it might be tough to make this a fun game on such a limited device so we decided to keep things simple. With only 25 red LEDs it was tough to think how we could show the various elements of the maze. We decided that:
- The player would be a very bright pixel and always in the middle of the screen. The rest of the maze would scroll around you.
- Walls would be dim pixels.
- Doors would be slowly flashing pixels.
- Keys would be rapidly flashing pixels.
- The exit would be flashing in some cool way that made it look enticing.
- Son's first idea was to tap the A button to move left and hold it to move down. Similarly, tapping the B button would move right while holding it would move up. This worked out pretty well and was very accurate but ultimately felt slow and sometimes we just got confused and moved in the wrong direction!
- Next up we tried using the micro:bit's accelerometer as a tilt controller: tilt the unit up, down, left or right to move. We added a delay so that movement didn't repeat too quickly and we set a minimum tilt amount so that you didn't move around when you were holding the device level. This method worked pretty well but it was too easy to accidentally move when you didn't want to. We added a sensitivity setting which let us tweak how far you needed to tilt the micro:bit before movement was triggered but no setting felt right - they were either too sensitive (leading to accidental movement) or not sensitive enough (meaning that you had to tilt so far that you couldn't comfortably see the display).
- Finally we tweaked the second method ever so slightly by adding a button press - you now had to tilt the micro:bit and press the A button to move. This felt much better. Not as satisfying as method 2 but much more accurate.
The code is probably the most complex thing we've written so far. Here it is in full:
""" Tilt Maze Find the exit and escape the maze. Navigate the maze by tilting the micro:bit and pressing the A button. Collect keys to open the doors. Developed by http://giggletronics.blogspot.co.uk/ This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. https://creativecommons.org/licenses/by-nc-sa/4.0/ """ from microbit import * # Map definition. You can change this to change the game. # Key: # w = Wall # s = Start position # k = Key # d = Door # e = Exit # Space = Location that player can walk on map_blocks = 'wwwwwwwwwwwwwwwwww'\ 'w w k kw'\ 'w s d k w'\ 'w wwdwwwwwwwwww'\ 'w k w w'\ 'wwwwwwwwwwwwwwwwdw'\ 'w d w w'\ 'w e w w w'\ 'w w w w'\ 'w w w'\ 'wwwwwwwwwwwwwwwwww' map_width = 18 map_height = 11 # A list of map blocks that should now be seen as clear spaces map_blocks_cleared = [] # Player variables num_keys = 0 # This is how sensitive the input is. Higher numbers mean that you need to tilt the micro:bit # further to move, lower numbers are move likely to trigger movement erroneously SENSITIVITY = 200 # Some custom images BLANK = Image('00000:00000:00000:00000:00000:') KEY = Image('00000:00099:99999:99099:99000:') DOOR0 = Image('99999:99999:99999:99999:99999:') DOOR1 = Image('99099:99099:99099:99099:99099:') DOOR2 = Image('90009:90009:90009:90009:90009:') ALL_DOORS = [DOOR0, DOOR1, DOOR2, BLANK] # Validates that the map is correctly formatted. Halts on error def validate_map(): # Helper function to show an error message and halt def show_error(error): full_error = "MAP ERROR: " + error print(full_error) display.scroll(full_error, loop=True) # The map should contain width * height blocks map_size = len(map_blocks) if map_size != map_width * map_height: show_error('Map size is incorrect') # Count the special map blocks door_count = 0 key_count = 0 start_count = 0 for i in range(map_size): block = map_blocks[i] if block == 'w' or block == ' ' or block == 'e': pass elif block == 'd': door_count = door_count + 1 elif block == 'k': key_count = key_count + 1 elif block == 's': start_count = start_count + 1 else: show_error('Unexpected map block \'' + block + '\' found') # Raise an error if the special block counts are wrong if door_count != key_count: show_error('Door and key counts do not match') if start_count != 1: show_error('Map should contain exactly 1 start point') # Returns the map block at the given position def get_map_block(x, y): # Clamp position map_x = min(max(x, 0), map_width - 1) map_y = min(max(y, 0), map_height - 1) block_number = map_x + map_y * map_width # Is this position still valid? if not block_number in map_blocks_cleared: # Yes, return the block from the map return map_blocks[block_number] else: # No, it's been cleared so return an empty space return ' ' # Returns the start x,y position on the map or 0,0 if it can't be found def find_start(): for y in range(map_height): for x in range(map_width): if get_map_block(x, y) == 's': return x, y return 0, 0 # Draws the current view of the world to the micro:bit display def show_world(player_x, player_y): # Create a buffer to draw the map into screen = BLANK.copy() # Loop over each pixel in the buffer for y in range(5): map_y = player_y + y - 2 for x in range(5): map_x = player_x + x - 2 block = get_map_block(map_x, map_y) if block == 'w': # Wall - Display as a solid pixel screen.set_pixel(x, y, 4) elif block == 'd': # Door - Display as a slowly flashing pixel screen.set_pixel(x, y, 4 if (running_time() % 1000 < 500) else 0) elif block == 'k': # Key - Display as a rapidly flashing pixel screen.set_pixel(x, y, 8 if (running_time() % 200 < 100) else 0) elif block == 'e': # Exit - Display as a super-fast flashing pixel screen.set_pixel(x, y, int(running_time() / 50) % 10) # Overlay the player in the middle screen.set_pixel(2, 2, 9) # Copy to the micro:bit display display.show(screen) # Shows the number of keys held def show_keys(): for i in range(3): display.show([KEY, str(num_keys)]) # Shows an animation for an opening door def show_door_opening(): display.show(ALL_DOORS) # Collects the key at x,y def collect_key_at(x, y): global num_keys global map_blocks_cleared # One more key for the player num_keys = num_keys + 1 # Mark the door block as clear map_blocks_cleared.append(x + y * map_width) # Show the player how many keys they now hold show_keys() # Tries to open the door at x,y def try_to_open_door_at(x, y): global num_keys global map_blocks_cleared if num_keys > 0: # Player has enough keys... take one away num_keys = num_keys - 1 # Mark the door block as clear map_blocks_cleared.append(x + y * map_width) # Show an animation of the door opening show_door_opening() else: # Player does not have enough keys.. show that show_keys() # Check that the map is correct validate_map() # Setup for game by placing the player at the start position player_x, player_y = find_start() # Main loop. We stay in this loop until the player reaches the exit while True: # Try to move when the player presses the A button if button_a.was_pressed(): # Read the accelerometer and use it to calculate a new player position acc_x = accelerometer.get_x() acc_y = accelerometer.get_y() new_x = player_x new_y = player_y if abs(acc_x) > abs(acc_y): # X move is largest if acc_x < -SENSITIVITY: new_x = player_x - 1 elif acc_x > SENSITIVITY: new_x = player_x + 1 else: # Y move is largest if acc_y < -SENSITIVITY: new_y = player_y - 1 elif acc_y > SENSITIVITY: new_y = player_y + 1 # Has the player moved? if new_x != player_x or new_y != player_y: # Yes. Which type of block are they trying to move on to? block = get_map_block(new_x, new_y) if block == 'w': # Wall - Can't move here pass elif block == 'd': # Door - Try to open it try_to_open_door_at(new_x, new_y) # Clear button input button_a.was_pressed() elif block == 'k': # Key - Collect it collect_key_at(new_x, new_y) # Clear button input button_a.was_pressed() elif block == 'e': # Exit - Break out of this function break else: # Space - Move the player here player_x = new_x player_y = new_y # Show the map show_world(player_x, player_y) # Shows a message congratulating the playing on winning show_door_opening() sleep(1000) display.scroll("Congratulations! You have escaped the maze!", loop=True)
There's one interesting thing about this project - we started to hit the limits of the micro:bit memory. We will expand on what this means (and how to scrounge more memory!) in a future post but suffice it to say that, for now, we worked our way around the limits. Probably the most interesting technique we used to save memory is the one that deals with changes to the map. The map is represented as an array of letters (which we call map blocks), so 'w' represents a wall block, 'k' is a key, ' ' is for a space that we can walk on etc.. When the player picks up a key we ned to erase it from the map. Python is great at this and we could have simply made a new copy of the map with the key removed, then swap the maps over. This would work, but requires (at least) twice as much memory - that's too much. The solution is quite neat - we never remove blocks from the map, meaning that we never need to copy the map! Instead we maintain a list (called map_blocks_cleared) of all the blocks that we've "erased". Whenever we try to read from the array of map blocks we first look in this list and if the block we're looking for is in there then we return ' ' instead of what's really there. We wrap all of this functionality up in get_map_block() so that's very easy to query map blocks.
The Python and hex file are here:
tilt_maze.py
tilt_maze.hex
Don't forget that you can just copy the hex file to your device to run it. To play the game tilt the micro:bit in the direction you wish to move and press A. You're looking for the exit (a crazy flashing/strobing pixels) and you'll need to collect keys (rapidly flashing pixels) to open doors (slowly flashing pixels).
Once you've completed the game, how about looking at the source code and figuring out how to make your own maze? Please share your creations in the comments if you do!
No comments:
Post a Comment