Etching Drawer
When you move a pen point around the screen with the WASD keys, the etching drawer forms a picture by tracing a continuous line, like the Etch A Sketch toy. Let your artistic side break out and see what images you can create! This program also lets you save your drawings to a text file so you can print them out later. Plus, you can copy and paste the WASD movements of other drawings into this program, like the WASD commands for the Hilbert Curve fractal presented on lines 6 to 14 of the source code.
etching_drawer.py
1"""Etching Drawer, by Al Sweigart al@inventwithpython.com
2An art program that draws a continuous line around the screen using the
3WASD keys. Inspired by Etch A Sketch toys.
4
5For example, you can draw Hilbert Curve fractal with:
6SDWDDSASDSAAWASSDSASSDWDSDWWAWDDDSASSDWDSDWWAWDWWASAAWDWAWDDSDW
7
8Or an even larger Hilbert Curve fractal with:
9DDSAASSDDWDDSDDWWAAWDDDDSDDWDDDDSAASDDSAAAAWAASSSDDWDDDDSAASDDSAAAAWA
10ASAAAAWDDWWAASAAWAASSDDSAASSDDWDDDDSAASDDSAAAAWAASSDDSAASSDDWDDSDDWWA
11AWDDDDDDSAASSDDWDDSDDWWAAWDDWWAASAAAAWDDWAAWDDDDSDDWDDSDDWDDDDSAASDDS
12AAAAWAASSDDSAASSDDWDDSDDWWAAWDDDDDDSAASSDDWDDSDDWWAAWDDWWAASAAAAWDDWA
13AWDDDDSDDWWAAWDDWWAASAAWAASSDDSAAAAWAASAAAAWDDWAAWDDDDSDDWWWAASAAAAWD
14DWAAWDDDDSDDWDDDDSAASSDDWDDSDDWWAAWDD
15
16This code is available at https://nostarch.com/big-book-small-python-programming
17Tags: large, artistic"""
18
19import shutil, sys
20
21# Set up the constants for line characters:
22UP_DOWN_CHAR = chr(9474) # Character 9474 is '│'
23LEFT_RIGHT_CHAR = chr(9472) # Character 9472 is '─'
24DOWN_RIGHT_CHAR = chr(9484) # Character 9484 is '┌'
25DOWN_LEFT_CHAR = chr(9488) # Character 9488 is '┐'
26UP_RIGHT_CHAR = chr(9492) # Character 9492 is '└'
27UP_LEFT_CHAR = chr(9496) # Character 9496 is '┘'
28UP_DOWN_RIGHT_CHAR = chr(9500) # Character 9500 is '├'
29UP_DOWN_LEFT_CHAR = chr(9508) # Character 9508 is '┤'
30DOWN_LEFT_RIGHT_CHAR = chr(9516) # Character 9516 is '┬'
31UP_LEFT_RIGHT_CHAR = chr(9524) # Character 9524 is '┴'
32CROSS_CHAR = chr(9532) # Character 9532 is '┼'
33# A list of chr() codes is at https://inventwithpython.com/chr
34
35# Get the size of the terminal window:
36CANVAS_WIDTH, CANVAS_HEIGHT = shutil.get_terminal_size()
37# We can't print to the last column on Windows without it adding a
38# newline automatically, so reduce the width by one:
39CANVAS_WIDTH -= 1
40# Leave room at the bottom few rows for the command info lines.
41CANVAS_HEIGHT -= 5
42
43"""The keys for canvas will be (x, y) integer tuples for the coordinate,
44and the value is a set of letters W, A, S, D that tell what kind of line
45should be drawn."""
46canvas = {}
47cursorX = 0
48cursorY = 0
49
50
51def getCanvasString(canvasData, cx, cy):
52 """Returns a multiline string of the line drawn in canvasData."""
53 canvasStr = ''
54
55 """canvasData is a dictionary with (x, y) tuple keys and values that
56 are sets of 'W', 'A', 'S', and/or 'D' strings to show which
57 directions the lines are drawn at each xy point."""
58 for rowNum in range(CANVAS_HEIGHT):
59 for columnNum in range(CANVAS_WIDTH):
60 if columnNum == cx and rowNum == cy:
61 canvasStr += '#'
62 continue
63
64 # Add the line character for this point to canvasStr.
65 cell = canvasData.get((columnNum, rowNum))
66 if cell in (set(['W', 'S']), set(['W']), set(['S'])):
67 canvasStr += UP_DOWN_CHAR
68 elif cell in (set(['A', 'D']), set(['A']), set(['D'])):
69 canvasStr += LEFT_RIGHT_CHAR
70 elif cell == set(['S', 'D']):
71 canvasStr += DOWN_RIGHT_CHAR
72 elif cell == set(['A', 'S']):
73 canvasStr += DOWN_LEFT_CHAR
74 elif cell == set(['W', 'D']):
75 canvasStr += UP_RIGHT_CHAR
76 elif cell == set(['W', 'A']):
77 canvasStr += UP_LEFT_CHAR
78 elif cell == set(['W', 'S', 'D']):
79 canvasStr += UP_DOWN_RIGHT_CHAR
80 elif cell == set(['W', 'S', 'A']):
81 canvasStr += UP_DOWN_LEFT_CHAR
82 elif cell == set(['A', 'S', 'D']):
83 canvasStr += DOWN_LEFT_RIGHT_CHAR
84 elif cell == set(['W', 'A', 'D']):
85 canvasStr += UP_LEFT_RIGHT_CHAR
86 elif cell == set(['W', 'A', 'S', 'D']):
87 canvasStr += CROSS_CHAR
88 elif cell == None:
89 canvasStr += ' '
90 canvasStr += '\n' # Add a newline at the end of each row.
91 return canvasStr
92
93
94moves = []
95while True: # Main program loop.
96 # Draw the lines based on the data in canvas:
97 print(getCanvasString(canvas, cursorX, cursorY))
98
99 print('WASD keys to move, H for help, C to clear, '
100 + 'F to save, or QUIT.')
101 response = input('> ').upper()
102
103 if response == 'QUIT':
104 print('Thanks for playing!')
105 sys.exit() # Quit the program.
106 elif response == 'H':
107 print('Enter W, A, S, and D characters to move the cursor and')
108 print('draw a line behind it as it moves. For example, ddd')
109 print('draws a line going right and sssdddwwwaaa draws a box.')
110 print()
111 print('You can save your drawing to a text file by entering F.')
112 input('Press Enter to return to the program...')
113 continue
114 elif response == 'C':
115 canvas = {} # Erase the canvas data.
116 moves.append('C') # Record this move.
117 elif response == 'F':
118 # Save the canvas string to a text file:
119 try:
120 print('Enter filename to save to:')
121 filename = input('> ')
122
123 # Make sure the filename ends with .txt:
124 if not filename.endswith('.txt'):
125 filename += '.txt'
126 with open(filename, 'w', encoding='utf-8') as file:
127 file.write(''.join(moves) + '\n')
128 file.write(getCanvasString(canvas, None, None))
129 except:
130 print('ERROR: Could not save file.')
131
132 for command in response:
133 if command not in ('W', 'A', 'S', 'D'):
134 continue # Ignore this letter and continue to the next one.
135 moves.append(command) # Record this move.
136
137 # The first line we add needs to form a full line:
138 if canvas == {}:
139 if command in ('W', 'S'):
140 # Make the first line a horizontal one:
141 canvas[(cursorX, cursorY)] = set(['W', 'S'])
142 elif command in ('A', 'D'):
143 # Make the first line a vertical one:
144 canvas[(cursorX, cursorY)] = set(['A', 'D'])
145
146 # Update x and y:
147 if command == 'W' and cursorY > 0:
148 canvas[(cursorX, cursorY)].add(command)
149 cursorY = cursorY - 1
150 elif command == 'S' and cursorY < CANVAS_HEIGHT - 1:
151 canvas[(cursorX, cursorY)].add(command)
152 cursorY = cursorY + 1
153 elif command == 'A' and cursorX > 0:
154 canvas[(cursorX, cursorY)].add(command)
155 cursorX = cursorX - 1
156 elif command == 'D' and cursorX < CANVAS_WIDTH - 1:
157 canvas[(cursorX, cursorY)].add(command)
158 cursorX = cursorX + 1
159 else:
160 # If the cursor doesn't move because it would have moved off
161 # the edge of the canvas, then don't change the set at
162 # canvas[(cursorX, cursorY)].
163 continue
164
165 # If there's no set for (cursorX, cursorY), add an empty set:
166 if (cursorX, cursorY) not in canvas:
167 canvas[(cursorX, cursorY)] = set()
168
169 # Add the direction string to this xy point's set:
170 if command == 'W':
171 canvas[(cursorX, cursorY)].add('S')
172 elif command == 'S':
173 canvas[(cursorX, cursorY)].add('W')
174 elif command == 'A':
175 canvas[(cursorX, cursorY)].add('D')
176 elif command == 'D':
177 canvas[(cursorX, cursorY)].add('A')