# Ink pong?

import math, random, sys

import pyx
from pyx import *
from pyx import bbox

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

# CreateSpace.com Book sizes
BookSizes = [
  (5.25, 8.0),
  (5.5, 8.5),
  (6.0, 9.0),
  (7.0, 10.0),
]

(w, h) = BookSizes[0]

BookSize = document.paperformat(w * unit.t_inch, h * unit.t_inch)

# Scale effective area for the tennis top
tw = 7.0
th = (tw / w) * h + 1.5

td = 2 * int(tw - 0) - 1

import md5
mymd5 = md5.new(file("paperpong.py").read()).hexdigest()

# Set of parameters a page has to deal with
class Info:
  def __init__(self, p1, sx, sy, ex, ey, cx):
    self.p1 = p1
    (self.sx, self.sy) = (sx, sy)
    (self.ex, self.ey) = (ex, ey)
    self.cx = cx
    
    m = (0.0 + ey - sy) / (ex - sx)
    b = sy - m * sx
    cy = m * cx + b
    self.cy = int(cy)

    # Field space ball x and y (1.5 to th-1.5, 0.5 to tw - 0.5)
    self.bx = cx * ((th - 3.0) / 3) + 1.5
    self.by =  cy * ((tw - 0.0) / td)

    # Scalled coords
    ssx = sx * ((th - 3.0) / 3) + 1.5
    ssy = sy * ((tw - 1.0) / td) + 0.5
    sex = ex * ((th - 3.0) / 3) + 1.5
    sey = ey * ((tw - 1.0) / td) + 0.5

    dx = ssx - sex
    dy = ssy - sey
    self.d = math.sqrt(dy ** 2 + dx ** 2)

    try:
      self.sin = dy / self.d
      self.cos = dx / self.d
    except:
      self.sin = 1
      self.cos = 0

  # This has some interesting consequences...
  def __cmp__(self, other):
    return cmp(str(self), str(other))

  # Should be cached... Would take less time to do than write this comment...
  def __hash__(self):
    return (self.p1 << 28) ^ (self.sx << 23) ^ (self.sy << 18) ^ (self.ex << 11) ^ (self.ey << 4) ^ (self.cx)
    
  def __str__(self):
    return "[p1: %s, start: (%s, %s), end: (%s, %s), current: (%s, %s), field: (%0.2f, %0.2f)]" % (self.p1, self.sx, self.sy, self.ex, self.ey, self.cx, self.cy, self.bx, self.by)
  
  def __repr__(self):
    return str(self)
    
curpageno = 1

def DrawTableTop(c, justFrame = False):

  attrs = [color.rgb.black]
  if justFrame: attrs = [color.gray(0.5)]
  
  ###########################
  # Table border
  r = path.rect(0.0, 0.0, th, tw)
  c.stroke(r, attrs)

  if justFrame: return
  ###########################
  # Mid-line
  divs = 11
  s = tw / divs
  for i in range(0, divs):
    if (i % 2): continue
    r = path.rect(th / 2.0 - 0.025, i * s, 0.05, s)
    c.draw(r, [deco.filled(attrs)])

def DrawPageNo(c):
  global curpageno
  pageno = curpageno
  curpageno += 1

  # Top or bottom of page?
  y = tw + 0.35
  if pageno % 2: y = -0.55

  c.text(0, y, str(pageno), [text.size.normalsize, color.rgb.black, text.parbox(th), text.halign.flushcenter])

  return pageno

def DrawGameOver():
  c = canvas.canvas()
  pgno = DrawPageNo(c)
  DrawTableTop(c, True)

  # Use a non-TeX font for drawing Game Over.
  img = Image.new("RGB", (3100, 400), (255, 255, 255))
  draw = ImageDraw.Draw(img)
  
  font = ImageFont.truetype("font.ttf", 350)
  
  width = draw.textsize("G A M E   O V E R", font = font)[0]
  draw.text(((3100 - width) // 2, 0), "G A M E   O V E R", font = font, fill = (0, 0, 0))
  
  bi = bitmap.image(3100, 400, "RGB", flatten(img))
  bm = bitmap.bitmap((th - 6) / 2.0, 4.7, bi, width = 6.0)
  
  c.insert(bm)

  c.text(3.6, 2.5, "To play again, go to page 1", [text.size.normalsize, color.rgb.black])
  c.text(3.6, 2.0, "To quit, close this book", [text.size.normalsize, color.rgb.black])

  return MakePage(c, pgno)

def DrawAbout():
  c = canvas.canvas()
  pgno = DrawPageNo(c)
  c = canvas.canvas(texrunner = pyx.text.texrunner(mode = 'latex'))
  DrawTableTop(c, True)

  img = Image.open("assets/me.png").convert("RGB")
  
  bi = bitmap.image(900, 900, "RGB", flatten(img))
  bm = bitmap.bitmap(0, 4, bi, width = 3.0)
  
  c.insert(bm)

  c.text(3.5, 6.5, "\\textbf{About the Author...}", [text.size.large, color.rgb.black])
  c.text(3.5, 6.0, """
    \\texttt{paperpong.py} --- 538~lines~of~code --- 
    CLN@62 --- 
    takes~up~16355~bytes ---
    interpreted~by~Python~2.5 ---
    typed~in~nano --- 
    run~on~iMac --- 
    makes~184~pages --- 
    runs~in~19.369s ---
    \\_\\_name\\_\\_~==~'\\_\\_main\\_\\_' --- 
    contains~2408~words ---
    lives~at~inode:~4081503 ---
    ISBN:~978-1438200279 --- 
    preserved~in~P4 ---
    MD5:~%s ---
    generates~\\texttt{paperpong.pdf} ---
    eats~nulls~for~breakfast ---
    enjoys~being~refactored~and~long~walks~along~the~beach
    """ % mymd5, [text.size.tiny, color.rgb.black, text.parbox(th - 4), text.halign.flushleft])

  c.text(3.5, 4.0, "\\textbf{About the Author's Author...}", [text.size.large, color.rgb.black])

  about = [
    'Richard Moore', 'RicMoo', "5'11\"", 'a Python fanatic',
    'is weird', 'Canadian', 'in Seattle', 'Math/Computer Science Major',
    'University of Waterloo', 'Rooster', 'Gemini',
    'loves chocolate', 'enjoys making lists', 'Idiocracy',
    'Fraggles', 'me@ricmoo.com', 'listening to Chemical Brothers',
    'Xkcd is life is Xkcd...', 'misses Inca Kola',
    'all hail Turing, Hopper and Knuth', 'drinks milk',
    'Don Hertzfeldt will be king', 'wrote pac-txt', 'calla lily',
    'pwn3d j00 at foosball', 'ate a guinea pig', 'camps',
    'is being watched by tofu cubes', "can't spell", "don't taze me bro",
    'swims', 'Yells out "Hi Mom, Dad, Grandma and Grandpa!"',
    'misses his Boo and Slippers', 'is hungry', 'should eat',
    'needs groceries', 'lives The Office', 'based on Zim',
    'thanks all his family and friends', 'snowboards',
    'enjoys free samples', 'blue', 'day dreams too much', 'nerdy',
    'sings in the shower', "can't sing", 'wears Puma',
    'was an emoticon in a previous life', "solves Rubik's Cubes",
    'jumps off bridges', 'will teach turtles voodoo', 'climbs trees',
    'would die without Wikipedia',
    'remembers Tawny, Sammy, Richie, Matty and Misty', 'codes',
    'rides the bus', 'will invade Europa', 'takes pictures',
    'raised in a small town', 'uses Listerine',
    'thanks you for{\\it reading} his book'
  ]
  c.text(0.5, 3.5, " --- ".join(s.replace(" ", "~") for s in about), [text.size.tiny, color.rgb.black, text.parbox(th - 1), text.halign.flushleft])

  c.text(0.5, 0.4, "\\textbf{Warning:} Don't lick a porcupine the wrong way.", [text.size.tiny, color.rgb.black, text.parbox(th - 1), text.halign.flushleft])

  return MakePage(c, pgno)

def MakePage(c = None, pgno = None):

  if c is None: c = canvas.canvas()

  # Extra margins for inside fold
  (odd, even) = (0, 0)
  if pgno is not None:
    (odd, even) = (0, 0.8)
    if pgno % 2: (odd, even) = (even, odd)

  return document.page(c, paperformat = BookSize, rotated = True, fittosize = True, margin = 0.2, bbox = bbox.bbox(-1, -1 - even, th + 1, tw + 1 + odd))

def DrawPage(info):
  c = canvas.canvas()
  DrawTableTop(c)
  pgno = DrawPageNo(c)
 
  ###########################
  # Ball Artifact
  
  def shadow(d, r, t, rx = 1, dy = 0):
    a = path.circle(d * info.cos * rx + info.bx, d * info.sin + info.by + dy, r)
    c.draw(a, [deco.filled([color.rgb.black, color.transparency(t)])])

  # Offsets for the specular highlight  
  shx = shy = 0
    
  # Shortened tail (to prevent ocluding text)
  if (info.cx == 0 and info.ex > 0) or (info.cx == 3 and info.ex < 3):
    shadow(0.29, 0.175, 0.9)
    shadow(0.17, 0.18, 0.8)
    shadow(0.1, 0.187, 0.7)
    shadow(0, 0.2, 0)
  
  else:
  
    # Hit the side walls
    if info.cy in (0, td):
      dy = -0.2
      if info.cy == 0: dy = 0.2
      shadow(0.5, 0.18, 0.9, 1, dy)
      shadow(0.25, 0.185, 0.8, 1, dy)
      shadow(0, 0.19, 0.7, 1, dy)

      # Calculate the next target temporarily
      (ttx, tty) = paths[(NONE, info.ex, info.ey)]
      tmp = Info(info.p1, ttx, tty, info.ex, info.ey, info.cx)

      # Bounce towards that
      (d, r, rx) = (0.25, 0.2, 1)
      a = path.circle(d * tmp.cos * rx + tmp.bx, d * tmp.sin + tmp.by + dy, r)
      c.draw(a, [deco.filled([color.rgb.black])])

      # Shifted specular higlight
      shx = 0.25 * tmp.cos * 1
      shy = 0.25 * tmp.sin + dy
      
    # Normal
    else:
      shadow(0.75, 0.18, 0.9)
      shadow(0.5, 0.185, 0.8)
      shadow(0.25, 0.19, 0.7)
      shadow(0, 0.2, 0)

  # Specular highlight
  a = path.circle(shx + info.bx - 0.065, shy + info.by + 0.065, 0.05)
  c.draw(a, [deco.filled([color.rgb.white])])  

  ###########################
  # P1 Artifact
  a = path.rect(0, info.p1 / 2.0, 0.2, 1)
  c.stroke(a, [deco.filled([color.rgb.black])])
  
  # Up Arrow option
  if info.p1 < td - 1:
    a = path.line(0.5, info.p1 / 2.0 + 0.55, 0.5, info.p1 / 2.0 + 1.05)
    c.stroke(a, [deco.earrow.large, color.gray(0.5), style.linewidth.Thick])
    txt = GetPage(0, info)
    c.text(0.15, info.p1 / 2.0 + 1.2, txt, [text.size.scriptsize, color.gray(0.5)])

  # Down Arrow option
  if info.p1 > 0:
    a = path.line(0.5, info.p1 / 2.0 + 0.45, 0.5, info.p1 / 2.0 - 0.05)
    c.stroke(a, [deco.earrow.large, color.gray(0.5), style.linewidth.Thick])
    txt = GetPage(1, info)
    c.text(0.15, info.p1 / 2.0 - 0.3, txt, [text.size.scriptsize, color.gray(0.5)])

  ###########################
  # P2 Artifact
  p2y = info.cy
  if p2y > td - 1: p2y = td - 1
  if p2y < 0: p2y = 0
  a = path.rect(th - 0.2, p2y / 2.0, 0.2, 1)
  c.stroke(a, [deco.filled([color.rgb.black])])
  
  # Debug... Very useful! Don't remove this line!
  #c.text(2, 1, str(info), [text.size.scriptsize, color.rgb.black])  
  
  # Build the page from the canvas  
  return MakePage(c, pgno)

# The pyx library doesn't like PIL tuple lists
def flatten(img):
  ret = ""
  for (r, g, b) in img.getdata():
    ret += chr(r) + chr(g) + chr(b)
  return ret
    
def DrawTitlePage(author = True):
  c = canvas.canvas()

  # Use a nicer font than TeX has
  img = Image.new("RGB", (2000, 400), (255, 255, 255))
  draw = ImageDraw.Draw(img)
  
  font = ImageFont.truetype("font.ttf", 350)
  
  width = draw.textsize("Paper P  ng", font = font)[0]
  draw.text(((2000 - width) // 2, 0), "Paper P  ng", font = font, fill = (0, 0, 0))
  
  bi = bitmap.image(2000, 400, "RGB", flatten(img))
  bm = bitmap.bitmap((th - 4) / 2.0, 5, bi, width = 4.0)
  
  c.insert(bm)

  # Now insert a ball for the "o" in Pong.
  info = Info(3, 4, 12, 0, -10, 1)
  dx = 2.405
  dy = 7.78
    
  def shadow(d, r, t):
    a = path.circle(dx + d * info.cos + info.bx, dy + d * info.sin + info.by, r * 0.95)
    c.draw(a, [deco.filled([color.rgb.black, color.transparency(t)])])
  
  shadow(0.75, 0.18, 0.9)
  shadow(0.5, 0.185, 0.8)
  shadow(0.25, 0.19, 0.7)
  shadow(0, 0.2, 0)

  # Specular highlight
  a = path.circle(dx + info.bx - 0.065, dy + info.by + 0.065, 0.05)
  c.draw(a, [deco.filled([color.rgb.white])])  

  # Include me! Highlighting RicMoo (or rather, un-highlighting hardre...)
  if author:
    img = Image.new("RGB", (2100, 400), (255, 255, 255))
    draw = ImageDraw.Draw(img)

    font = ImageFont.truetype("font.ttf", 280)

    width = draw.textsize("Richard Moore", font = font)[0]
  
    draw.text(((2100 - width) // 2, 0), "Ric", font = font, fill = (0, 0, 0))
    dw = draw.textsize("Ric", font = font)[0]

    draw.text(((2100 - width) // 2 + dw, 0), "hard", font = font, fill = (125, 125, 125))
    dw = draw.textsize("Richard ", font = font)[0]

    draw.text(((2100 - width) // 2 + dw, 0), "Moo", font = font, fill = (0, 0, 0))
    dw = draw.textsize("Ricahrd Moo", font = font)[0]

    draw.text(((2100 - width) // 2 + dw, 0), "re", font = font, fill = (125, 125, 125))
  
    bi = bitmap.image(2100, 400, "RGB", flatten(img))
    bm = bitmap.bitmap((th - 4) / 2.0, 2, bi, width = 4.0)
  
    c.insert(bm)

  return MakePage(c, None)

def DrawCopyrightPage():
  c = canvas.canvas(texrunner = pyx.text.texrunner(mode = 'latex'))

  c.text(0, 7.4, "\\textbf{Creative Commons: Attribution-Noncommercial 3.0}", [text.size.small, color.rgb.black, text.parbox(th), text.halign.flushcenter])

  c.text(0, 6.6, """
\\textbf{You are free:}
\\begin{itemize}
  \\item\\textbf{to Share} -- to copy, distribute and transmit the work
  \\item\\textbf{to Remix} -- to adapt the work
\\end{itemize}
\\textbf{Under the following conditions:}
\\begin{itemize}
  \\item\\textbf{Attribution.} You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).
  \\item\\textbf{Noncommercial.} You may not use this work for commercial purposes.
\\end{itemize}
""", [text.size.small, color.rgb.black, text.parbox(th), text.halign.flushleft])

  c.text(0, 2, "\\copyright 2008, Richard Moore, Some Rights Reserved. For a full legal description of this license, see \\texttt{http://www.paperconsole.com/licenses/by-nc-3/}", [text.size.small, color.rgb.black, text.parbox(th), text.halign.flushleft])  

  return MakePage(c)

NONE = 0
TOP = 1
BOTTOM = 2

# Ball path from (3, 6) to (-1, 11) with P1 at y = 8 and ball in x = 2
init = Info(8, 3, 6, -1, 11, 2)

# These tuples map:
#   (part of hit paddle, ball x, ball y) => (new target x, new target y)
#   "Part of hit paddle" is only relevent for P1 hits. Otherwise None.
paths = {
  (TOP, 0, 11): (1, 13),        # a
  (NONE, 1, 13): (4, 9),        # b
  
  (BOTTOM, 0, 11): (4, 9),      # c

  (NONE, 3, 12): (1, 0),        # d
  (NONE, 3, 9): (-1, 6),        # e
  
  (NONE, 3, 6): (-1, 11),       # f
  
  (TOP, 0, 6): (4, 12),         # g
  (BOTTOM, 0, 6): (4, 3),       # h
  
  (TOP, 0, 3): (4, 6),          # i
  (NONE, 3, 4): (-1, 6),        # j
  
  (BOTTOM, 0, 3): (4, 4),       # k
  
  (NONE, 1, 0): (-1, 3),        # l
  
  (NONE, 2, 0): (-1, 3),        # m
  (NONE, 3, 3): (2, 0),         # n
}

GAME_OVER = object()

# Generate all sets of parameters we need to draw the book, and calculate
# what set of parameters the up and down choice for each will lead.
infos = { }
sub = { }
def GenInfos(info):
  if info in infos: return
  
  # If we are at a padding, heading toward the paddle OR we hit a side wall...
  if (info.cx == 0 and info.ex == -1) or (info.cx == 3 and info.ex == 4) or (info.cx == info.ex):

    # Where the ball "would" theoretically hit
    hit = (info.cy + info.ey) // 2
    
    # Hit the top or bottom of the paddle?
    paddle = BOTTOM    
    if info.p1 < hit: paddle = TOP
    
    if info.cx == 3 or info.cx == info.ex: paddle = NONE
    
    # Find our new target
    try:
      tx, ty = paths[(paddle, info.cx, info.ey)]
    except:
      print "Egad Brain!!", info.cx, info.cy
      return

    # Hit a wall. It's only 1 frame unlike a paddle hit, so continue along
    if info.cx == info.ex:  
      if info.ex > info.sx: 
        ncx = info.cx + 1
      if info.ex < info.sx: 
        ncx = info.cx - 1

      up = Info(info.p1 + 1, info.ex, info.ey, tx, ty, ncx)
      down = Info(info.p1 - 1, info.ex, info.ey, tx, ty, ncx)

    # Send it back from whence it came...
    else:
      up = Info(info.p1 + 1, info.ex, info.ey, tx, ty, info.cx)
      down = Info(info.p1 - 1, info.ex, info.ey, tx, ty, info.cx)

    # It's up to P1. Determine if the game would end
    if info.cx == 0:
      hit = (info.cy + info.ey) // 2

      if not (hit - 1 <= info.p1 + 1 <= hit + 2): up = GAME_OVER
      if not (hit - 1 <= info.p1 - 1 <= hit + 2): down = GAME_OVER
  
  # Keep on truckin'      
  else:
    if info.ex > info.sx: 
      ncx = info.cx + 1
    if info.ex < info.sx: 
      ncx = info.cx - 1

    up = Info(info.p1 + 1, info.sx, info.sy, info.ex, info.ey, ncx)
    down = Info(info.p1 - 1, info.sx, info.sy, info.ex, info.ey, ncx)
  
  infos[info] = (up, down)

  # Calculate our ups and downs...
  if up != GAME_OVER and info.p1 < td - 1: GenInfos(up)
  if down != GAME_OVER and info.p1 > 0: GenInfos(down)

# Bootstap recursion
GenInfos(init)

# order the pages randomly
infoidx = list(set(infos))
infoidx.append(GAME_OVER)

random.seed(1337)
random.shuffle(infoidx)

# Make sure the first page stays page 1 though
infoidx.remove(init)
infoidx.insert(0, init)

# Front matter
pages = [ DrawTitlePage(False), MakePage(), DrawTitlePage(), DrawCopyrightPage() ]

# Get the page number for a move
def GetPage(dir, info):
  try:
    if infos[info][dir] == GAME_OVER: return "page " + str(infoidx.index(GAME_OVER) + 1)
    return "page " + str(infoidx.index(infos[info][dir]) + 1)
  except Exception, e:
    print "Gah!?", e
    return "Gah!?"

# Draw all the pages
for info in infoidx:
  if info == GAME_OVER:
    pages.append(DrawGameOver())
  else:
    pages.append(DrawPage(info))

pages.append(DrawAbout())

# Make the PDF
d = document.document(pages)
d.writePDFfile("paperpong", title = "Paper Pong", author = "Richard Moore")


