#!/usr/bin/env python # coding: utf-8 # After working through the exercises in this notebook you should be familiar with Python variables, indexing, [for loops][for], and [if statements][if]. # # [for]: http://docs.python.org/2/reference/compound_stmts.html#the-for-statement # [if]: http://docs.python.org/2/reference/compound_stmts.html#the-if-statement # In[3]: from ipythonblocks import BlockGrid, colors, show_color, show_color_triple # [IPython help features](https://jakevdp.github.io/PythonDataScienceHandbook/01.01-help-and-documentation.html) # In[4]: get_ipython().run_line_magic('pinfo', 'BlockGrid') # These blocks with `In[ ]:` and `Out[ ]:` next to them are "cells". Jupyter "echos" whatever is on the last line of a cell. # In[5]: BlockGrid(4, 5) # Use an `=` to assign something (on the right side of the `=`) to the name you want it to have (on the left side of the `=`). # In[6]: grid = BlockGrid(5, 5, fill=colors.RosyBrown) # In[7]: grid # `grid.show()` is another way to get a grid displayed, it's especially useful if you can't echo the grid on the last line of a cell. # # `.show` is a "method" on the `grid` instance. It's like a function attached to this particular grid. Note the parentheses used to "call" the method (and to call the `BlockGrid` above when making `grid`). # In[8]: grid.show() # The `show_color` and `show_color_triple` functions are useful for previewing colors. The `colors` object/dictionary has a bunch of useful preset colors. # In[9]: show_color_triple(colors.Bisque) # In[10]: colors.AliceBlue # When pulling an individual block out of a grid use square brackets and specify a row and column: # `grid[row_index, column_index`. # Python is zero-indexed. # In[11]: block = grid[0, 0] block # `.rgb` is an "attribute" of blocks that has the "red, green, blue" values of the color of the block. # These these integers can have values from 0 - 255 and the combination of the three specifies the color of the block. # In[12]: block.rgb # We can assign a new triplet of color integers to the `.rgb` attribute to change the color of a block. # In[13]: block.rgb = colors.AliceBlue block # We can see further information about a block using the `print` and `type` functions. # In[14]: print(block) # In[15]: type(block) # Note that changing the color of the block variable above also changed the color of the block in the grid. # When we ran `block = grid[0, 0]` above we created a new name (`block`) that referenced the top-left cell # of the grid. We did _not_ make a copy of that block. # In[16]: grid # We could also change the color of the top-left block without first pulling it into a variable. # In[17]: grid[0, 0].rgb = colors.Cyan grid # Note also that assigning the `grid` variable to a new variable (`new_grid` here) there is still _no copy_. Modifying `new_grid` modifies the original `grid` as well because they are different names for the same thing. # In[18]: new_grid = grid new_grid[1, 1].rgb = colors.DarkCyan grid # In[19]: grid[2, 2] = colors.FireBrick grid # The `grid` object doesn't have a `.rgb` attribute. # Python lets you create one by assigning something to it, but nothing happens to the appearance of the grid. # In[20]: grid.rgb = colors.Gold grid # Exercise 2: "Change the color of the block in the third row and fourth column." # In[21]: grid[2, 3].rgb = colors.Black grid # Exercise 3: "Change the color of the block in the lower-right corner of the grid." # In[22]: grid[4, 4].rgb = colors.Teal # In[23]: grid # Python also supports negative indexing, allowing you to index into the ends of containers without knowing # how big they are. # Index `-1` is the last item, index `-2` is the second-to-last item, etc. # In[24]: grid[-1, -1].rgb = colors.Violet grid # If you wanted to show two or more grids in the output of a notebook cell you could use the `.show()` method. # In[25]: grid.show() new_grid.show() # Exercise 4: "Use negative indexing to change the color of the block in the second row # and first column." # In[26]: grid[-4, -5].rgb = colors.Wheat grid # ## Loops # # Python's `for thing in container:` syntax lets you iterate over whatever is in `container`. # But, note that what exactly this means varies depending on what `container` is. # In the case of our grids, each iteration of the loop gives us one of the blocks from the grid. # # As an example we could change the color of every block in the grid using a loop. # In[27]: for block in grid: block.rgb = colors.HotPink grid # Exercise 5: "Use a `for` loop to change the color of every block in your grid." # # The `.animate()` method will show you the state of grid as the loop runs. # (But you'll only see the final state of the grid after the loop if you're reading this later.) # In[28]: for block in grid.animate(): block.rgb = colors.Honeydew # ## Ifs # # `if` statements allow us to only execute code when a given condition is true. # The `==` comparator checks whether two things are equal. # Don't get it confused with the single `=` used for assignment! # In[29]: for block in grid.animate(): if block.row == 2: block.rgb = colors.PapayaWhip # How would I figure out that blocks had a `.row` attribute? # I could use the `?` help feature to read the docstring or use `block.` to see available methods # and attributes. # But, note that `block.` only works if block is an existing variable with something assigned to it # when you press tab. That's not the case when writing the loop above. # In[30]: block = grid[0, 0] get_ipython().run_line_magic('pinfo', 'block') # Exercise 6: "Use a `for` loop and an `if` statement to change the color of every block # in the fourth *column* of your grid." # # Blocks also have a `.col` attribute that has the column index of the block. # In[31]: for block in grid.animate(): if block.col == 3: block.rgb = colors.Magenta # `elif` and `else` allow us to write separate blocks of code that run under different conditions. # Only one of these branches will run for each `block` in the grid. # `else` applies to anything not matched by an `if` or `elif` above it. # In[32]: for block in grid.animate(): if block.row == 0: block.rgb = colors.Black elif block.row == 2: block.rgb = colors.White else: block.rgb = colors.Blue # Exercise 7: "Augment your code from Exercise 6 with an `elif` and an `else` to change the # color of all blocks in your grid. But make the second column red, the # third column green, and all the other blocks gold." # In[33]: for block in grid.animate(): if block.col == 1: block.rgb = colors.Red elif block.col == 2: block.rgb = colors.Green else: block.rgb = colors.Gold # Other Python comparators: # # - `<` less than # - `<=` less than or equal # - `>` greater than # - `>=` greater than or equal # - `!=` not equal # `and` and `or` allow us to construct more complicated `if` expressions. # When using `or` only one of the conditions needs to be true for the branch to be executed. # In[34]: for block in grid.animate(): if block.col == 2 or block.col == 4: block.rgb = colors.Khaki # Exercise 8: "Use an `if` with an `or` to change the color of the blocks in the first # and fifth rows to black." # In[35]: for block in grid.animate(): if block.row == 0 or block.row == 4: block.rgb = colors.Black # If we want to make checks on the _color_ of the blocks here are some different ways to do that. # In[36]: block = grid[1, 1] # In[37]: block.rgb # In[38]: block.rgb == (255, 0, 0) # In[39]: block.rgb == colors.Red # In[40]: block.red # In[41]: block.blue # Exercise 9: "Use an `if` with an `and` condition to turn every block that is in the # fourth column AND black to blue." # # When using `and` both of the conditions must be true in order for the branch to be executed. # In[42]: for block in grid.animate(): if block.col == 3 and block.rgb == colors.Black: block.rgb = colors.Blue # You can use parentheses to specify an order of operations for `if` conditions. # Absent parentheses `or` and `and` will be combined left to right. # In[43]: for block in grid.animate(): if (block.col == 1 or block.col == 2) and block.rgb == colors.Black: block.rgb = colors.Blue # ## `range` # # Sometimes we don't need to loop over every block in a grid because we know ahead of time we're # only interested in some subset of them. # The `range` function can help construct a sequence of integer values to use as indices while in the loop. # [`range` documentation](https://docs.python.org/3.8/library/stdtypes.html#range) # # Note: the `stop` argument to range is not included in the result! # In[44]: for index in range(5): print(index) # In[45]: for index in range(5, 8): print(index) # In[46]: for index in range(5, 15, 3): print(index) # Here we want to change the color of every block in the 2nd row, so we loop over a `range` that encompasses # all the columns in the grid. # In[47]: for column_index in range(grid.width): grid[1, column_index].rgb = colors.Orange grid # Exercise 10: "Make a new 20 by 20 block grid. Use a `for` loop with `range` to change the # color of every block in the third, 10th, and 18th rows." # # Note the use of `grid.flash()` to briefly show the grid at each iteration since we're no longer # using `grid.animate()`. We also need to echo `grid` on the last line (or use `grid.show()` # after the loop) in order to see the final result. # In[48]: grid = BlockGrid(20, 20) # In[49]: for column_index in range(grid.width): grid[2, column_index].rgb = colors.YellowGreen grid[9, column_index].rgb = colors.YellowGreen grid[17, column_index].rgb = colors.YellowGreen grid.flash() grid # An alternate way of doing this is to have nested loops, the outer one for the columns we want to change and # the inner one for the rows we want to change. # Note that the `[2, 9, 17]` is a list with the row indices. # (We can't use `range` for the row indices because they aren't evenly spaced.) # In[50]: for column_index in range(grid.width): for row_index in [2, 9, 17]: grid[row_index, column_index].rgb = colors.Indigo grid.flash() grid # Exercise 11: "Use nested `for` loops with `range` to change the color of the bottom-left # quadrant of your 20 by 20 grid." # # Here we use some math instead of hard-coding the row and column indexes in our calls to `range`. # The `//` operator does floor division, returning an integer and discarding any fractional component of the result # (needed because `range` doesn't work with floating point inputs and `/` always returns a float). # In[51]: for row_index in range(grid.height // 2, grid.height): for column_index in range(0, grid.width // 2): grid[row_index, column_index].rgb = colors.LavenderBlush grid # The `int` function will try to convert an input to an integer, it also truncates the input (does not round). # In[52]: int(2.3) # ## Slicing # # Slicing is an abbreviated, inline way of specifying a subset of a Python container. # The syntax of a slice is `start:stop:step`, but not all of those are required. # # In this first example `5:15` means "from the 5th index up to but not including the 15th index". # In[53]: grid[5:15, 5:15] # When the start or stop of a slice is not specified it means "from the beginning" or "until the end", respectively. # In[54]: grid[:5, 15:] # A lone `:` means "include everything along this dimension" (in this case all the columns). # In[55]: grid[:5, :] # We can assign a color to a slice of a grid to change the color of all the blocks in that slice. # In[56]: grid[:5, :] = colors.Crimson grid # Exercise 12: "Try to get the same affects as in Exercises 10 & 11, but using slicing instead # of `for` loops." # In[57]: grid[2, :] = colors.Thistle grid[9, :] = colors.Tomato grid[17, :] = colors.Turquoise grid[-10:, :10] = colors.Blue grid # Slicing also supports specifying a step-size, very similar to `range`. # Here `1::2` means "start at the first index, go through the end, and include every 2nd index". # In[58]: grid[1::2, :] = colors.Teal grid # ## Functions # # Functions are one way to package code and re-use it again and again. # Note the `def` keyword indicating we're writing a function, the name of the function # (here `fahr_to_celsius`), the parentheses, and the list of arguments inside the paretheses. # # The `return` statement is used to pass something back to the code that calls the function. # A function can return multiple things by separating them with commas after the `return`: # `return 1, 2, 3, 4`. # In[59]: def fahr_to_celsius(temp): return ((temp - 32) * (5 / 9)) # In[60]: fahr_to_celsius(63) # In[61]: fahr_to_celsius(90) # Exercise 13: "Write a function that takes a grid as input and returns a new grid with # the colors inverted (e.g. white becomes black, black becomes white, # yellow becomes ???)." # In[65]: def invert_colors(input_grid): """ Returns a new grid the same size as the input grid but with colors "inverted". Colors in our grids are are integers that can have values between 0 - 255 (inclusive). To "invert" a color channel (red, green, or blue) we mirror it within that 0 - 255 range: if it was 0 the output will be 255 and if it was 255 the output will be 0. A value of 123 will invert to 132. """ new_grid = input_grid.copy() for row_index in range(input_grid.height): for col_index in range(input_grid.width): new_grid[row_index, col_index].red = 255 - input_grid[row_index, col_index].red new_grid[row_index, col_index].blue = 255 - input_grid[row_index, col_index].blue new_grid[row_index, col_index].green = 255 - input_grid[row_index, col_index].green return new_grid # In[63]: invert_colors(grid) # Exercise 14: "Write a function that takes a grid and a color as input returns a new grid # that is the input grid with an added border of the given color. # # BONUS: Make the color input argument optional." # # If the user doesn't supply a value for the `border_color` input it will default to `colors.Black`. # In[66]: def add_border(input_grid, border_color=colors.Black): """ Add a two-block border to the input grid. Specify the border color as a tuple of (red, green, blue) integers. """ # increase grid size by 4 to accomodate the two-block border on each side new_width = input_grid.width + 4 new_height = input_grid.height + 4 new_grid = BlockGrid(new_width, new_height, fill=border_color) for block in input_grid: # The +2 here offsets the input grid within the new grid so it ends up with a border new_grid[block.row + 2, block.col + 2].rgb = block.rgb return new_grid # In[67]: add_border(grid, border_color=colors.Cyan) # Exercise 15: Write a function that can follow a list of directions # (`'up'`, `'down'`, `'left'`, `'right'`) along a path, changing the color of # blocks as it goes. # An example function definition: # # ```python # def path_color(grid, path, starting_point, color): # ... # ``` # In[73]: def path_color(input_grid, path, starting_point, color): """ Change the color of all the blocks along a path starting at starting_point. Specify the path as a sequence of 'up', 'down', 'left', 'right' strings. Specify the starting point as (row, column) tuple. Specify the color a (red, green, blue) tuple. """ new_grid = input_grid.copy() # pull the location into separate row/col indices we can update individually current_row = starting_point[0] current_col = starting_point[1] # first, change the color of the block we're at new_grid[current_row, current_col].rgb = color # then move to the new location based on the path for direction in path: if direction == 'up': # one row higher in the grid current_row = current_row - 1 elif direction == 'down': # one row lower current_row = current_row + 1 elif direction == 'left': # one column to the left current_col = current_col - 1 elif direction == 'right': # one column to the right current_col = current_col + 1 # and update the new location's color new_grid[current_row, current_col].rgb = color return new_grid # In[74]: path = ( 'right', 'right', 'down', 'right', 'down', 'down', 'left', 'down', 'down', 'right', 'right', 'right', 'up' ) path_color(grid, path, (3, 4), colors.HotPink) # In[ ]: