#!/usr/bin/python
from ncurses.curses import *
import time, traceback, whrandom, copy, UserList, socket, sys, getopt
import string

class Matrix(UserList.UserList):
    "line oriented two-dimensional matrix"
    # i are lines, j columns
    def __init__(self, imin=0, imax=10, jmin=0, jmax=10):
        self.imin = imin; self.imax = imax
        self.jmin = jmin; self.jmax = jmax

        l = (jmax-jmin+1)*[0] # one line
        nb = []
        for line in range(imax-imin+1):
            nb.append(copy.copy(l))
        self.data = nb


    def __cmp__(self, other):
        if isinstance(other, Matrix):
            return cmp(self.data, other.data)
        else:
            return cmp(self.data, other)

    def __len__(self): return (self.imax-self.imin+1)*(self.jmax-self.jmin+1)

    def __getitem__(self, coor):
        return self.data[coor[0]-self.imin][coor[1]-self.jmin]
        
    def __setitem__(self, coor, item):
        self.data[coor[0]-self.imin][coor[1]-self.jmin] = item
        
    def __delitem__(self, coor):
        self.__setitem__(coor, None)
    
    def __getslice__(self, i, j):
        # not yet
        i = max(i, 0); j = max(j, 0)
        userlist = self.__class__()
        userlist.data[:] = self.data[i:j]
        return userlist
        
        
    def __setslice__(self, i, j, other):
        # not yet
        i = max(i, 0); j = max(j, 0)
        if isinstance(other, UserList):
            self.data[i:j] = other.data
        elif isinstance(other, type(self.data)):
            self.data[i:j] = other
        else:
            self.data[i:j] = list(other)
            
    def __delslice__(self, i, j):
        # not yet
        i = max(i, 0); j = max(j, 0)
        del self.data[i:j]
        
    def getline(self, line):
        return self.data[line-self.imin]

    def setline(self, line, con):
        self.data[line-self.imin] = con

    def addline(self, con=None):
        if con:
            self.data.append(con)
        else:
            self.data.append((self.jmax-self.jmin+1)*[None])
        self.imax = self.imax + 1
        



class Globalvar:

    def __init__(self):
        self.zoom = (1, 2)
        self.colours = 1
        self.inverse = 0
        self.ascii = 1
        self.myport = 5634
        self.otherhost = "localhost"
        self.otherport = 5635
        self.xsize = 25
        self.ysize = 16
        self.pieces = "basic"
        self.nlevel = 3
        self.speedup = 0.985
        self.wait = 1
        self.kpract = kpract
        self.pushing = 1



class Display:

    def __init__(self):

        # initialize curses - be sure to assign stdscr to a variable to prevent it
        # from being immediately garbage collected.
        self.stdscr = initscr()
        if globalvar.colours:
            start_color() # turn on color mode
        cbreak() # provide unbuffered input
        noecho() # turn off input echoing
        nonl() # turn off newline translation
        intrflush(FALSE) # turn off flush-on-interrupt
    
        self.one = WINDOW(0, globalvar.zoom[1]*globalvar.ysize+2, 0, 0)
        self.two = WINDOW(0, globalvar.zoom[1]*globalvar.ysize+2, 0, globalvar.zoom[1]*globalvar.ysize+2)
        self.info = WINDOW(5, 10, 0, globalvar.zoom[1]*globalvar.ysize*2+4)
        self.one.keypad(TRUE) # turn on keypad mode
        init_pair(1, COLOR_YELLOW, COLOR_GREEN)
        init_pair(2, COLOR_CYAN, COLOR_RED)
        init_pair(3, COLOR_RED, COLOR_CYAN)
        init_pair(4, COLOR_GREEN, COLOR_YELLOW)
        init_pair(5, COLOR_BLUE, COLOR_CYAN)
        init_pair(6, COLOR_CYAN, COLOR_MAGENTA)
        init_pair(7, COLOR_BLUE, COLOR_RED)
        self.one.line_border()
        self.two.line_border()
        self.info.line_border()
        self.info.move(1,1)
        self.info.addchstr("Score:")
        self.two.nodelay(1)
        self.one.nodelay(1)
        self.info.noutrefresh()
        self.two.noutrefresh()
        self.one.noutrefresh() # copy window to virtual screen, don't update real screen


    def plotsquare(self, player, x, y, par):
        # plot square, x, y are internal coords - translates them to ncurses
        scr = self.one
        if player == 2:
            x=-x
        for i in range(globalvar.zoom[0]):
            cx = (game.xsize-x)*globalvar.zoom[0]+i
            cy = (y-1)*globalvar.zoom[1]+1
            if cx >= LINES()-1:
                cx = cx-LINES()+2
                scr = self.two
            if cx > 0:
                scr.move(cx, cy)
                if par:
                    if globalvar.ascii:
                        p = pieces.sqchars[par]
                    else:
                        p = " "
                    scr.addchstr(p*globalvar.zoom[1])
                    if globalvar.inverse:
                        attr = A_REVERSE
                    else:
                        attr = A_NORMAL
                    scr.chgat(globalvar.zoom[1], attr, pieces.sqcolours[par])
                else:
                    scr.addchstr(" "*globalvar.zoom[1])

    def refresh(self):
        self.info.noutrefresh()
        self.two.noutrefresh()
        self.one.noutrefresh()
        doupdate() # update screen
        
    def drawsep(self, c):
        cx = game.xsize*globalvar.zoom[0]
        scr = self.one
        if cx >= LINES()-1:
            cx = cx - LINES()+2
            scr = self.two
        scr.move(cx, 1)
        scr.addchstr(c*game.ysize*globalvar.zoom[1])
        cx = globalvar.xsize*2*globalvar.zoom[0]
        scr = self.one
        if cx >= LINES()-1:
            cx = cx - LINES()+2
            scr = self.two
        scr.move(cx, 1)
        scr.addchstr(c*game.ysize*globalvar.zoom[1])

    def redisplay(self):
        self.drawsep(" ")
        for i in range(1, game.xsize+1):
            for j in range(1, game.ysize+1):
                p = game.matrix[i, j]
                if p:
                    s = p
                else:
                    s = None
                self.plotsquare(1, i, j, s)
        for i in range(game.hismatrix.imin, game.hismatrix.imax+1):
            for j in range(game.hismatrix.jmin, game.hismatrix.jmax+1):
                p = game.hismatrix[i, j]
                if p:
                    s = p
                else:
                    s = None
                self.plotsquare(2, i, j, s)
        self.drawsep("=")
        self.displayscore()

    def displayscore(self):
        self.info.move(2, 1)
        self.info.addchstr("%6i" % game.score)

    

class Net:

    def __init__(self):
        self.outsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.outsock.bind('', 0)
        self.insock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.insock.setblocking(0)
        self.insock.bind('', globalvar.myport)
        self.netbuf = ""

    def sendmsg(self, msg, level):
        if level <= globalvar.nlevel:
            try:
                self.outsock.sendto(msg+"\n", (globalvar.otherhost, globalvar.otherport))
            except socket.error, x:
                pass

    def receivemsg(self):
        try:
            data, addr = self.insock.recvfrom(4096)
        except socket.error, x:
            if x[0] == 11:
                return None
            print "Network error:", x
            return None
        return data
 



class Game:

    def __init__(self):

        self.xsize = globalvar.xsize
        self.ysize = globalvar.ysize
        # size of canvas. xsize is vertical, ysize horizontal
        self.matrix = Matrix(1, self.xsize, 1, self.ysize)
        self.sleeptime = 0.04
        self.bottomline = 0  # how high does the bottom reach
        self.hisbottomline = 0
        self.score = 0
        self.hisscore = 0
        self.hisnmb = 255 # to control if his piece is new one
        self.nmb = 0


    def makebottom(self):
        self.sleeptime = self.sleeptime*globalvar.speedup
        if self.bottomline >= self.xsize:  # end of game - lost
            return 3
        found = 0
        nm = Matrix(1, self.xsize, 1, self.ysize)
        inm = 1
        for i in range(1, self.bottomline+1):
            gl = self.matrix.getline(i)
            bline = map(lambda x: not not x, gl)
            if bline <> self.ysize*[1]: # not full line - copy it, remove full lines
                nm.setline(inm, gl)
                inm = inm+1
            else:
                found = found + 1
        self.bottomline = self.bottomline - found

        if found:
            self.score = self.score + 8*found
            self.xsize = self.xsize + found*globalvar.pushing
            net.sendmsg("X%i %.5f" % (self.xsize, self.sleeptime), 1)
            self.matrix = nm
            for i in range(found*globalvar.pushing):
                self.matrix.addline()
            display.redisplay()
            if self.xsize >= 2*globalvar.xsize:  # end of game - victory
                return 4
        mtob = []
        for i in range(1, self.bottomline+1):
            mtob.append(self.matrix.getline(i))
        net.sendmsg("B" + string.replace(`[self.bottomline]+mtob`, " ", ""), 2)
        
        return 2
                    
        
                
class Pieces:

    def __init__(self, t):

        self.sqchars = " #$%@*+&"
        self.sqcolours = (0,1,2,3,4,5,6,7)
        masks = {}
        masks["basic"] = [ # (xpos, ypos, type)
                     [(0,-1,7), (0,0,7), (0,1,7), (0,2,7)],
                     [(-1,1,2), (-1,0,2), (0,0,2), (0,1,2)],
                     [(1,-1,3), (1,0,3), (0,0,3), (0,1,3)],
                     [(-1,-1,4), (-1,0,4), (-1,1,4), (0,1,4)],
                     [(0,-1,5), (0,0,5), (0,1,5), (1,0,5)],
                     [(-1,1,6), (0,1,6), (1,0,6), (0,0,6)],
                     [(0,-1,1), (0,0,1), (0,1,1), (-1,1,1)]
                    ]
                    
        masks["simple"] = [
                     [(0,-1,1), (0,0,1), (0,1,1)],
                     [(-1,0,2), (-1,1,2), (0,1,2)],
                     [(1,0,3), (1,1,3), (0,1,3)]
                    ]

        masks["extended"] = masks["simple"]+masks["basic"] + \
                [
                     [(0,0,1)],
                     [(0,-1,2), (0,0,2)],
                     [(1,-1,3), (0,-1,3), (0,0,3), (0,1,3), (1,1,3)],
                     [(1,-1,4), (0,-1,4), (1,0,4), (0,1,4), (1,1,4)]
                ] 
        masks["crazy"] = masks["extended"] + \
                [
                     [(0,-1,1), (0,0,1), (0,2,1)],
                     [(-1,0,2), (-1,1,2), (0,-1,2)],
                     [(1,0,3), (1,1,3), (0,-1,3)],
                     [(-1,-1,3), (0,0,3), (1,1,3)],
                     [(0,-1,4), (0,1,4), (1,0,4)],
                     [(0,-1,2), (0,0,2), (1,1,2), (1,2,2)],
                     [(1,-1,1), (1,0,1), (0,1,1), (0,2,1)],
                     [(-1,0,4), (0,1,4), (1,0,4), (0,-1,4)],
                     [(-1,1,7), (1,1,7), (1,-1,7), (-1,-1,7)],
                     [(-1,-1,5), (0,0,5), (1,1,5), (-1,1,5), (1,-1,5)],
                     [(-1,-1,6), (-1,0,6), (-1,1,6), (0,-1,6), (0,1,6), (1,-1,6), (1,0,6), (1,1,6), (1,-1,6)]
                ]
                    
        self.masks = masks[t]
                    
    def getpiece(self):
        #return self.masks[whrandom.randint(0, len(self.masks)-1)]
        return whrandom.choice(self.masks)



class HisPiece:
    def __init__(self, mask=[]):
        self.x = game.xsize
        self.y = game.ysize/2
        self.mask = mask

    def draw(self):
        for i in self.mask:
            display.plotsquare(2, self.x + i[0], self.y + i[1], i[2])
    
    def undraw(self):
        for i in self.mask:
            display.plotsquare(2, self.x + i[0], self.y + i[1], None)


class Piece:

    def __init__(self, mask):
        self.x = game.xsize
        self.y = game.ysize/2
        self.mask = mask
        self.tickc = 15
        self.nmb = game.nmb + 1
        if self.nmb > 255:
            self.nmb = 0
        game.nmb = self.nmb

    def tick(self, tickv=1):
        self.tickc = self.tickc - tickv
        if self.tickc <= 0:
            self.tickc = 15
            r = self.down()
        else:
            r = 0
        return r
        
    def down(self):
        global game
        r = self.validpos(self.x-1, self.y, self.mask)
        if r == 1: # ok, go down
            self.undraw()
            self.x = self.x - 1
            self.draw()
        elif r == 2: # we hit the bottom
            for i in self.mask:
                try:
                    game.matrix[self.x+i[0], self.y+i[1]] = i[2]
                    game.bottomline = max(game.bottomline, self.x+i[0])
                    game.score = game.score + 1
                    display.displayscore()
                except:
                    return 3 # end of game
            r = game.makebottom()
        return r
        
    def left(self):
        if self.validpos(self.x, self.y-1, self.mask) == 1:
            self.undraw()
            self.y = self.y - 1
            self.draw()

    def right(self):
        if self.validpos(self.x, self.y+1, self.mask) == 1:
            self.undraw()
            self.y = self.y + 1
            self.draw()

    def rotate(self):
        self.nmask = []
        for i in self.mask:
            self.nmask.append((i[1], -i[0], i[2]))
        if self.validpos(self.x, self.y, self.nmask) == 1:
            self.undraw()
            self.mask = self.nmask
            self.draw()

    def drop(self):
        while 1:
            r = self.down()
            if r == 2 or r == 3 or r == 4:
                return r
        
    def draw(self):
        net.sendmsg("P"+chr(self.nmb)+"%i %i " % (self.x, self.y)  + string.replace(`self.mask`, " ", ""), 3)
        for i in self.mask:
            display.plotsquare(1, self.x + i[0], self.y + i[1], i[2])
        self.redraw()
    
    def undraw(self):
        for i in self.mask:
            display.plotsquare(1, self.x + i[0], self.y + i[1], None)
        
    def redraw(self):
        display.refresh()

    def validpos(self, x, y, mask):
        "test if x, y, mask is a valid position"
        for i in mask:
            if  y+i[1] < 1 or y+i[1] > game.ysize:
                return 0
            if x+i[0] < 1:
                return 2
            if x+i[0] > game.xsize:
                return 1
            if game.matrix[x+i[0], y+i[1]]:
                return 2
        return 1
        
        
def actonmsg(m):

    if not m:
        return 1
    if m[-1:] <> '\n':
        print "Unexpected packet"
        return 1
    t = m[0]
    if t == 'L': # our victory
        return 4
    elif t == 'V': # our lost
        return 3
    elif t == 'X': # change in xsize
        s = string.split(m[1:-1]) # his size
        ns = globalvar.xsize*2 - int(s[0])
        for i in range(game.xsize - ns):
            game.hismatrix.addline()
        game.xsize = ns
        game.sleeptime = float(s[1]) # synchronize speed as well
        display.redisplay()
        return 0
    elif t == 'B': # bottom
        hism = eval(m[1:-1])
        game.hismatrix = Matrix(1, max(len(hism), game.hisbottomline), 1, game.ysize)
        nhb = hism[0]
        for i in range(nhb-game.hisbottomline):
            game.hismatrix.addline()
        game.hisbottomline = nhb
        for i in range(1, len(hism)):
            game.hismatrix.setline(i, hism[i])
        display.redisplay()
    elif t == 'P': # piece
        nmb = ord(m[1])
        s = string.split(m[2:-1], " ", 2)
        nx = int(s[0])
        if nmb == game.hisnmb: # the same packet
            if nx <= game.hispiece.x: # but only if received in order
                game.hispiece.undraw()
                game.hispiece.x = nx
                game.hispiece.y = int(s[1])
                game.hispiece.mask = eval(s[2])
                game.hispiece.draw()
        else:
            game.hisnmb = nmb
            game.hispiece.x = nx
            game.hispiece.y = int(s[1])
            game.hispiece.mask = eval(s[2])
            game.hispiece.draw()
    else:
        return 1


def kpract(a):
    "read pressed key and act on it"
    tickv = 1
    r = 1
    kpr = display.one.getch()
    
    if kpr == KEY_LEFT:
        kpr = ord('u')
    elif kpr == KEY_RIGHT:
        kpr = ord('o')
    elif kpr == KEY_UP:
        kpr = ord('i')
    elif kpr == KEY_DOWN:
        kpr = ord('m')
        
    if kpr == ord('u'):
        a.left()
    elif kpr == ord('o'):
        a.right()
    elif kpr == ord('i'):
        a.rotate()
    elif kpr == ord('m'):
        tickv = 15
    if kpr == ord(' '):
        r = a.drop()
    else:
        r = a.tick(tickv)
    return r
        
def kpract_r(a):
    "act as robot"
    tickv = 1
    r = 1
    kpr = ord(whrandom.choice(['u', 'i', 'o']))
    if whrandom.random()>0.3:
        kpr = -1
    if kpr == ord('u'):
        a.left()
    elif kpr == ord('o'):
        a.right()
    elif kpr == ord('i'):
        a.rotate()
    elif kpr == ord('m'):
        tickv = 15
    if kpr == ord(' '):
        r = a.drop()
    else:
        r = a.tick(tickv)
    return r
        


def main():
    global game
    display.drawsep("=")

    game.hispiece = HisPiece()
    game.hismatrix = copy.deepcopy(game.matrix)
    if globalvar.wait:
        while 1:
            net.sendmsg("R", 0) # request to run
            time.sleep(1)
            m = net.receivemsg()
            if m=="R\n" or m=="U\n": # other side is alive
                break
        while 1:
            net.sendmsg("U", 0)
            time.sleep(1)
            m = net.receivemsg()
            if m<>None and m<>"R\n": # other side is already playing
                break
    
        for i in range(5): 
            net.sendmsg("U", 0)
            time.sleep(1)
        

    while 1:
        a = Piece(pieces.getpiece())
        while 1:
            m = net.receivemsg()
            r = actonmsg(m)
            if r==3 or r==4:
                break
            r = 1
            time.sleep(game.sleeptime)
            r = globalvar.kpract(a)
            if r == 2 or r == 3 or r == 4: # bottom or end of game
                break
        if r == 3 or r == 4: # end of game
            break
    beep()
    if r == 3:
        for i in range(10): # give the net 10 seconds
            net.sendmsg("L", 1)
            time.sleep(1)
            m = net.receivemsg()
            if m=="E\n" or m=="V\n": # other side signals end of game or victory
                break
        s = "You lost! Your score is %i." % game.score
    elif r == 4:    
        for i in range(10): # give the net 10 seconds
            net.sendmsg("V", 1)
            time.sleep(1)
            m = net.receivemsg()
            if m=="E\n" or m=="L\n": # other side signals end of game or defeat
                break
        s = "You won! Your score is %i." % game.score
    else:
        for i in range(10): # give the net 10 seconds
            net.sendmsg("E", 1)
            time.sleep(1)
            m = net.receivemsg()
            if m=="E\n" or m=="L\n" or m=="V\n":
                break
        s = "What happened?"
    time.sleep(1)
    display.stdscr.move(LINES()/2, (COLS()-len(s))/2)
    display.stdscr.addstr(s) # output string
    display.stdscr.noutrefresh()
    doupdate()
    beep()
    time.sleep(3)
    s = "Press a key...."
    display.stdscr.move(LINES()/2+2, (COLS()-len(s))/2)
    display.stdscr.addstr(s)
    beep()
    display.stdscr.getch()    
    sys.exit(r)




def help():
    print """

pytris [-options] myport hostname otherport


-a n, --ascii-chars=n
    if n=0, do not use ascii characters to draw pieces
-c n, --colour=n
    if n=0, do not use colour
-r n, --inverse=n
    if n=1, use reverse colour
-x n, --vsize=n
    set vertical size to n
-y n, --hsize=n
    set horizontal size to n
--hzoom=n
    set horizontal zoom
--vzoom=n
    set vertical zoom
-p name, --pieces=name
    select type of falling pieces, name is one of:
    simple
    basic
    extended
    crazy
-n n, --nlevel=n
    select network level, 1<=n<=3
    the bigger level, the more information is transferred
-h, --help
-v, --version
-w n, --wait=n
    if n=0, do not wait for your oponent to start game
--robot=n
    if n=1, act as a (completely dumb) robot
--pushing=n
    push screen by n lines down when you complete a line.
    default n=1
""" 
    sys.exit(0)


def version():
    print "version"
    sys.exit(0)

globalvar = Globalvar()

try:
    optlist, args = getopt.getopt(sys.argv[1:], "a:c:r:x:y:n:p:w:vh",
                        ["ascii-chars=", "colour=", "inverse=",
                         "hsize=", "vsize=", "pieces=", "nlevel=",
                         "wait=", "robot=", "hzoom=", "vzoom=", "pushing=",
                         "version", "help"])
except:
    help()

for i in optlist:
    if i[0] in ["--ascii-chars", "-a"]:
        globalvar.ascii = int(i[1])
    elif i[0] in ["--colour", "-c"]:
        globalvar.colours = int(i[1])
    elif i[0] in ["--inverse", "-r"]:
        globalvar.inverse = int(i[1])
    elif i[0] in ["--vsize", "-x"]:
        globalvar.xsize = int(i[1])
    elif i[0] in ["--hsize", "-y"]:
        globalvar.ysize = int(i[1])
    elif i[0] in ["--pieces", "-p"]:
        globalvar.pieces = i[1]
    elif i[0] in ["--nlevel", "-n"]:
        globalvar.nlevel = int(i[1])
    elif i[0] in ["--wait", "-w"]:
        globalvar.wait = int(i[1])
    elif i[0] == "--hzoom":
        globalvar.zoom = (globalvar.zoom[0], int(i[1]))
    elif i[0] == "--vzoom":
        globalvar.zoom = (int(i[1]), globalvar.zoom[1])
    elif i[0] == "--pushing":
        globalvar.pushing = int(i[1])
    elif i[0] == "--robot":
        if i[1] == "1":
            globalvar.kpract = kpract_r
        else:
            globalvar.kpract = kpract
    elif i[0] in ["--version", "-v"]:
        version()
    elif i[0] in ["--help", "-h"]:
        help()



if len(args) <> 3:
    help()

globalvar.myport = int(args[0])
globalvar.otherhost = args[1]
globalvar.otherport = int(args[2])

display = Display()
game = Game()
pieces = Pieces(globalvar.pieces)
net=Net()
main()

