PROJECTS

Curses, Circle, and Golf

Published 2019-06-18

15 min read

#Programming #Project #Python #Curses #Code golf

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

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:

Circle

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:

animated circle

To quit, simply press q.

Golf

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:

Finally, we need to make sure:

Result

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.