Published 2019-06-18 15 min read
A while ago I learned to draw circles using Bresenham’s algorithm (thanks CPEN 311). I also stumbled upon Python’s curses library which enables interactive GUI in the command line. So I decided to make an animated circle that runs in the command line. And then code golf it.
Curses is a built-in library to Python 3. In a nutshell, we can “draw” shapes and text on the command line/terminal window. I won’t go too much detail into how to write program using curses.
There are multiple ways to instantiate the window object in curses. I will use the wrapper which handles the lifetime of the screen, and we will put everything we want to do in the main
function:
import curses
def main(screen):
"""screen will be passed by the curses wrapper caller"""
# do our stuff
# entry point
if __name__ == '__main__': curses.wrapper(main)
This is our bare minimum code. Here are some useful functions that I will use:
screen.addstr(row, col, str)
. Note that we specify the coordinates with row (y coords) first.addstr
calls, we need to call screen.refresh()
to apply and render it onto the terminal screen.screen.clear()
.screen.getch()
and it will return the key code.screen.nodelay(True)
to tell it not to wait for user input.screen.getmaxyx()
. Also note that the values are ordered Y first, then X.curses.curs_set(0)
.I won’t go over specifics of how to draw a circle using Bresenham’s algorithm but you can find it here on GeeksforGeeks. A simple implementation of the algorithm, in combination of the curses functions would be like this:
def circle(screen, x_center, y_center, radius):
def draw_piece(screen, x_center, y_center, x, y):
x, y = int(x), int(y)
screen.addstr(y_center + y, x_center + x, '*')
screen.addstr(y_center + y, x_center - x, '*')
screen.addstr(y_center - y, x_center + x, '*')
screen.addstr(y_center - y, x_center - x, '*')
screen.addstr(y_center + x, x_center + y, '*')
screen.addstr(y_center + x, x_center - y, '*')
screen.addstr(y_center - x, x_center + y, '*')
screen.addstr(y_center - x, x_center - y, '*')
x = 0
y = radius
d = 3 - 2 * radius
draw_piece(screen, x, y)
while y >= x:
x += 1
if d > 0:
y -= 1
d += 4 * (x - y) + 10
else:
d += 4 * x + 6
draw_piece(screen, x, y)
screen.refresh()
Cool, now let’s use the circle()
function in main()
and animate it by looping:
def main(screen):
frame_count = 0
# set up curses
curses.curs_set(0)
screen.nodelay(True)
# infinite loop
while True:
# get user key input
key == screen.getch()
# if q is pressed, quit
if key == ord('q'):
break
# clear
screen.clear()
# get geometry and draw circle at the center of the screen
# the radius of the circle is sinusoidal based on frame count
height, width = screen.getmaxyx()
radius = 10 * (math.sin(frame_count * 0.05) + 1) + 5
circle(screen, width // 2, height // 2, radius)
# increment frame count
frame_count += 1
When we run this in Python, we get:
To quit, simply press q
.
Code golf is a silly thing where we want to make the shortest possible code at the cost of readability and efficiency. Note that I’m by no means good at code golf (probably because I suck at programming in general) so feedback is welcome.
Let’s first look at the function circle
and draw_piece
. There is a lot of repetitive code, so let’s turn the permutations of different coordinates into a list. Then iterate through the list and call addstr
into a loop.
def draw_piece(screen, x, y):
x, y = int(x), int(y)
a, b = [y, y, -y, -y, x, x, -x, -x], [x, -x, x, -x, y, -y, y, -y]
for p in zip(a, b):
screen.addstr(y_center + p[0], x_center + p[1])
Since the lists a
and b
are only used once, there is no need to put them in a variable.
def draw_piece(screen, x, y):
x, y = int(x), int(y)
- a, b = [y, y, -y, -y, x, x, -x, -x], [x, -x, x, -x, y, -y, y, -y]
+ for p in zip([y, y, -y, -y, x, x, -x, -x], [x, -x, x, -x, y, -y, y, -y]):
screen.addstr(y_center + p[0], x_center + p[1])
Let’s also remove all the white space, and use one character for variable and function names.
-def draw_piece(screen, x, y):
+def d(s, x, y):
x, y = int(x), int(y)
for p in zip([y, y, -y, -y, x, x, -x, -x], [x, -x, x, -x, y, -y, y, -y]):
- screen.addstr(y_center + p[0], x_center + p[1])
+ s.addstr(u + p[0], v + p[1])
Remove all whitespace and use a single space for indentation to use less characters.
def d(s,x,y):
x,y=int(x),int(y)
for p in zip([y,y,-y,-y,x,x,-x,-x],[x,-x,x,-x,y,-y,y,-y]):s.addstr(u+p[0],v+p[1])
The variables y_center
and x_center
are located in the outer scope. We can move the getmaxyx()
call from main
to here so that we’re not passing the values all the way down. But this means we are calling getmaxyx()
eight times per frame. Oh well, speed is not the objective here. It will make the d()
function longer, but it will make the overall code smaller.
# note that we're not passing center x and y to c() anymore
def c(s,r):
h,w=s.getmaxyx()
def d(s,x,y):
x,y=int(x),int(y)
for p in zip([y,y,-y,-y,x,x,-x,-x],[x,-x,x,-x,y,-y,y,-y]): s.addstr(h//2+p[0],w//2+p[1])
# ...
Removing white space in the c()
:
def c(s,r):
# ...
- x = 0
- y = radius
- d = 3 - 2 * radius
+ x,r,d=0,r,3-2*r
h,w=s.getmaxyx()
- draw_piece(screen, x, y)
+ d(s,x,y)
while y >= x:
x += 1
if d > 0:
- y -= 1
- d += 4 * (x - y) + 10
+ y,d=y-1,d+4*(x-y)+10
else:
d += 4 * x + 6
- draw_piece(screen, x, y)
+ d(s,x,y)
- screen.refresh()
+ s.refresh()
Now we remove whitespace and use single character indentation, and we finally get:
def c(s,r):
# ...
x,r,d,h,w=0,r,3-2*r,*s.getmaxyx() # using '*' to unroll
d(s,x,y)
while y>=x:
x+=1
if d>0:y,d=y-1,d+4*(x-y)+10
else:d+=4*x+6
d(s,x,y)
s.refresh()
It turns out that it makes no difference if we use y>x
and discard the first d(s,x,y)
call, so let’s remove those. That means we are only calling the function d()
once, so let’s just get rid of the function. The result is:
def c(s,r):
s.clear()
x,y,d,h,w=0,r,3-2*r,*s.getmaxyx()
while x<y:
a,b,x=int(x),int(y),x+1
for p in zip([b,b,-b,-b,a,a,-a,-a],[a,-a,a,-a,b,-b,b,-b]):s.addstr(h//2+p[0],w//2+p[1],'*')
if d>0:y,d=y-1,d+4*(x-y)+10
else:d+=4*x+6
s.refresh()
Now let’s optimize the main function:
# BEFORE
def main(screen):
frameCount = 0
curses.curs_set(0)
screen.nodelay(True)
while True:
key = screen.getch()
if key == ord('q'):
break
screen.clear()
height, width = screen.getmaxyx()
radius = 10 * (math.sin(frameCount * 0.05) + 1) + 5
circle(screen, width // 2, height // 2, radius)
frameCount += 1
# AFTER
def m(s):
s.nodelay(1)
i=1
while 1:
if s.getch()==49:break
c(s,9*math.sin(i)+15)
i+=.1
Changes:
1
instead of True
to save 3 characters.curs_set
because cursor is not that big of a deal.frameCount
.ord()
. But now it means we press 1
to quit instead of q
.getmaxyx
call because that’s done in the circle function already.Finally, we need to make sure:
Here is the final code:
import curses,math
def c(s,r):
s.clear()
x,y,d,h,w=0,r,3-2*r,*s.getmaxyx()
while x<y:
a,b,x=int(x),int(y),x+1
for p in zip([b,b,-b,-b,a,a,-a,-a],[a,-a,a,-a,b,-b,b,-b]):s.addstr(h//2+p[0],w//2+p[1],'*')
if d>0:y,d=y-1,d+4*(x-y)+10
else:d+=4*x+6
s.refresh()
def m(s):
s.nodelay(1)
i=1
while 1:
if s.getch()==49:break
c(s,9*math.sin(i)+15)
i+=.1
curses.wrapper(m)
Total size: 381 bytes down from 1182 bytes in the un-golf’d version. Link to the code can be found here.