commit e904d0bf052a3f72ee84e98e71b247047afcce86 Author: jpenner Date: Tue Jun 14 23:15:01 2005 +0000 Initial check-in. diff --git a/TennisForTwo.leo b/TennisForTwo.leo new file mode 100644 index 0000000..73c65ea --- /dev/null +++ b/TennisForTwo.leo @@ -0,0 +1,156 @@ + + + + + + + + + + +Files +@thin TennisForTwo.py +@thin widgets.py +@thin widgetsdemo.py +@thin pygame2exe.py +@thin pygame2app.py + +Tasks +Starting menu +__init__ +<< Menu layout >> + + +Utilities +@run TennisForTwo.py +@run TennisForTwo.py client localhost 7554 +@run widgetsdemo.py +@run PyGame2Exe.py py2exe +@run python2.4 pygame2app.py py2app + + + + + +@ignore +@language python +#make standalone, needs at least pygame-1.5.3 and py2exe-0.3.1 + +from distutils.core import setup +import sys, os, pygame, shutil, glob +import py2exe + +#setup the project variables here. +#i can't claim these will cover all the cases +#you need, but they seem to work for all my +#projects, just change as neeeded. + + +script = "TennisForTwo.py" #name of starting .PY +icon_file = "" #ICO file for the .EXE (not working well) +optimize = 2 #0, 1, or 2; like -O and -OO +dos_console = 0 #set to 0 for no dos shell when run +extra_data = ['data'] #extra files/dirs copied to game +extra_modules = ['pygame.locals'] #extra python modules not auto found + + + + + + +#use the default pygame icon, if none given +if not icon_file: + path = os.path.split(pygame.__file__)[0] + icon_file = '"' + os.path.join(path, 'pygame.ico') + '"' +#unfortunately, this cool icon stuff doesn't work in current py2exe :( +#icon_file = '' + +project_name = os.path.splitext(os.path.split(script)[1])[0] + + +#this will create the executable and all dependencies +setup(#name=project_name, + windows=[script], + data_files=[("data", glob.glob("data\\*"))] + ) + + + + + +def __init__( self ): + """Initialize Demo Class""" + # instead of pygame.init(), initialize modules manually to + # avoid initing pygame.sound + pygame.display.init() + pygame.font.init() + + # setup screen + screen = pygame.display.set_mode( (640, 480), DOUBLEBUF ) + pygame.display.set_caption( 'WidgetsDemo' ) + + # request regular event for updating animation + self.DRAWEVENT = USEREVENT + 1 + pygame.time.set_timer( self.DRAWEVENT, 33 ) #33 == 30fps + + # filter events + badevents = [NOEVENT, ACTIVEEVENT, JOYAXISMOTION, JOYBALLMOTION, JOYHATMOTION, JOYBUTTONDOWN ,JOYBUTTONUP, VIDEORESIZE, SYSWMEVENT, NUMEVENTS] + goodevents = [self.DRAWEVENT, KEYDOWN, KEYUP, MOUSEMOTION, MOUSEBUTTONDOWN, MOUSEBUTTONUP, QUIT ] + pygame.event.set_blocked( badevents ) + + # initialize the WidgetWindow base class + WidgetWindow.__init__( self, screen ) + + # create special widgets + edit = EditClass( self, self.editaction, (325, 300, 265, 25), "text" ) + self.page = PageClass( self, (325, 25, 265, 275), GRAY ) + + # put the widgets in the window + self.addwidget( MultiLineTextClass( self, (25, 25, 250, 145), "MultiLineTextClass\n(transparent)\nHas automatic word wrapping.", 36 ) ) + self.addwidget( MultiLineTextClass( self, (25, 195, 250, 145), "MultiLineTextClass\n(with background)\nHas automatic word wrapping.", 36, WHITE, BLACK ) ) + self.addwidget( EditClass( self, self.editaction, (25, 350, 150, 30), "second edit box" ), TABTARGET ) + self.addwidget( TextClass( self, (25, 400, 590, 19), "TextClass (transparent)", 20 ) ) + self.addwidget( TextClass( self, (25, 440, 590, 19), "TextClass (with background)", 20, WHITE, BLACK ) ) + self.addwidget( self.page ) + self.addwidget( edit, TABTARGET ) + self.addwidget( ButtonClass( self, self.buttonaction, (205, 110, 200, 105), "This button overlaps" ) ) + + # set keyboard focus to the edit widget + edit.focus() + + # initial screen draw + self.eventproc( pygame.event.Event( NOEVENT, {} ) ) + + # animation data + self.animrect = Rect( (325, 350, 265, 50) ) + self.animline = Rect( self.animrect ) + self.animline.width = 5 + self.animline.left = self.animrect.left + + + +self.addwidget(TextClass(self, (120, 40, 400, 30), "Tennis For Two", 36)) + +self.addwidget(ButtonClass(self, self.startSingle, (120, 120, 400, 30), "Start Single-Player Game")) + +self.addwidget(TextClass(self, (120, 190, 400, 20), "Your IP Address: " + getMyIP())) +self.addwidget(TextClass(self, (120, 215, 60, 20), "Port:")) +self.serverPort = EditClass(self, None, (180, 215, 60, 20), "7554") +self.addwidget(self.serverPort) +self.addwidget(ButtonClass(self, self.startServer, (120, 240, 400, 30), "Start Server")) + +self.addwidget(TextClass(self, (120, 310, 160, 20), "Server Address:")) +self.clientAddr = EditClass(self, None, (280, 310, 240, 20)) +self.addwidget(self.clientAddr) +self.addwidget(TextClass(self, (120, 335, 60, 20), "Port:")) +self.clientPort = EditClass(self, None, (180, 335, 60, 20), "7554") +self.addwidget(self.clientPort) +self.addwidget(ButtonClass(self, self.startClient, (120, 360, 400, 30), "Start Client")) + + + + + diff --git a/TennisForTwo.py b/TennisForTwo.py new file mode 100644 index 0000000..209e4c4 --- /dev/null +++ b/TennisForTwo.py @@ -0,0 +1,814 @@ +#@+leo-ver=4-thin +#@+node:jpenner.20050305105206:@thin TennisForTwo.py +#@@language python + +import pygame, math, cPickle, sys, StringIO, socket, re, urllib +from pygame.locals import * +from twisted.internet import task, reactor, protocol, udp +from widgets import * + +#@+others +#@+node:jpenner.20050601180947:Helper / Misc +#@+others +#@+node:jpenner.20050305105934:Constants +#constants +SCREENRECT = Rect(0, 0, 640, 480) +GRAVITY = 0.25 +FRICTION = 4 +BALL_STARTX = 100 +BALL_STARTY = 350 + +SINGLE_PLAYER = 10 +#@nonl +#@-node:jpenner.20050305105934:Constants +#@+node:jpenner.20050310195953:Globals +#globals +class Game: + currentplayer = 1 + SeverPort = 7554 + Port = None +#@nonl +#@-node:jpenner.20050310195953:Globals +#@+node:jpenner.20050424164431:Logging +def startLogging(fn): + Game.logfile = open(fn, 'wt') + +def log(text): + Game.logfile.write(str(text) + '\n') + +def stopLogging(): + Game.logfile.close() +#@nonl +#@-node:jpenner.20050424164431:Logging +#@+node:jpenner.20050427175747:Angles +def makeAngle(pos): + xdist = pos[0] - Game.ball.rect.centerx + ydist = pos[1] - Game.ball.rect.centery + return math.atan2(ydist, xdist) + +def anglePos(rect, angle, radius): + return( (rect.centerx + (math.cos(angle) * radius)), (rect.centery + (math.sin(angle) * radius))) + +#@-node:jpenner.20050427175747:Angles +#@+node:jpenner.20050605110605:IP address +def getMyIP(): + try: + f = urllib.urlopen('http://checkip.dyndns.org') + s = f.read() + m = re.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s) + outsideip = m.group(0) + except: + outsideip = None + + try: + insideip = socket.gethostbyname(socket.gethostname()) + except: + insideip = None + + if (outsideip == None) and (insideip == None): + ip = "Unknown" + else: + if (outsideip == None): + ip = insideip + " (No internet?)" + else: + ip = outsideip + if (insideip <> None) and (insideip <> outsideip): + ip = ip + " (Firewalled?)" + return ip +#@-node:jpenner.20050605110605:IP address +#@-others +#@nonl +#@-node:jpenner.20050601180947:Helper / Misc +#@+node:jpenner.20050305124252:Events +#@+others +#@+node:jpenner.20050305130011.1:Event Manager +class EventManager: + def __init__(self): + self.queue = [] + self.handlers = [[] for i in range(NUM_EVENTS)] + + def postEvent(self,event): + self.queue.append(event); + + def tick(self): + for event in self.queue: + for handler in self.handlers[event.type]: + handler(event) + self.queue = [] + + def registerHandler(self, evtype, func): + if (evtype == NUM_EVENTS): + for i in range(NUM_EVENTS): + self.registerHandler(i, func) + else: + self.handlers[evtype].append(func) +#@nonl +#@-node:jpenner.20050305130011.1:Event Manager +#@+node:jpenner.20050305130011:Types +EV_HIT = 0 +EV_SCORE = 1 +EV_BOUNCE = 2 +EV_PLAYER_UPDATE = 3 +EV_SERVE = 4 +EV_CLICK = 5 +EV_BALLPOS = 6 +EV_CONNECT = 7 +EV_PLAYER_SWITCH = 8 +EV_HELLO = 9 +NUM_EVENTS = 10 + +class Event: + def __init__(self, type): + self.type = type + self.fromplayer = Game.myplayer + +class ClickEvent (Event): + def __init__(self, xvel, yvel, player): + Event.__init__(self, EV_CLICK) + self.xvel = xvel + self.yvel = yvel + self.player = player + +class HitEvent (Event): + def __init__(self, xvel, yvel): + Event.__init__(self, EV_HIT) + self.xvel = xvel + self.yvel = yvel + +class ScoreEvent (Event): + def __init__(self, player): + Event.__init__(self, EV_SCORE) + self.player = player + +class BounceEvent (Event): + def __init__(self): + Event.__init__(self, EV_BOUNCE) + +class PlayerUpdateEvent (Event): + def __init__(self): + Event.__init__(self, EV_PLAYER_UPDATE) + +class ServeEvent (Event): + def __init__(self): + Event.__init__(self, EV_SERVE) + +class BallPosEvent (Event): + def __init__(self, x, y): + Event.__init__(self, EV_BALLPOS) + self.x = x + self.y = y + +class ConnectEvent (Event): + def __init__(self): + Event.__init__(self, EV_CONNECT) + +class PlayerSwitchEvent (Event): + lastseq = 0 + def __init__(self, xvel, yvel, xpos, ypos, stopped): + Event.__init__(self, EV_PLAYER_SWITCH) + self.xvel = xvel + self.yvel = yvel + self.xpos = xpos + self.ypos = ypos + self.stopped = stopped + self.seq = PlayerSwitchEvent.lastseq + PlayerSwitchEvent.lastseq = PlayerSwitchEvent.lastseq + 1 + +class HelloEvent(Event): + def __init__(self): + Event.__init__(self, EV_HELLO) + +#@-node:jpenner.20050305130011:Types +#@-others +#@nonl +#@-node:jpenner.20050305124252:Events +#@+node:jpenner.20050319143816:Networking +#@+others +#@+node:jpenner.20050320120319:Message +class TFTMessage: + def __init__(self, sequence, event): + self.sequence = sequence + self.event = event +#@nonl +#@-node:jpenner.20050320120319:Message +#@+node:jpenner.20050424135832:Protocol +class TFTProtocol (protocol.DatagramProtocol): + def __init__(self): + self.mysequence = 0 + self.hissequence = -1 + self.address = None + Game.evMgr.registerHandler(EV_BALLPOS, self.sendEvent) + Game.evMgr.registerHandler(EV_SERVE, self.sendEvent) + Game.evMgr.registerHandler(EV_PLAYER_SWITCH, self.sendEvent) + Game.evMgr.registerHandler(EV_CONNECT, self.connectTransport) + + def startProtocol(self): + if self.address <> None: + log (self.address) + + def datagramReceived(self, data, address): + if self.address == None: + self.address = address + log(address) + msg = cPickle.Unpickler(StringIO.StringIO(data)).load() + + if (msg.sequence > self.hissequence): + self.hissequence = msg.sequence + if msg.event <> None: + log ("get " + str(msg.event.type) + ": " + str(msg.event.fromplayer)) + Game.evMgr.postEvent(msg.event) + + def sendEvent(self, event): + if (self.address <> None) and (self.transport <> None) and (event.fromplayer == Game.myplayer): + log ("send " + str(event.type)) + self.mysequence = self.mysequence + 1 + s = StringIO.StringIO() + cPickle.Pickler(s).dump(TFTMessage(self.mysequence, event)) + self.transport.write(s.getvalue(), self.address) + + def connectTransport(self, event): + Game.evMgr.postEvent(HelloEvent()) + + + +#@-node:jpenner.20050424135832:Protocol +#@-others +#@-node:jpenner.20050319143816:Networking +#@+node:jpenner.20050604112932:Menu +#@+others +#@+node:jpenner.20050605142055:Base Menu +class BaseMenu(WidgetWindow): + def __init__(self): + WidgetWindow.__init__(self, Game.screen) + + self.menuLayout() + + def onEnter(self): + self.invalidaterect() + self.eventproc( pygame.event.Event( NOEVENT, {} ) ) + + def tick(self): + for event in pygame.event.get(): + if event.type == QUIT: + reactor.stop() + try: + self.eventproc(event) + except: + pass + +#@-node:jpenner.20050605142055:Base Menu +#@+node:jpenner.20050605102056:Main Menu +class MainMenu(BaseMenu): + def menuLayout(self): + #@ << Menu layout >> + #@+node:jpenner.20050605102056.1:<< Menu layout >> + self.addwidget(TextClass(self, (120, 40, 400, 30), "Tennis For Two", 36)) + + self.addwidget(ButtonClass(self, self.startSingle, (120, 120, 400, 30), "Start Single-Player Game")) + + self.addwidget(TextClass(self, (120, 190, 400, 20), "Your IP Address: " + getMyIP())) + self.addwidget(TextClass(self, (120, 215, 60, 20), "Port:")) + self.serverPort = EditClass(self, None, (180, 215, 60, 20), "7554") + self.addwidget(self.serverPort) + self.addwidget(ButtonClass(self, self.startServer, (120, 240, 400, 30), "Start Server")) + + self.addwidget(TextClass(self, (120, 310, 160, 20), "Server Address:")) + self.clientAddr = EditClass(self, None, (280, 310, 240, 20)) + self.addwidget(self.clientAddr) + self.addwidget(TextClass(self, (120, 335, 60, 20), "Port:")) + self.clientPort = EditClass(self, None, (180, 335, 60, 20), "7554") + self.addwidget(self.clientPort) + self.addwidget(ButtonClass(self, self.startClient, (120, 360, 400, 30), "Start Client")) + #@-node:jpenner.20050605102056.1:<< Menu layout >> + #@nl + + def startSingle(self): + Game.myplayer = SINGLE_PLAYER + Game.isClient = False + Game.rules.infinitehits = True + Game.gameMgr.changeState(Game.gameMgr.STATE_SINGLE_PLAYER) + + def startServer(self): + Game.myplayer = 1 + Game.isClient = False + Game.ServerPort = int(self.serverPort.text) + Game.gameMgr.changeState(Game.gameMgr.STATE_WAIT_FOR_CLIENT) + + def startClient(self): + Game.myplayer = 2 + Game.isClient = True + Game.ServerPort = int(self.clientPort.text) + Game.ServerIP = self.clientAddr.text + Game.gameMgr.changeState(Game.gameMgr.STATE_CONNECT) +#@-node:jpenner.20050605102056:Main Menu +#@+node:jpenner.20050605173506:Server Wait Screen +class ServerWait(BaseMenu): + def menuLayout(self): + self.addwidget(TextClass(self, (120, 180, 400, 40), "Waiting For Client...", 48)) + self.addwidget(ButtonClass(self, self.cancel, (120, 360, 400, 30), "Cancel")) + + self.connected = False + Game.evMgr.registerHandler(EV_HELLO, self.connect) + + def cancel(self): + Game.gameMgr.changeState(Game.gameMgr.STATE_MENU) + + def connect(self, ev): + self.connected = True + + def tick(self): + BaseMenu.tick(self) + if self.connected: + self.connected = False + Game.gameMgr.changeState(Game.gameMgr.STATE_NETWORK_GAME) +#@nonl +#@-node:jpenner.20050605173506:Server Wait Screen +#@+node:jpenner.20050605180040:Client Wait Screen +class ClientWait(BaseMenu): + def menuLayout(self): + self.addwidget(TextClass(self, (120, 180, 400, 40), "Contacting Server...", 48)) + self.addwidget(ButtonClass(self, self.cancel, (120, 360, 400, 30), "Cancel")) + + self.connected = False + Game.evMgr.registerHandler(EV_BALLPOS, self.connect) + + def cancel(self): + Game.gameMgr.changeState(Game.gameMgr.STATE_MENU) + + def connect(self, ev): + self.connected = True + + def tick(self): + BaseMenu.tick(self) + if self.connected: + self.connected = False + Game.gameMgr.changeState(Game.gameMgr.STATE_NETWORK_GAME) + else: + Game.network.sendEvent(ConnectEvent()) + +#@-node:jpenner.20050605180040:Client Wait Screen +#@-others +#@-node:jpenner.20050604112932:Menu +#@+node:jpenner.20050305121157:Game +#@+others +#@+node:jpenner.20050604175241:Graphics +#@+others +#@+node:jpenner.20050604192205:Generic Graphics Manager +class GameGraphicsMgr: + def __init__(self): + self.all = pygame.sprite.RenderUpdates() + + self.bkg = pygame.Surface((SCREENRECT.width, SCREENRECT.height)) + self.bkg.fill(pygame.color.Color("black")) + + self.trackedSprites = [] + + def trackSprite(self, sprite, spriteToTrack): + self.trackedSprites.append({'sprite': sprite, 'spriteToTrack': spriteToTrack}) + + def clearScreen(self): + Game.screen.set_clip() + Game.screen.fill(pygame.color.Color("black")) + pygame.display.update() + + def tick(self): + for item in self.trackedSprites: + item['sprite'].rect.centerx = item['spriteToTrack'].rect.centerx + item['sprite'].rect.centery = item['spriteToTrack'].rect.centery + + self.all.clear(Game.screen, self.bkg) + self.all.update() + dirty = self.all.draw(Game.screen) + pygame.display.update(dirty) + +#@-node:jpenner.20050604192205:Generic Graphics Manager +#@+node:jpenner.20050604175241.1:TFT Graphics Manager +class TFTGraphicsMgr(GameGraphicsMgr): + def __init__(self): + GameGraphicsMgr.__init__(self) + Floor.containers = self.all + Net.containers = self.all + Ball.containers = self.all + ShotLine.containers = self.all + + Game.ball = Ball(BALL_STARTX, BALL_STARTY) + Game.floor = Floor() + Game.net = Net() + self.shotLine = ShotLine() + + self.trackSprite(self.shotLine, Game.ball) + + def tick(self): + self.shotLine.updateAngle() + GameGraphicsMgr.tick(self) +#@-node:jpenner.20050604175241.1:TFT Graphics Manager +#@+node:jpenner.20050305120654:Sprites +#@+others +#@+node:jpenner.20050305112032:Floor +class Floor(pygame.sprite.Sprite): + def __init__(self): + pygame.sprite.Sprite.__init__(self, self.containers) + self.image = pygame.Surface((SCREENRECT.width - (SCREENRECT.width / 8), SCREENRECT.height / 96)) + self.image.fill(pygame.color.Color("white")) + self.rect = self.image.get_rect() + self.reloading = 0 + self.rect.centerx = SCREENRECT.centerx + self.rect.bottom = SCREENRECT.bottom * 0.875 + self.origtop = self.rect.top + self.facing = -1 + + +#@nonl +#@-node:jpenner.20050305112032:Floor +#@+node:jpenner.20050305120626:Net +class Net(pygame.sprite.Sprite): + def __init__(self): + pygame.sprite.Sprite.__init__(self, self.containers) + self.image = pygame.Surface((SCREENRECT.width / 128, SCREENRECT.height / 12)) + self.image.fill(pygame.color.Color("white")) + self.rect = self.image.get_rect() + self.reloading = 0 + self.rect.centerx = SCREENRECT.centerx + self.rect.bottom = SCREENRECT.bottom * 0.875 + self.origtop = self.rect.top + self.facing = -1 + + +#@-node:jpenner.20050305120626:Net +#@+node:jpenner.20050427175728:Ball +class Ball(pygame.sprite.Sprite): + def __init__(self, x, y): + pygame.sprite.Sprite.__init__(self, self.containers) + self.image = pygame.Surface((SCREENRECT.width / 128, SCREENRECT.height / 96)) + self.image.fill(pygame.color.Color("white")) + self.rect = self.image.get_rect() + self.reloading = 0 + self.rect.centerx = x + self.rect.centery = y + self.origtop = self.rect.top + self.facing = -1 + + def currentPlayer(self): + if self.rect.centerx < Game.net.rect.centerx: + return 1 + else: + return 2 + + + +#@-node:jpenner.20050427175728:Ball +#@+node:jpenner.20050305123236:Shot Line +class ShotLine(pygame.sprite.Sprite): + def __init__(self): + pygame.sprite.Sprite.__init__(self, self.containers) + self.image = pygame.Surface((SCREENRECT.width / 32, SCREENRECT.height / 24), SRCALPHA) + self.image.set_colorkey(pygame.color.Color("purple")) + self.rect = self.image.get_rect() + self.updateAngle() + self.reloading = 0 + self.origtop = self.rect.top + self.facing = -1 + + def updateAngle(self): + angle = makeAngle(pygame.mouse.get_pos()) + self.image.fill(pygame.color.Color("purple")) # clear + pygame.draw.line(self.image, pygame.color.Color("white"), + anglePos(self.image.get_rect(), angle, 6), anglePos(self.image.get_rect(),angle,12)) + + +#@-node:jpenner.20050305123236:Shot Line +#@-others +#@nonl +#@-node:jpenner.20050305120654:Sprites +#@-others +#@nonl +#@-node:jpenner.20050604175241:Graphics +#@+node:jpenner.20050320112219:Physics +#@+others +#@+node:jpenner.20050424152556:Physics Manager +class PhysicsMgr: + def __init__(self): + self.gamePhysics = PhysicsEngine() + self.netPhysics = NetworkPhysicsEngine() + + Game.evMgr.registerHandler(EV_PLAYER_UPDATE, self.switchEngines) + Game.evMgr.registerHandler(EV_PLAYER_SWITCH, self.switchToGame) + + def onEnter(self): + if Game.currentplayer == Game.myplayer: + self.current = self.gamePhysics + else: + self.current = self.netPhysics + self.lastSeq = -1 + + def switchEngines(self, ev): + if Game.ball.currentPlayer() <> Game.myplayer: + self.current = self.netPhysics + self.current.switchStarted = True + else: + self.current = self.gamePhysics + + def switchToGame(self, ev): + if ev.fromplayer <> Game.myplayer: + if ev.seq > self.lastSeq: + self.lastSeq = ev.seq + self.gamePhysics.resync(ev) + Game.evMgr.postEvent(PlayerUpdateEvent()) + + + def tick(self): + self.current.tick() +#@nonl +#@-node:jpenner.20050424152556:Physics Manager +#@+node:jpenner.20050306103246:Game Physics +class PhysicsEngine: + def __init__(self): + Game.evMgr.registerHandler(EV_HIT, self.hitHandler) + Game.evMgr.registerHandler(EV_SERVE, self.serveHandler) + + self.xvel = 0 + self.yvel = 0 + self.stopped = True + + def tick(self): + # only do gravity / collision detection if ball is moving + if not self.stopped: + # Gravity + self.yvel += GRAVITY + + #@ << Floor collision >> + #@+node:jpenner.20050310185348:<< Floor collision >> + if (Game.ball.rect.bottom + self.yvel) > Game.floor.rect.top: + self.yvel = -abs(self.yvel) + FRICTION + if self.yvel > 0: + self.yvel = 0 + self.xvel = 0 + self.stopped = True + + Game.evMgr.postEvent(BounceEvent()) + #@nonl + #@-node:jpenner.20050310185348:<< Floor collision >> + #@nl + #@ << Net collision >> + #@+node:jpenner.20050310185348.1:<< Net collision >> + if (( ((Game.ball.rect.right < Game.net.rect.left) and ((Game.ball.rect.right + self.xvel) >= Game.net.rect.left) ) or + ((Game.ball.rect.left > Game.net.rect.right) and ((Game.ball.rect.left + self.xvel) <= Game.net.rect.right) ) ) and + ((Game.ball.rect.bottom + self.yvel) > Game.net.rect.top)): + self.xvel = -self.xvel + + #@-node:jpenner.20050310185348.1:<< Net collision >> + #@nl + #@ << Switch players >> + #@+node:jpenner.20050310185414:<< Switch players >> + if ((Game.ball.rect.centerx < Game.net.rect.centerx) and ((Game.ball.rect.centerx + self.xvel) >= Game.net.rect.centerx) or + (Game.ball.rect.centerx >= Game.net.rect.centerx) and ((Game.ball.rect.centerx + self.xvel) < Game.net.rect.centerx)): + Game.evMgr.postEvent(PlayerUpdateEvent()) + + #@-node:jpenner.20050310185414:<< Switch players >> + #@nl + + Game.ball.rect.centerx += self.xvel + Game.ball.rect.centery += self.yvel + + Game.evMgr.postEvent(BallPosEvent(Game.ball.rect.centerx, Game.ball.rect.centery)) + Game.switchEv = PlayerSwitchEvent(self.xvel, self.yvel, Game.ball.rect.centerx, Game.ball.rect.centery, self.stopped) + + def hitHandler(self,hitEvent): + self.xvel = hitEvent.xvel + self.yvel = hitEvent.yvel + self.stopped = False + + def serveHandler(self, ev): + self.stopped = True + + def resync(self, ev): + self.xvel = ev.xvel + self.yvel = ev.yvel + Game.ball.rect.centerx = ev.xpos + Game.ball.rect.centery = ev.ypos + self.stopped = ev.stopped + +#@nonl +#@-node:jpenner.20050306103246:Game Physics +#@+node:jpenner.20050320112219.1:Network Physics +class NetworkPhysicsEngine: + def __init__(self): + Game.evMgr.registerHandler(EV_BALLPOS, self.updatePos) + self.switchStarted = False + + def updatePos(self, ev): + if (ev.fromplayer <> Game.myplayer): + self.switchStarted = False + Game.ball.rect.centerx = ev.x + Game.ball.rect.centery = ev.y + + def tick(self): + log ("netphysics") + if self.switchStarted: + Game.evMgr.postEvent(Game.switchEv) + +#@-node:jpenner.20050320112219.1:Network Physics +#@-others +#@nonl +#@-node:jpenner.20050320112219:Physics +#@+node:jpenner.20050307180329:Sound +class SoundEngine: + def __init__(self): + f = { EV_HIT: 'hit.wav', + EV_BOUNCE: 'bounce.wav', + EV_SCORE: 'score.wav', + EV_HELLO: 'connect.wav' } + + self.sound = {} + for evtype, filename in f.iteritems(): + try: + self.sound[evtype] = pygame.mixer.Sound('data/' + filename) + Game.evMgr.registerHandler(evtype, self.noise) + except: + pass + + def noise(self, ev): + self.sound[ev.type].play() + + +#@-node:jpenner.20050307180329:Sound +#@+node:jpenner.20050312135503:Rules +class RuleManager: + def __init__(self): + Game.evMgr.registerHandler(EV_CLICK, self.clickHandler) + Game.evMgr.registerHandler(EV_SERVE, self.serveHandler) + Game.evMgr.registerHandler(EV_PLAYER_UPDATE, self.updateHandler) + self.playerhit = False + self.infinitehits = False + Game.currentplayer = 1 + + def clickHandler(self, ev): + if not self.playerhit and (ev.player == Game.currentplayer or ev.player == SINGLE_PLAYER): + Game.evMgr.postEvent(HitEvent(ev.xvel, ev.yvel)) + if not self.infinitehits: + self.playerhit = True + + def updateHandler(self, ev): + self.playerhit = False + Game.currentplayer = Game.ball.currentPlayer() + + def serveHandler(self, ev): + Game.ball.rect.centerx = BALL_STARTX + Game.ball.rect.centery = BALL_STARTY + Game.evMgr.postEvent(PlayerUpdateEvent()) + + +#@-node:jpenner.20050312135503:Rules +#@+node:jpenner.20050310200258:Input +class InputManager: + def tick(self): + for event in pygame.event.get(): + if event.type == QUIT: + reactor.stop() + self.sdl_event(event) + + def sdl_event(self, event): + if event.type == MOUSEBUTTONUP: + angle = makeAngle(event.pos) + Game.evMgr.postEvent( ClickEvent(math.cos(angle) * 12, math.sin(angle) * 9, Game.myplayer) ) + + if event.type == KEYUP: + Game.evMgr.postEvent( ServeEvent() ) + +#@nonl +#@-node:jpenner.20050310200258:Input +#@+node:jpenner.20050305122430:Game Loop + +#@+others +#@+node:jpenner.20050606072251:State Machine +class GameState: + def __init__(self, manager, onenter = None, onexit = None): + def doNothing(): pass + + if manager <> None: + self.tick = manager.tick + else: + self.tick = doNothing + + if onenter <> None: + self.enter = onenter + else: + self.enter = doNothing + + if onexit <> None: + self.exit = onexit + else: + self.exit = doNothing + +class StateMachine: + def __init__(self, states, initialState = 0): + self.state = initialState + self.states = states + self.states[self.state].enter() + self.newstate = -1 + + def changeState(self, newstate): + # Don't change states in the middle of a tick! + self.newstate = newstate + + def tick(self): + if self.newstate >= 0: + self.states[self.state].exit() + self.state = self.newstate + self.states[self.state].enter() + self.newstate = -1 + + self.states[self.state].tick() + +class GameLoop: + def __init__(self, managers): + self.managers = managers + + def tick(self): + for manager in self.managers: + manager.tick() +#@-node:jpenner.20050606072251:State Machine +#@+node:jpenner.20050606072251.1:Game Manager +class GameMgr (StateMachine): + def __init__(self): + self.STATE_MENU = 0 + self.STATE_NETWORK_GAME = 1 + self.STATE_SINGLE_PLAYER = 2 + self.STATE_WAIT_FOR_CLIENT = 3 + self.STATE_CONNECT = 4 + + inputMgr = InputManager() + self.graphicsMgr = TFTGraphicsMgr() + self.physicsMgr = PhysicsMgr() + + mainMenu = MainMenu() + self.serverWait = ServerWait() + self.clientWait = ClientWait() + + StateMachine.__init__(self, + [GameState(MainMenu(), mainMenu.onEnter), + GameState(GameLoop([inputMgr, Game.evMgr, self.physicsMgr, self.graphicsMgr]), self.gameEnter), + GameState(GameLoop([inputMgr, Game.evMgr, PhysicsEngine(), self.graphicsMgr]), self.gameEnter), + GameState(GameLoop([Game.evMgr, self.serverWait]), self.serverEnter, self.networkExit), + GameState(GameLoop([Game.evMgr, self.clientWait]), self.clientEnter, self.networkExit) + ]) + + def gameEnter(self): + self.graphicsMgr.clearScreen() + self.physicsMgr.onEnter() + + def clientEnter(self): + Game.network = TFTProtocol() + def onResolve(ip): + Game.network.address = (ip, Game.ServerPort) + Game.port = reactor.listenUDP(0, Game.network) + reactor.resolve(Game.ServerIP).addCallback(onResolve) + self.clientWait.onEnter() + + def serverEnter(self): + Game.network = TFTProtocol() + Game.port = reactor.listenUDP(Game.ServerPort, Game.network) + log ("Listening on " + str(Game.ServerPort)) + self.serverWait.onEnter() + + def networkExit(self): + if (self.newstate == self.STATE_MENU) and (Game.port <> None): # cancelled + Game.port.stopListening() + +#@-node:jpenner.20050606072251.1:Game Manager +#@-others + + +#@-node:jpenner.20050305122430:Game Loop +#@+node:jpenner.20050319125841:Setup +def main(): + pygame.init() + + startLogging('tennis.log') + + Game.screen = pygame.display.set_mode(SCREENRECT.size, DOUBLEBUF, 16) + + Game.evMgr = EventManager() + Game.sound = SoundEngine() + Game.rules = RuleManager() + Game.gameMgr = GameMgr() + + task.LoopingCall(Game.gameMgr.tick).start(0.03) + + reactor.run() + + #cleanup + stopLogging() + + + +#@-node:jpenner.20050319125841:Setup +#@-others +#@nonl +#@-node:jpenner.20050305121157:Game +#@-others + +if __name__ == '__main__': main() +#@nonl +#@-node:jpenner.20050305105206:@thin TennisForTwo.py +#@-leo diff --git a/data/bounce.wav b/data/bounce.wav new file mode 100644 index 0000000..adeb6e8 Binary files /dev/null and b/data/bounce.wav differ diff --git a/data/connect.wav b/data/connect.wav new file mode 100644 index 0000000..adeb6e8 Binary files /dev/null and b/data/connect.wav differ diff --git a/data/hit.wav b/data/hit.wav new file mode 100644 index 0000000..adeb6e8 Binary files /dev/null and b/data/hit.wav differ diff --git a/data/score.wav b/data/score.wav new file mode 100644 index 0000000..adeb6e8 Binary files /dev/null and b/data/score.wav differ diff --git a/pygame2app.py b/pygame2app.py new file mode 100644 index 0000000..aa10f6e --- /dev/null +++ b/pygame2app.py @@ -0,0 +1,46 @@ +#@+leo-ver=4-thin +#@+node:jpenner.20050604144534:@thin pygame2app.py +#@@language python +#make standalone, needs at least pygame-1.5.3 and py2exe-0.3.1 + +from distutils.core import setup +import sys, os, pygame, shutil, glob +import py2app + +#setup the project variables here. +#i can't claim these will cover all the cases +#you need, but they seem to work for all my +#projects, just change as neeeded. + + +script = "TennisForTwo.py" #name of starting .PY +icon_file = "" #ICO file for the .EXE (not working well) +optimize = 2 #0, 1, or 2; like -O and -OO +dos_console = 0 #set to 0 for no dos shell when run +extra_data = ['data'] #extra files/dirs copied to game +extra_modules = ['pygame.locals'] #extra python modules not auto found + + + + + + +#use the default pygame icon, if none given +if not icon_file: + path = os.path.split(pygame.__file__)[0] + icon_file = '"' + os.path.join(path, 'pygame.ico') + '"' +#unfortunately, this cool icon stuff doesn't work in current py2exe :( +#icon_file = '' + +project_name = os.path.splitext(os.path.split(script)[1])[0] + + +#this will create the executable and all dependencies +setup(#name=project_name, + app=[script], + data_files=[("data", glob.glob("data\\*"))] + ) + + +#@-node:jpenner.20050604144534:@thin pygame2app.py +#@-leo diff --git a/pygame2exe.py b/pygame2exe.py new file mode 100644 index 0000000..7bb6155 --- /dev/null +++ b/pygame2exe.py @@ -0,0 +1,40 @@ +#make standalone, needs at least pygame-1.5.3 and py2exe-0.3.1 + +from distutils.core import setup +import sys, os, pygame, shutil, glob +import py2exe + +#setup the project variables here. +#i can't claim these will cover all the cases +#you need, but they seem to work for all my +#projects, just change as neeeded. + + +script = "TennisForTwo.py" #name of starting .PY +icon_file = "" #ICO file for the .EXE (not working well) +optimize = 2 #0, 1, or 2; like -O and -OO +dos_console = 0 #set to 0 for no dos shell when run +extra_data = ['data'] #extra files/dirs copied to game +extra_modules = ['pygame.locals'] #extra python modules not auto found + + + + + + +#use the default pygame icon, if none given +if not icon_file: + path = os.path.split(pygame.__file__)[0] + icon_file = '"' + os.path.join(path, 'pygame.ico') + '"' +#unfortunately, this cool icon stuff doesn't work in current py2exe :( +#icon_file = '' + +project_name = os.path.splitext(os.path.split(script)[1])[0] + + +#this will create the executable and all dependencies +setup(#name=project_name, + windows=[script], + data_files=[("data", glob.glob("data\\*"))] + ) + diff --git a/widgets.py b/widgets.py new file mode 100644 index 0000000..795f6e7 --- /dev/null +++ b/widgets.py @@ -0,0 +1,1113 @@ +#@+leo-ver=4-thin +#@+node:jpenner.20050604112932.1:@thin widgets.py +#@@language python +#@<< widgets declarations >> +#@+node:jpenner.20050604112932.2:<< widgets declarations >> +#!/usr/bin/env python +# +# Widgets.py +# Copyright (C) 2003 Michael Leonhard +# http://tamale.net/ +# +# version 1.0 RC1 (2003/07/06 00:19) + +import string, pygame, pygame.event, pygame.font +from pygame.locals import * + +# initialize modules +pygame.font.init() + +TABTARGET = 1 +BACKWARD = 1 +# keyboard repeat event +KEYREPEAT = NUMEVENTS - 1 +KEYREPEATTIME = 50 #in milliseconds +KEYREPEATWAIT = 10 + +# colors +WHITE = (0xFF, 0xFF, 0xFF, 0xFF) +NAVYBLUE = (0x22, 0x44, 0x66, 0xFF) +BLACK = (0x00, 0x00, 0x00, 0xFF) +DARKGRAY = (0x40, 0x40, 0x40, 0xFF) +GRAY = (0x80, 0x80, 0x80, 0xFF) +RED = (0xFF, 0x00, 0x00, 0xFF) +BLUE = (0x00, 0x00, 0xFF, 0xFF) +GREEN = (0x00, 0xFF, 0x00, 0xFF) +YELLOW = (0x00, 0xFF, 0xFF, 0xFF) + +#@-node:jpenner.20050604112932.2:<< widgets declarations >> +#@nl +#@+others +#@+node:jpenner.20050604112932.3:class WidgetException +class WidgetException( Exception ): + """Widget Exception class""" + #@ @+others + #@+node:jpenner.20050604112932.4:__init__ + def __init__( self, reason ): + """Initialize a new Widget exception""" + self.reason = reason + #@-node:jpenner.20050604112932.4:__init__ + #@+node:jpenner.20050604112932.5:__str__ + def __str__( self ): + """String representation of exception""" + return `self.reason` + #@-node:jpenner.20050604112932.5:__str__ + #@-others +#@-node:jpenner.20050604112932.3:class WidgetException +#@+node:jpenner.20050604112932.6:class WidgetWindow + +class WidgetWindow: + #@ @+others + #@+node:jpenner.20050604112932.7:__init__ + def __init__( self, screen, background = NAVYBLUE ): + """Initialize WidgetWindow class""" + # data + self.screen = screen + self.widgets = [] + self.reversewidgets = [] + self.taborder = [] + self.mousedest = None + self.keydest = None + self.background = background + self.dirtyrect = self.screen.get_rect() + self.focuslosecallback = None + self.lastkeydownevent = None + self.keyrepeatwaitcount = 0 + + # initial blank screen draw + self.eventproc( pygame.event.Event( NOEVENT, {} ) ) + #@-node:jpenner.20050604112932.7:__init__ + #@+node:jpenner.20050604112932.8:addwidget + def addwidget( self, widget, tabable=None ): + """Add a widget to the window""" + self.widgets.append( widget ) + self.reversewidgets.insert( 0, widget ) + self.invalidaterect( widget.rect ) + + # widget is tabable + if tabable: + # add to tab order + self.taborder.append( widget ) + #@-node:jpenner.20050604112932.8:addwidget + #@+node:jpenner.20050604112932.9:prevtab + def prevtab( self, widget ): + """Switch the keyboard focus to the previous widget in the tab order""" + # call nexttab with the BACKWARD option + self.nexttab( widget, BACKWARD ) + #@-node:jpenner.20050604112932.9:prevtab + #@+node:jpenner.20050604112932.10:nexttab + def nexttab( self, widget, backward=None ): + """Switch the keyboard focus to next widget in the tab order""" + + last = len( self.taborder ) - 1 + + # no tabable widgets available + if last < 0: + # error + raise WidgetException( "no tabable widgets available" ) + + # no focus currently + if not self.keydest: + # moving backward + if backward: + # set to last widget in taborder + self.taborder[last].focus() + # moving forward + else: + # set to first widget in taborder + self.taborder[0].focus() + return + + # catch exception + try: + # find index of focused widget + focused = self.taborder.index( self.keydest ) + + # moving backward + if backward: + # next widget + focused -= 1 + # was first widget + if focused < 0: + # use last widget + focused = last + # moving forward + else: + # next widget + focused += 1 + # was last widget + if focused > last: + # use first widget + focused = 0 + + # currently focused widget is not in tab order + except ValueError: + # moving backward + if backward: + # use the last widget in the tab order + focused = last + # moving forward + else: + # use the first widget in the tab order + focused = 0 + + # focus the widget + self.taborder[focused].focus() + #@-node:jpenner.20050604112932.10:nexttab + #@+node:jpenner.20050604112932.11:grabmouse + def grabmouse( self, widget ): + """Cause all mouse messages to be received by specified widget""" + self.mousedest = widget + #@-node:jpenner.20050604112932.11:grabmouse + #@+node:jpenner.20050604112932.12:grabkey + def grabkey( self, widget, focuslosecallback = None, sendrepeats = None ): + """Cause all keyboard messages to be received by specified widget""" + # previous widget requested callback + if self.keydest and self.focuslosecallback: + # inform widget that it is losing key focus + self.focuslosecallback( self.keydest ) + + # widget not specified so unset + if widget == None: + # unregister event + pygame.time.set_timer( KEYREPEAT, 0 ) + # keyboard focus is unset + self.keydest = None + self.focuslosingcallback = None + + # widget is specified + else: + # remember widget and specified callback + self.keydest = widget + self.focuslosecallback = focuslosecallback + + # keyboard repeats were requested + if sendrepeats: + # register regular event for repeats + pygame.time.set_timer( KEYREPEAT, KEYREPEATTIME ) + + # stop current repeat + self.lastkeydownevent = None + self.keyrepeatwaitcount = 0 + #@-node:jpenner.20050604112932.12:grabkey + #@+node:jpenner.20050604112932.13:invalidaterect + def invalidaterect( self, rect=None ): + """Cause the specified region of the screen to be redrawn""" + # no rect supplied + if not rect: + # use entire screen + self.dirtyrect = self.screen.get_rect() + return + + # previous invalid rect exists + if self.dirtyrect: + # combine the two regions + self.dirtyrect.union_ip( rect ) + # this is first invalid rect + else: self.dirtyrect = Rect( rect ) + #@-node:jpenner.20050604112932.13:invalidaterect + #@+node:jpenner.20050604112932.14:dispatch + def dispatch( self, event ): + """Send the event to the widget under the mouse""" + # check each widget, youngest to oldest + for w in self.reversewidgets: + # event occurs in widget's region + if w.rect.collidepoint( event.pos ): + # let the widget process the event + w.eventproc( event ) + # only deliver event to one widget + break + #@-node:jpenner.20050604112932.14:dispatch + #@+node:jpenner.20050604112932.15:eventproc + def eventproc( self, event ): + """Dispatch events to widgets and redraw the screen""" + # mouse events + if event.type == MOUSEMOTION or event.type == MOUSEBUTTONDOWN or event.type == MOUSEBUTTONUP: + # mouse is grabbed + if self.mousedest != None: + self.mousedest.eventproc( event ) + # not grabbed + else: self.dispatch( event ) + + # keyboard events + elif event.type == KEYDOWN or event.type == KEYUP: + # keyboard is grabbed + if self.keydest: + # send the keyboard event to the widget that has keyboard focus + self.keydest.eventproc( event ) + # keydown event + if event.type == KEYDOWN: + # save the event for repeats + self.lastkeydownevent = event + # reset the wait count + self.keyrepeatwaitcount = 0 + # keyup event + else: + # no repeats after keyup + self.lastkeydownevent = None + + # regular event for keyboard repeats + elif event.type == KEYREPEAT: + # keyboard is grabbed + if self.keydest: + # keydown event was saved + if self.lastkeydownevent: + # waited long enough + if self.keyrepeatwaitcount > KEYREPEATWAIT: + # resend the keydown event to the widget + self.keydest.eventproc( self.lastkeydownevent ) + # must wait longer + else: self.keyrepeatwaitcount += 1 + + # there is stuff to be drawn + if self.dirtyrect: + # draw the background of the area to be drawn + self.screen.set_clip( self.dirtyrect ) + self.screen.fill( self.background, self.dirtyrect ) + + # check each widget + for w in self.widgets: + # widget is covered by invalidated rectangle + if self.dirtyrect.colliderect( w.rect ): + # keep the clipping area + self.screen.set_clip( self.dirtyrect ) + + # draw the widget + w.draw( self.screen ) + + # debug clipping regions + #self.screen.set_clip() + #self.screen.fill( (self.debugred, self.debugred, self.debugred, 0x80), self.dirtyrect ) + #self.debugred += 16 + #self.debugred %= 256 + + # screen is clean + self.dirtyrect = None + + # flip the display + pygame.display.update() + #@-node:jpenner.20050604112932.15:eventproc + #@-others +#@-node:jpenner.20050604112932.6:class WidgetWindow +#@+node:jpenner.20050604112932.16:class ButtonClass + +class ButtonClass: + #@ @+others + #@+node:jpenner.20050604112932.17:__init__ + def __init__( self, manager, action, rect, text="OK", foreground=WHITE, shadowcolor=BLACK, textcolor=BLACK, fontsize=24 ): + """Initialize the Button class""" + self.action = action + self.manager = manager + self.rect = Rect( rect ) + self.text = None + self.foreground = foreground + self.shadowcolor = shadowcolor + self.textcolor = textcolor + self.pressed = 0 + self.buttondown = 0 + + # button area + self.button = Rect( self.rect ) + self.button.width -= 5 + self.button.height -= 5 + + # shadow area + self.shadow = Rect( self.button ) + self.shadow.right += 5 + self.shadow.bottom += 5 + + # font + self.font = pygame.font.Font( None, fontsize ) + + # update button text + self.settext( text ) + #@-node:jpenner.20050604112932.17:__init__ + #@+node:jpenner.20050604112932.18:settext + def settext( self, text ): + # new text is same as old + if self.text == text: + # do nothing + return + + # save text for action functions + self.text = text + # remove trailing whitespace + text = text.rstrip() + # if text is empty + if len( text ) == 0: + # use a space + text = " " + + # make text + self.surface = self.font.render( text, 1, self.textcolor, self.foreground ) + # optimize for blitting + self.surface = self.surface.convert() + + # text position (unpressed) + textrect = self.surface.get_rect() + textrect.center = self.button.center + self.textrect = Rect( textrect ) + + # text position (pressed) + textrect.center = self.shadow.center + self.textrectpressed = textrect + + # cause widget to be drawn + self.manager.invalidaterect( self.rect ) + #@-node:jpenner.20050604112932.18:settext + #@+node:jpenner.20050604112932.19:eventproc + def eventproc( self, event ): + """Process mouse events""" + # button is clicked + if event.type == MOUSEBUTTONDOWN and self.button.collidepoint( event.pos): + self.pressed = 1 + self.manager.invalidaterect( self.rect ) + # watch the mouse + self.buttondown = 1 + self.manager.grabmouse( self ) + + # button is released + if event.type == MOUSEBUTTONUP and self.buttondown == 1: + # done watching the mouse + self.buttondown = 0 + self.manager.grabmouse( None ) + + # button was in pressed position + if self.pressed == 1: + self.pressed = 0 + self.manager.invalidaterect( self.rect ) + + # do button action + self.action() + + # mouse cursor moves and holds mouse button + if event.type == MOUSEMOTION and self.buttondown == 1: + # button is in pressed position + if self.pressed == 1: + # cursor is not over the button + if not self.rect.collidepoint( event.pos ): + # button becomes unpressed + self.pressed = 0 + self.manager.invalidaterect( self.rect ) + # button is in unpressed position + else: + # cursor is over the button + if self.button.collidepoint( event.pos ): + # button becomes pressed + self.pressed = 1 + self.manager.invalidaterect( self.rect ) + #@-node:jpenner.20050604112932.19:eventproc + #@+node:jpenner.20050604112932.20:draw + def draw( self, screen ): + """Draw the widget""" + # clip our drawing + screen.set_clip( screen.get_clip().clip( self.rect ) ) + + # button is in pressed state + if self.pressed == 1: + # draw the button + screen.fill( self.foreground, self.shadow ) + # blit the text + screen.blit( self.surface, self.textrectpressed ) + + + # button is in unpressed state + else: + # draw the shadow + screen.fill( self.shadowcolor, self.shadow ) + + # draw the button + screen.fill( self.foreground, self.button ) + + # blit the text + screen.blit( self.surface, self.textrect ) + #@-node:jpenner.20050604112932.20:draw + #@-others +#@-node:jpenner.20050604112932.16:class ButtonClass +#@+node:jpenner.20050604112932.21:class PageClass + +class PageClass: + #@ @+others + #@+node:jpenner.20050604112932.22:eventproc + def eventproc( self, event ): + """Process mouse events""" + # button is clicked + if event.type == MOUSEBUTTONDOWN: + (x, y) = event.pos + # click is on the scroll shuttle + if self.shuttlerect.collidepoint( event.pos ): + # distance from the click to the bottom of the shuttle + self.clickoffset = self.shuttlerect.bottom - y + # begin scrolling + self.manager.grabmouse( self ) + self.scrollgrabbed = 1 + + # button is released + if event.type == MOUSEBUTTONUP: + # currently scrolling + if self.scrollgrabbed == 1: + # final scroll position + self.scroll( event.pos ) + # done scrolling + self.manager.grabmouse( None ) + self.scrollgrabbed = 0 + + # mouse cursor moves + if event.type == MOUSEMOTION: + # scroll bar is grabbed + if self.scrollgrabbed == 1: self.scroll( event.pos ) + #@-node:jpenner.20050604112932.22:eventproc + #@+node:jpenner.20050604112932.23:__init__ + def __init__( self, manager, rect, background=(0, 0, 0, 0), fontsize=24, maxlines=40, foreground=WHITE ): + """Initialize scrollable text display""" + self.manager = manager + self.rect = Rect( rect ) + self.background = background + self.font = pygame.font.Font( None, fontsize ) + self.maxlines = maxlines + self.foreground = foreground + + # Scroll bar + scrollwidth = 10 + self.scrollgrabbed = 0 + + # list of surfaces with line text, youngest is first + self.lines = [] + + # line that is visible at the bottom of page (0 is youngest line) + self.showline = 0 + + # page rect + self.pagerect = Rect( rect ) + self.pagerect.width -= scrollwidth + + # scrollbar area + self.scrollrect = Rect( rect ) + self.scrollrect.width = scrollwidth + self.scrollrect.right = self.rect.right + + # shuttle initially fills scrollbar + self.shuttlerect = Rect( self.scrollrect ) + + # blank line to start with + self.append( " " ) + #@-node:jpenner.20050604112932.23:__init__ + #@+node:jpenner.20050604112932.24:append + def append( self, newtext, foreground=None ): + """Add one or more new lines""" + # use default foreground color + if foreground == None: foreground = self.foreground + + # expand tabs + newtext = newtext.expandtabs() + + # split text into lines + for line in newtext.splitlines(): + # append the line + self.appendline( line, foreground ) + #@-node:jpenner.20050604112932.24:append + #@+node:jpenner.20050604112932.25:fits + def fits( self, text ): + """Check if text fits on one line""" + # size of the text + (fw, fh) = self.font.size( text ) + # too wide + if fw > self.pagerect.width: return 0 + return 1 + #@-node:jpenner.20050604112932.25:fits + #@+node:jpenner.20050604112932.26:appendline + def appendline( self, newtext, foreground ): + """Find and prepare each line of new text""" + # remove trailing whitespace + newtext = newtext.rstrip() + + # find largest piece of text that will fit on one line + pt = len( newtext ) + while not self.fits( newtext[:pt] ): + # find the last space + pt = newtext.rfind( " ", 0, pt ) + # no spaces left + if pt == -1: break + + # first word doesn't fit + if pt == -1: + # break word + pt = len( newtext ) + while not self.fits( newtext[:pt] ): + # not even one letter will fit + if pt == 1: raise Exception + # move the break one letter to the left + pt -= 1 + # found string that fits on line + else: + # include the space + pt += 1 + + # this text fits on the line + thistext = newtext[:pt] + + # check that line is not empty + if len( thistext ) == 0: + # use a space + thistext = " " + + # make a new surface + surface = self.font.render( thistext, 1, foreground, self.background ) + + # optimize for blitting with transparency + surface = surface.convert() + + # all of the lines should be the same height + self.lineheight = surface.get_height() + # add the line surface to the list + self.lines.insert( 0, surface ) + # too many lines + while len( self.lines ) > self.maxlines: + # discard last line + del self.lines[-1] + + # if not scrolled to the bottom + if self.showline != 0: + # keep scroll bar on same line + self.showline += 1 + # if line disappears then stay at the top + if self.showline >= len( self.lines ): self.showline = len( self.lines ) - 1 + + # resize scrollbar + self.scrollcheck() + + # text that wouldn't fit + newtext = newtext[pt:] + + # leftover wrapped text + if len( newtext ) > 0: + # do the leftovers + self.appendline( newtext, foreground ) + # no more lines to be added + else: + # need screen update + self.manager.invalidaterect( self.rect ) + #@-node:jpenner.20050604112932.26:appendline + #@+node:jpenner.20050604112932.27:scrollcheck + def scrollcheck( self ): + """Adjust the scroll shuttle after text is added""" + #print "" + + # How many lines are visible? + visiblelines = self.pagerect.height / self.lineheight + #print "visiblelines", visiblelines + + #print "lines", len( self.lines ) + + # All the lines fit on the page + if len( self.lines ) <= visiblelines: + self.shuttlerect = Rect( self.scrollrect ) + return + + # keep the oldest line at the top or above the page + if self.showline > len( self.lines ) - visiblelines: + self.showline = len( self.lines ) - visiblelines + + # Determine height of the shuttle + self.shuttlerect.height = int( (float( visiblelines ) / float( len( self.lines ) )) * float( self.pagerect.height ) ) + #print "shuttle height", self.shuttlerect.height + + # Number of lines that are off the page (top and bottom) + linetravel = len( self.lines ) - visiblelines + #print "linetravel", linetravel + + # Pixels on the scroll bar representing the off-page lines + travel = self.scrollrect.height - self.shuttlerect.height + #print "travel", travel + + # Distance in scrollbar-pixels of the youngest visible line to the youngest line + x = int( (float( self.showline ) / float( linetravel )) * float( travel ) ) + #print "x", x + + # Move the scroll bar to keep this distance from the bottom + self.shuttlerect.bottom = self.scrollrect.bottom - x + #print "shuttle top", self.shuttlerect.top + + # keep shuttle on scroll bar + self.shuttlerect.clamp_ip( self.scrollrect ) + #print "shuttle", self.shuttlerect + + # will need redraw + self.manager.invalidaterect( self.rect ) + #@-node:jpenner.20050604112932.27:scrollcheck + #@+node:jpenner.20050604112932.28:scroll + def scroll( self, (x, y) ): + """Adjust the scroll shuttle and visible text when the user scrolls""" + #print "" + # shuttle must track mouse + self.shuttlerect.bottom = self.clickoffset + y + # keep shuttle on scroll bar + self.shuttlerect.clamp_ip( self.scrollrect ) + #print "shuttle", self.shuttlerect + + # redraw scroll bar + self.manager.invalidaterect( self.scrollrect ) + + #print "lines", len( self.lines ) + + # Pixels that the scrollbar can slide + travel = self.scrollrect.height - self.shuttlerect.height + #print "travel", travel + # Number of lines that fit on the page + visiblelines = self.pagerect.height / self.lineheight + #print "visiblelines", visiblelines + + # All the lines fit on the page + if len( self.lines ) <= visiblelines: + # Keep the youngest line at the bottom + self.showline = 0 + return + + # How many lines the scrollbar can slide through + linetravel = len( self.lines ) - visiblelines + #print "linetravel", linetravel + + # Distance in pixels of the scroll bar to the top + x = self.shuttlerect.top - self.scrollrect.top + #print "x", x + # Number of oldest lines that should be above the top of the page + l = int( (float( x ) / float( travel )) * float( linetravel ) ) + #print "l", l + + # Remember the current scroll position + oldshowline = self.showline + + # Determine the new scroll position: this is oldest line, + # then down to oldest visible line, then down to youngest visible line + self.showline = len( self.lines ) - l - visiblelines + #print "showline", self.showline + + # text has moved + if oldshowline != self.showline: + # need to redraw text + self.manager.invalidaterect( self.rect ) + #@-node:jpenner.20050604112932.28:scroll + #@+node:jpenner.20050604112932.29:draw + def draw( self, screen ): + """Draw the widget""" + # clip our drawing + screen.set_clip( screen.get_clip().clip( self.rect ) ) + # clear the page area + screen.fill( self.background, self.pagerect ) + + # start at the bottom + y = self.pagerect.bottom + # draw each line + for surface in self.lines[self.showline:]: + # move up one line (upper left corner) + y -= self.lineheight + # blit it to the screen + screen.blit( surface, (self.rect.left, y) ) + # drew the topmost visible line + if y <= self.rect.top: break + + #draw scroll bar + screen.fill( self.background, self.scrollrect ) + screen.fill( self.foreground, self.shuttlerect ) + #@-node:jpenner.20050604112932.29:draw + #@-others +#@-node:jpenner.20050604112932.21:class PageClass +#@+node:jpenner.20050604112932.30:class EditClass + +class EditClass: + #@ @+others + #@+node:jpenner.20050604112932.31:__init__ + def __init__( self, manager, action, rect, initialtext="", fontsize=24, maxchars=60, foreground=WHITE, background=DARKGRAY ): + """Initialize the Edit class""" + rect = Rect( rect ) + self.manager = manager + self.action = action + self.rect = Rect(rect) + self.font = pygame.font.Font( None, fontsize ) + self.maxchars = maxchars + self.foreground = foreground + self.background = background + self.blitoffset = 0 + self.allowed = """`1234567890-= qwertyuiop[]\\asdfghjkl;'zxcvbnm,./ ~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM= self.maxchars: return + + # convert key to character code + try: + character = str( event.unicode ) + + # character is not allowed + if string.find( self.allowed, character ) == -1: return + + # add it to the buffer + self.settext( self.text + character ) + except: return + #@nonl + #@-node:jpenner.20050604112932.32:eventproc + #@+node:jpenner.20050604112932.33:showcaret + def showcaret( self ): + self.caretvisible = 1 + # widest section of text that can be visible + self.renderwidth = self.rect.width - self.caretwidth + # rightmost edge of text, leaving room for caret + self.renderright = self.rect.right - self.caretwidth + # will need redraw + self.manager.invalidaterect( self.rect ) + #@-node:jpenner.20050604112932.33:showcaret + #@+node:jpenner.20050604112932.34:hidecaret + def hidecaret( self ): + self.caretvisible = 0 + # text can now take up whole width of widget + self.renderwidth = self.rect.width + # rightmost edge of text + self.renderright = self.rect.right + # will need redraw + self.manager.invalidaterect( self.rect ) + #@-node:jpenner.20050604112932.34:hidecaret + #@+node:jpenner.20050604112932.35:focus + def focus( self ): + """Grab keyboard focus""" + # already focused + if self.focused: return + + # get keyboard focus from widget window + self.manager.grabkey( self, self.unfocusCALLBACK, 1 ) + + # keyboard focus is gained + self.focused = 1 + # show the caret + self.showcaret() + #@-node:jpenner.20050604112932.35:focus + #@+node:jpenner.20050604112932.36:unfocusCALLBACK + def unfocusCALLBACK( self, widget ): + """Called by manager when keyboard focus is lost""" + + # don't have focus + if not self.focused: return + + # keyboard focus is lost + self.focused = None + # hide the caret + self.hidecaret() + #@-node:jpenner.20050604112932.36:unfocusCALLBACK + #@+node:jpenner.20050604112932.37:settext + def settext( self, newtext ): + """Replace the edit box text""" + + # no change in text + if self.text == newtext: return + + # udpate widget with new text + self.text = newtext + self.maketext() + #@-node:jpenner.20050604112932.37:settext + #@+node:jpenner.20050604112932.38:slidecheck + def slidecheck( self ): + """Adjust the position of the text to keep the last portion visible""" + # we can assume that self.surface != None + + # width of text that we have to display + width = self.surface.get_width() + + # text doesn't fit + if width > self.renderwidth: + # align the right edge + self.blitoffset = self.renderright - width + # text fits + else: + # align the left edge + self.blitoffset = self.rect.left + + # caret position (to the right of text) + self.caretleft = self.blitoffset + width + #@-node:jpenner.20050604112932.38:slidecheck + #@+node:jpenner.20050604112932.39:maketext + def maketext( self ): + """Prepare the new text for display""" + + # will need redraw + self.manager.invalidaterect( self.rect ) + + # text is empty + if not len( self.text ): + # clear the surface + self.surface = None + # caret at far left + self.caretleft = self.rect.left + return + + # make a new surface + self.surface = self.font.render( self.text, 1, self.foreground, self.background ) + + # text has transparent background + if self.background == None: + # optimize for blitting with transparency + self.surface = self.surface.convert_alpha() + # opaque background + else: + # optimize for normal blitting + self.surface = self.surface.convert() + + # adjust the new text position horizontally + self.slidecheck() + #@-node:jpenner.20050604112932.39:maketext + #@+node:jpenner.20050604112932.40:draw + def draw( self, screen ): + """Draw the widget""" + # clip our drawing + screen.set_clip( screen.get_clip().clip( self.rect ) ) + + # widget is not transparent + if self.background: + # clear the area + screen.fill( self.background, self.rect ) + + # not empty + if self.surface: + # blit it to the screen + screen.blit( self.surface, (self.blitoffset, self.rect.top) ) + + # caret is visible + if self.caretvisible: + # blit the caret + screen.blit( self.caret, (self.caretleft, self.rect.top ) ) + #@-node:jpenner.20050604112932.40:draw + #@-others +#@-node:jpenner.20050604112932.30:class EditClass +#@+node:jpenner.20050604112932.41:class TextClass + +class TextClass: + #@ @+others + #@+node:jpenner.20050604112932.42:eventproc + def eventproc( self, event ): return + #@-node:jpenner.20050604112932.42:eventproc + #@+node:jpenner.20050604112932.43:__init__ + def __init__( self, manager, rect, text="", fontsize=24, foreground=WHITE, background=None ): + """Initialize the Text class""" + rect = Rect( rect ) + self.manager = manager + self.text = text + self.rect = Rect(rect) + self.foreground = foreground + self.background = background + self.font = pygame.font.Font( None, fontsize ) + self.settext( text ) + #@-node:jpenner.20050604112932.43:__init__ + #@+node:jpenner.20050604112932.44:settext + def settext( self, newtext ): + """Replace the text in the widget""" + # expand tabs + newtext = newtext.expandtabs() + # remove trailing whitespace + self.text = newtext.rstrip() + # update surface + self.maketext() + #@-node:jpenner.20050604112932.44:settext + #@+node:jpenner.20050604112932.45:maketext + def maketext( self ): + """Prepare the text for display""" + # if text is empty + if len( self.text ) == 0: + # use a space + self.text = " " + # transparent background + if self.background == None: + # make text + self.surface = self.font.render( self.text, 1, self.foreground ) + # optimize for blitting with transparency + self.surface = self.surface.convert_alpha() + # opaque background + else: + # make text + self.surface = self.font.render( self.text, 1, self.foreground, self.background ) + # optimize for blitting + self.surface = self.surface.convert() + # need screen update + self.manager.invalidaterect( self.rect ) + #@-node:jpenner.20050604112932.45:maketext + #@+node:jpenner.20050604112932.46:draw + def draw( self, screen ): + """Draw the widget""" + # clip our drawing + screen.set_clip( screen.get_clip().clip( self.rect ) ) + # background is set + if self.background != None: + # fill background + screen.fill( self.background, self.rect ) + # blit text to the screen + screen.blit( self.surface, self.rect ) + #@-node:jpenner.20050604112932.46:draw + #@-others +#@-node:jpenner.20050604112932.41:class TextClass +#@+node:jpenner.20050604112932.47:class MultiLineTextClass + +class MultiLineTextClass: + #@ @+others + #@+node:jpenner.20050604112932.48:eventproc + def eventproc( self, event ): return + #@-node:jpenner.20050604112932.48:eventproc + #@+node:jpenner.20050604112932.49:__init__ + def __init__( self, manager, rect, text="", fontsize=24, foreground=WHITE, background=None ): + """Initialize the MultLineText class""" + rect = Rect( rect ) + self.manager = manager + self.lines = [] + self.rect = Rect(rect) + self.foreground = foreground + self.background = background + self.font = pygame.font.Font( None, fontsize ) + self.settext( text ) + #@-node:jpenner.20050604112932.49:__init__ + #@+node:jpenner.20050604112932.50:settext + def settext( self, newtext ): + """Replace the text of the widget""" + # expand tabs + newtext = newtext.expandtabs() + # append each line separately + for line in newtext.splitlines(): + self.appendline( line ) + #@-node:jpenner.20050604112932.50:settext + #@+node:jpenner.20050604112932.51:fits + def fits( self, text ): + """Determine if the text fits on one line""" + # size of the text + (fw, fh) = self.font.size( text ) + # too wide + if fw > self.rect.width: return 0 + return 1 + #@-node:jpenner.20050604112932.51:fits + #@+node:jpenner.20050604112932.52:appendline + def appendline( self, newtext ): + """Prepare the lines of new text for display""" + # remove trailing whitespace + newtext = newtext.rstrip() + + # find largest piece of text that will fit on one line + pt = len( newtext ) + while not self.fits( newtext[:pt] ): + # find the last space + pt = newtext.rfind( " ", 0, pt ) + # no spaces left + if pt == -1: break + + # first word doesn't fit + if pt == -1: + # break word + pt = len( newtext ) + while not self.fits( newtext[:pt] ): + # not even one letter will fit + if pt == 1: raise Exception + # move the break one letter to the left + pt -= 1 + # found string that fits on line + else: + # include the space + pt += 1 + + # line that will fit + thisline = newtext[:pt] + + # check that line is not empty + if len( thisline ) == 0: + # use a space + thisline = " " + + # transparent background + if self.background == None: + # make text + surface = self.font.render( thisline, 1, self.foreground ) + # optimize for blitting with transparency + surface = surface.convert_alpha() + # opaque background + else: + # make text + surface = self.font.render( thisline, 1, self.foreground, self.background ) + # optimize for blitting + surface = surface.convert() + + self.lineheight = surface.get_height() + self.lines.append( surface ) + + # text that wouldn't fit + newtext = newtext[pt:] + + # remove trailing whitespace + #newtext = newtext.rstrip() + + # leftover wrapped text + if len( newtext ) > 0: + # make lines out of it + self.appendline( newtext ) + # last line to be appended + else: + # need screen update + self.manager.invalidaterect( self.rect ) + #@-node:jpenner.20050604112932.52:appendline + #@+node:jpenner.20050604112932.53:draw + def draw( self, screen ): + """Draw the widget""" + # clip our drawing + screen.set_clip( screen.get_clip().clip( self.rect ) ) + # background is set + if self.background != None: + # fill background + screen.fill( self.background, self.rect ) + # start at the top + y = self.rect.top + # draw each line + for surface in self.lines: + # blit it to the screen + screen.blit( surface, (self.rect.left, y) ) + # next line is lower + y += self.lineheight + #@-node:jpenner.20050604112932.53:draw + #@-others +#@-node:jpenner.20050604112932.47:class MultiLineTextClass +#@-others +#@-node:jpenner.20050604112932.1:@thin widgets.py +#@-leo diff --git a/widgetsdemo.py b/widgetsdemo.py new file mode 100644 index 0000000..33351f2 --- /dev/null +++ b/widgetsdemo.py @@ -0,0 +1,131 @@ +#@+leo-ver=4-thin +#@+node:jpenner.20050604113053:@thin widgetsdemo.py +#@@language python +#@<< widgetsdemo declarations >> +#@+node:jpenner.20050604113053.1:<< widgetsdemo declarations >> +#!/usr/bin/env python +# +# WidgetsDemo.py demonstrates the usage Widgets.py +# Copyright (C) 2003 Michael Leonhard +# http://tamale.net/ +# +# version 1.0 RC1 (2003/07/06 00:42) + +import pygame, pygame.display, pygame.event, pygame.time, pygame.font, pygame.draw +from pygame.locals import * +import math, string +from widgets import * + +#@-node:jpenner.20050604113053.1:<< widgetsdemo declarations >> +#@nl +#@+others +#@+node:jpenner.20050604113053.2:class WidgetsDemo +class WidgetsDemo( WidgetWindow ): + #@ @+others + #@+node:jpenner.20050604113053.3:__init__ + def __init__( self ): + """Initialize Demo Class""" + # instead of pygame.init(), initialize modules manually to + # avoid initing pygame.sound + pygame.display.init() + pygame.font.init() + + # setup screen + screen = pygame.display.set_mode( (640, 480), DOUBLEBUF ) + pygame.display.set_caption( 'WidgetsDemo' ) + + # request regular event for updating animation + self.DRAWEVENT = USEREVENT + 1 + pygame.time.set_timer( self.DRAWEVENT, 33 ) #33 == 30fps + + # filter events + badevents = [NOEVENT, ACTIVEEVENT, JOYAXISMOTION, JOYBALLMOTION, JOYHATMOTION, JOYBUTTONDOWN ,JOYBUTTONUP, VIDEORESIZE, SYSWMEVENT, NUMEVENTS] + goodevents = [self.DRAWEVENT, KEYDOWN, KEYUP, MOUSEMOTION, MOUSEBUTTONDOWN, MOUSEBUTTONUP, QUIT ] + pygame.event.set_blocked( badevents ) + + # initialize the WidgetWindow base class + WidgetWindow.__init__( self, screen ) + + # create special widgets + edit = EditClass( self, self.editaction, (325, 300, 265, 25), "text" ) + self.page = PageClass( self, (325, 25, 265, 275), GRAY ) + + # put the widgets in the window + self.addwidget( MultiLineTextClass( self, (25, 25, 250, 145), "MultiLineTextClass\n(transparent)\nHas automatic word wrapping.", 36 ) ) + self.addwidget( MultiLineTextClass( self, (25, 195, 250, 145), "MultiLineTextClass\n(with background)\nHas automatic word wrapping.", 36, WHITE, BLACK ) ) + self.addwidget( EditClass( self, self.editaction, (25, 350, 150, 30), "second edit box" ), TABTARGET ) + self.addwidget( TextClass( self, (25, 400, 590, 19), "TextClass (transparent)", 20 ) ) + self.addwidget( TextClass( self, (25, 440, 590, 19), "TextClass (with background)", 20, WHITE, BLACK ) ) + self.addwidget( self.page ) + self.addwidget( edit, TABTARGET ) + self.addwidget( ButtonClass( self, self.buttonaction, (205, 110, 200, 105), "This button overlaps" ) ) + + # set keyboard focus to the edit widget + edit.focus() + + # initial screen draw + self.eventproc( pygame.event.Event( NOEVENT, {} ) ) + + # animation data + self.animrect = Rect( (325, 350, 265, 50) ) + self.animline = Rect( self.animrect ) + self.animline.width = 5 + self.animline.left = self.animrect.left + #@-node:jpenner.20050604113053.3:__init__ + #@+node:jpenner.20050604113053.4:loop + def loop( self ): + """Program loop""" + while 1: + # get the next event + event = pygame.event.wait() + # Redraw the animation + if event.type == self.DRAWEVENT: + # get all pending DRAWEVENTs + pending = pygame.event.get( self.DRAWEVENT ) + # number of events we are doing + num = len( pending ) + 1 + + # reset the clipping region + self.screen.set_clip() + # erase the animation + self.screen.fill( self.background, self.animline ) + + # update the animation + self.animline.left += 1 * num + # line is outside the region + while not self.animline.colliderect( self.animrect ): + # move it back to the left + self.animline.left -= self.animrect.width + # draw the animation + self.screen.fill( RED, self.animline ) + # flip the display + pygame.display.update() + + # user pressed the X button to close the window + elif event.type == QUIT: break + # pass the even to the widgets + else: self.eventproc( event ) + + #clean up + pygame.time.set_timer( self.DRAWEVENT, 0 ) + pygame.quit() + #@-node:jpenner.20050604113053.4:loop + #@+node:jpenner.20050604113053.5:editaction + def editaction( self, widget ): + """Function called when enter key is pressed""" + self.page.append( widget.text ) + widget.settext( "" ) + #@-node:jpenner.20050604113053.5:editaction + #@+node:jpenner.20050604113053.6:buttonaction + def buttonaction( self ): + """Function called when button is pressed""" + self.page.append( "ACTION" ) + #@-node:jpenner.20050604113053.6:buttonaction + #@-others +#@-node:jpenner.20050604113053.2:class WidgetsDemo +#@-others + +demo = WidgetsDemo() +demo.loop() +#@-node:jpenner.20050604113053:@thin widgetsdemo.py +#@-leo