from pandac.PandaModules import NodePath, DirectionalLight, AmbientLight, VBase4, Vec4, Vec3
from pandac.PandaModules import OdeWorld, OdeBody, OdeMass, Quat, OdePlaneGeom, BitMask32
from pandac.PandaModules import OdeSimpleSpace, OdeJointGroup, OdeBoxGeom
from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from direct.gui.DirectGui import *
from pandac.PandaModules import TextNode
from direct.task import Task
import random, sys, time

"""
* Dice in a Stone Box
*
* This is a small game based on Panda3D, a free python 3D engine available at http://www.panda3d.org/
* I'm not sure if this is the 'best' python 3D engine around, but it has quite a few features and is
* used by Disney for some of their games so it fares pretty well.
*
* This has to be one of the more entertaining projects I've worked on in my spare time since I can remember. 
* Some of the concepts started from work related to my tenure at VGT, but for the most part, this is all home brew.
*
* Anyhow, DSB was coded in various incarnations over about three or four months, this is the final version, or
* more correctly, where I stopped. It could use some enhancements and probably has a few bugs but was
* way fun and time for new things, right?
*
* I wanted to do a very small but complete game and use the whole tool chain. I did everything, including
* modeling the die in SoftImageMod Tool, and it was ridiculously fun.
*
* Since this about the seventh or so refactor/plunder, it came out reasonably small for the amount of 3D
* you get out of it. It is one big class and somewhat brute force but I tried for some elegance as well.
*
* Basically, this game runs as a series of tasks, all handled by the Panda3D task manager.  The __init__
* section loads the models, sets up the physics engine and then starts the tasks running. Some of the tasks
* just run, others turn individual tasks on or off.  The keys are mapped to individual routines with only the
* space bar actually kicking off any task.  The other keys simply increment the bet and credits amount.
*
* The winning combos are hard coded, that would be one thing that needs an enhancement. Also, the 'fun' content
* of this game is somewhat questionable, it's amusing to watch the physics run, but it was probably way more
* fun to write than it is to play.  Nevertheless, it does have small examples of things that were really a bear
* to track down or discover so I wanted to post this source up and make it available to others.
*
* So, this code is free, blah, blah, if it wipes your hard drive or gives you seizures or you just plain
* don't like it, sorry about that ;)
*
* Martan 
* martan@cstone.net
*
"""


FRAMESKIP = 4

# Panda Render Window - Main Game Class
class PandaApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        # global things to do
        base.disableMouse()     # turn off internal camera control
        base.setBackgroundColor(0.2, 0.2, 0.2) # not that we care really, can't see it

        # std random startup stuff
        random.seed()

        # main physics world lives here, set the gravity to earth std
        self.myWorld = OdeWorld()
        self.myWorld.setGravity(0, 0, -9.81)

        # Create a space and add a contactgroup for our physics system
        self.space = OdeSimpleSpace()
        self.space.enable()
        self.space.setAutoCollideWorld(self.myWorld)
        self.contactgroup = OdeJointGroup()
        self.space.setAutoCollideJointGroup(self.contactgroup)

        # The surface table is needed for autoCollide
        self.myWorld.initSurfaceTable(1)
        self.myWorld.setSurfaceEntry(0, 0, 20, 3.8, 1.1, 0.9, 0.00001, 0.0, 0.002)

        # Load Sounds (notes) for dice models
        self.loadSounds()

        # load dice models and set them for dynamics
        self.LoadModels()

        # load the room we bounce in, set the physics floor and walls
        self.LoadRoom()

        # setup global collision system callback
        self.space.setCollisionEvent("ode-collision")
        base.accept("ode-collision", self.onCollision)

        # add a directional light
        dlight = DirectionalLight('dlight')
        dlight.setColor(VBase4(0.9, 0.9, 0.9, 1))
        dlnp = render.attachNewNode(dlight)
        dlnp.setHpr(0, -60, 0)
        render.setLight(dlnp)

        # and an ambient light too
        alight = AmbientLight('alight')
        alight.setColor(VBase4(0.15, 0.15, 0.15, 1))
        alnp = render.attachNewNode(alight)
        render.setLight(alnp)

        # camera coords
        self.cameraX = 0
        self.cameraY = -25
        self.cameraZ = 11
        self.cameraH = 0
        self.cameraP = -16
        self.cameraR = 0

        # fix the camera here
        self.camera.setPos(self.cameraX, self.cameraY, self.cameraZ)
        self.camera.setHpr(self.cameraH, self.cameraP, self.cameraR)

        # add keyboard controls
        self.accept('space', self.throwDice)
        self.accept('escape', sys.exit)
        self.accept('c', self.addCredits)
        self.accept('b', self.addBet)

        # setup some flags and things
        self.frame = 0
        self.computedWin = True
        self.addForce = False
        self.runSimulation = False
        self.inSimulation = False
        self.rotationRunning = False
        self.collision = False
        self.winlist = {}
        self.positionlist = {}
        self.prevlist = {}

        # money and such
        self.credits = 10
        self.bet = 1
        self.winnings = 0

        # wins per face
        self.ones = 0
        self.twos = 0
        self.threes = 0
        self.fours = 0
        self.fives = 0
        self.sixes = 0

        # setup the screen text displays and cute little dice button
        self.drawScreenText()

        # start everything and we are done with initialize
        taskMgr.doMethodLater(0.1, self.startRot, "StartRotation")
        taskMgr.doMethodLater(0.7, self.startGrav, "StartGravity")

    # Load the sounds, these are notes, simple blues scale, sampled from FL studio
    def loadSounds(self):
        f0 = loader.loadSfx("models/f.wav")
        g0 = loader.loadSfx("models/g.wav")
        b0 = loader.loadSfx("models/bb.wav")
        c0 = loader.loadSfx("models/c.wav")
        e0 = loader.loadSfx("models/eb.wav")
        f1 = loader.loadSfx("models/f1.wav")
        g1 = loader.loadSfx("models/g1.wav")
        b0 = loader.loadSfx("models/bb1.wav")
        c1 = loader.loadSfx("models/c1.wav")
        self.sounds = [f0, g0, c0, f1, g1, c1, f0, g0, c1 ]
        self.soundIndex = 0

    # Text on the Screen
    def drawScreenText(self):
        text = ""
        self.textObject = OnscreenText(text=text, \
                                       pos=(-1.05,-0.95), \
                                       scale = 0.10, \
                                       fg=(1, 1, 1, 1), \
                                       align=TextNode.ALeft, \
                                       mayChange=1)
        text = "Credits:"
        self.CreditsText = OnscreenText(text=text, \
                                        pos=(-1.3, 0.9), \
                                        scale = 0.1, \
                                        fg=(1, 1, 1, 1), \
                                        align=TextNode.ALeft, \
                                        mayChange=1)
        text = "%d" % self.credits
        self.CreditsNum = OnscreenText(text=text, \
                                        pos=(-0.9, 0.9), \
                                        scale = 0.13, \
                                        fg=(1, 1, 1, 1), \
                                        align=TextNode.ALeft, \
                                        mayChange=1)
        text = "Bet:"
        self.BetText = OnscreenText(text=text, \
                                        pos=(.94, 0.9), \
                                        scale = 0.1, \
                                        fg=(1, 1, 1, 1), \
                                        align=TextNode.ALeft, \
                                        mayChange=1)
        text = "%d" % self.bet
        self.BetNum = OnscreenText(text=text, \
                                        pos=(1.14, 0.9), \
                                        scale = 0.13, \
                                        fg=(1, 1, 1, 1), \
                                        align=TextNode.ALeft, \
                                        mayChange=1)
        text = "Win: 0"
        self.textWin = OnscreenText(text=text, \
                                       pos=(.94,-0.95), \
                                       scale = 0.10, \
                                       fg=(1, 1, 1, 1), \
                                       align=TextNode.ALeft, \
                                       mayChange=1)
        text = "Idle"
        self.textStatus = OnscreenText(text=text, \
                                       pos=(.94,-0.85), \
                                       scale = 0.04, \
                                       fg=(1, 1, 1, 1), \
                                       align=TextNode.ALeft, \
                                       mayChange=1)

        # Add button - this is the 3D die button in the lower left corner
        buttonText = ("", "", "Throw", "")
        self.bDie = DirectButton(text = buttonText, \
                    geom = self.displayDie, \
                    geom_pos = (0,0,0), \
                    geom_scale = (0.1, 0.1, 0.1), \
                    geom_hpr = (0, 0, 0), \
                    relief = None, \
                    text_fg = (1, 1, 1, 1), \
                    text_scale = (0.3, 0.3), \
                    text_shadow = (0, 0, 0, 1), \
                    text_style = ScreenTitle, \
                    frameColor = (1, 1, 1, 1), \
                    borderWidth = (0.05, 0.05), \
                    #image = 'symbols\symbol_1.png', \
                    pos = (-1.2, 0, -.90), \
                    scale = 0.3, \
                    hpr = (0,0,0), \
                    command=self.throwDice)

    #start up the physics task
    def startGrav(self, task):
        self.startGravitySim()

    # spin the dice weirdly rotate task
    def startRot(self, task):
        self.startRotation()

    # add some credits to our pot
    def addCredits(self):
        self.credits = self.credits + 1
        s = "%d" % self.credits
        self.CreditsNum.setText(s)

    # bet some credits on this spin
    def addBet(self):
        self.bet = self.bet + 1
        if self.bet > 5:
           self.bet = 1
        s = "%d" % self.bet
        self.BetNum.setText(s)

    # spin the models randomly - this is what you see when the game starts
    def rotateModels(self, task):
        self.frame = self.frame + 1
        if self.frame >= FRAMESKIP:
           self.frame = 0
           for die in self.dice:
               x = random.randint(0,359)
               y = random.randint(0,359)
               z = random.randint(0,359)
               die[0].setHpr(x,y,z)
               die[3].setPosition(die[0].getPos(render))
               die[3].setQuaternion(die[0].getQuat(render))
        if self.collision == False:
           return Task.cont
        else:
           return Task.done

    # start the dice rotating - this adds the task above to the task Manager
    def startRotation(self):
        if self.rotationRunning == False:
           self.rotationRunning = True
           self.inSimulation = False
           self.taskMgr.add(self.rotateModels, "rotateModels")

    # force (explode) the dice - this is the task that happens when you hit the space bar
    def forceDie(self):
        for i in range(0,9):
            body = self.dice[i][3]
            a = random.randint(-17000000,17000000)
            b = random.randint(-17000000,17000000)
            c = random.randint(10000000,33000000) # up is always big to overcome gravity
            body.setForce(Vec3(a, b, c))

    # do everything to make a throw happen
    def throwDice(self):
        if self.credits - self.bet < 0:
           return

        if self.computedWin == False:
           return

        self.computedWin = False

        self.textStatus.setText("Throw Dice")
        #print "THROW DICE"
        self.credits = self.credits - self.bet
        s = "%d" % self.credits
        self.CreditsNum.setText(s)

        self.winnings = 0
        w = "Win: %d" % self.winnings
        self.textWin.setText(w)

        for i in range(0,9):
            self.prevlist[i] = (0.0, 0.0, 0.0)
        self.moving = 0
        self.forceDie()
        taskMgr.doMethodLater(0.2, self.evaluateWinList, "EvaluateWinList")

    # This is called to see if the dice have stopped moving yet so we can compute the win
    def checkMotion(self):
        moving = False
        for d in self.winlist.keys():
            (l,m,n) = self.winlist[d]
            a = "%03.0f" % l
            b = "%03.0f" % m
            c = "%03.0f" % n

            (o,p,q) = self.prevlist[d]
            e = "%03.0f" % o
            f = "%03.0f" % p
            g = "%03.0f" % q

            if a != e:
               moving = True
            if b != f:
               moving = True
            if c != g:
               moving = True

        for d in self.winlist.keys():
            self.prevlist[d] = self.winlist[d]

        return moving

    # evaluate (count up faces) dice as they come to rest on the floor, a task that runs every frame
    def evaluateWinList(self, task):

        if self.checkMotion() == False:
           self.moving = self.moving + 1
        else:
           self.moving = 0

        ones = 0
        twos = 0
        threes = 0
        fours = 0
        fives = 0
        sixes = 0

        a = b = c = 0

        for d in self.winlist.keys():
            l, m, n = self.winlist[d]
            a = int(l+0.5)
            b = int(m+0.5)
            c = int(n+0.5)
            if b == 0 and c == -89:
               ones = ones + 1
            elif b == 0 and c == 0:
               twos = twos + 1
            elif b == 90:
               threes = threes + 1
            elif b < -88:
               fours = fours + 1
            elif b == 0 and c != 90:
               fives = fives + 1
            elif b == 0 and c == 90:
               sixes = sixes + 1

        if self.ones == ones and self.twos == twos and self.threes == threes \
           and self.fours == fours and self.fives == fives and self.sixes == sixes:

           t = ""
           if ones > 3:
              t = t + "%d ones " % ones
           if twos > 3:
              t = t + "%d twos " % twos
           if threes > 3:
              t = t + "%d threes " % threes
           if fours > 3:
              t = t + "%d fours " % fours
           if fives > 3:
              t = t + "%d fives " % fives
           if sixes > 3:
              t = t + "%d sixes " % sixes
           if ones > 0 and twos > 0 and threes > 0 and fours > 0 and fives > 0 and sixes > 0:
              t = t + "six sequence"
           self.textObject.setText(t)

        else:
            self.ones = ones
            self.twos = twos
            self.threes = threes
            self.fours = fours
            self.fives = fives
            self.sixes = sixes

        self.bDie["geom_hpr"] = (a,b,c)
        if self.moving > 5:
           taskMgr.doMethodLater(0.1, self.computeTotals, "compute total wins")
           return Task.done
        return Task.cont

    # Compute the totals from the faces that are up, tweak this to make the game pay off easier
    def computeTotals(self, task):
        self.textStatus.setText("Compute win")
        self.winnings = 0

        # five of a kind or greater pays bet * number of a kind
        if self.ones > 3:
           self.winnings = self.winnings + (self.ones * self.bet)

        if self.twos > 3:
           self.winnings = self.winnings + self.twos * self.bet

        if self.threes > 3:
           self.winnings = self.winnings + (self.threes * self.bet)

        if self.fours > 3:
           self.winnings = self.winnings + self.fours * self.bet

        if self.fives > 3:
           self.winnings = self.winnings + self.fives * self.bet

        if self.sixes > 3:
           self.winnings = self.winnings + self.sixes * self.bet

        if self.ones > 0 and self.twos > 0 and self.threes > 0 and self.fours > 0 and self.fives > 0 and self.sixes > 0:
           self.winnings = self.winnings + (2 * self.bet)

        w = "Win: %d" % self.winnings
        self.textWin.setText(w)

        self.credits = self.credits + self.winnings
        s = "%d" % self.credits
        self.CreditsNum.setText(s)

        self.computedWin = True

        return Task.done

    # start the gravity/collision simulation
    def startGravitySim(self):
        self.addForce = False
        if self.runSimulation == False:
           self.deltaTimeAccumulator = 0.0
           self.stepSize = 1.0 / 90.0
           self.runSimulation = True
           taskMgr.doMethodLater(0.1, self.simulationTask, "Physics Simulation")

    # callback for collisions of the dice
    def onCollision(self, entry):
        geom1 = entry.getGeom1()
        geom2 = entry.getGeom2()
        body1 = entry.getBody1()
        body2 = entry.getBody2()

        for np, geom, sound, body in self.dice:
            if geom == geom1 or geom == geom2:
               velocity = body1.getAngularVel()
               if velocity[0] > 2.0 and sound.status != sound.PLAYING:
                  sound.setVolume(velocity[0] / 80.0)
                  sound.play()
                  self.collision = True

    # run the gravity/collision simulation task
    def simulationTask(self, task):
        self.inSimulation = True
        self.space.autoCollide()
        #self.myWorld.quickStep(globalClock.getDt())
        self.myWorld.quickStep(0.016)
        i = 0
        for np, geom, sound, body in self.dice:
            self.positionlist[i] = np.getPos()
            self.winlist[i] = np.getHpr()
            i = i + 1
            if not np.isEmpty():
               np.setPosQuat(render, geom.getBody().getPosition(), Quat(geom.getBody().getQuaternion()))
               self.contactgroup.empty() # Clear the contact joints
        if self.runSimulation == True:
           return task.cont

    # set already existing models to standard places
    def setModels(self):
        i = 0
        z = 2.0
        for x in [-1.0, 0.0, 1.0]:
            die = self.dice[i][0]
            die.setPos(x, 0.4, z)
            die.setHpr(0,0,0)
            body = self.dice[i][3]
            body.setPosition(die.getPos(render))
            body.setQuaternion(die.getQuat(render))
            i = i + 1
        z = 3.0
        for x in [-1.0, 0.0, 1.0]:
            die = self.dice[i][0]
            die.setPos(x, 0.0, z)
            die.setHpr(0,0,0)
            body = self.dice[i][3]
            body.setPosition(die.getPos(render))
            body.setQuaternion(die.getQuat(render))
            i = i + 1
        z = 4.0
        for x in [-1.0, 0.0, 1.0]:
            die = self.dice[i][0]
            die.setPos(x, 0.4, z)
            die.setHpr(0,0,0)
            body = self.dice[i][3]
            body.setPosition(die.getPos(render))
            body.setQuaternion(die.getQuat(render))
            i = i + 1

    # build individual die, set mass, collision and physics
    def buildModel(self, x, y, modelDice):
        die = modelDice.copyTo(self.root)
        die.setPos(x, 0.0, y)
        die.setScale(0.25, 0.25, 0.25)
        diceBody = OdeBody(self.myWorld)
        mass = OdeMass()
        wt = random.randint(10000, 14240)
        mass.setBox(wt, 1, 1, 1)
        diceBody.setMass(mass)
        diceBody.setPosition(die.getPos(render))
        diceBody.setQuaternion(die.getQuat(render))
        diceBody.enable()
        diceGeom = OdeBoxGeom(self.space, 1, 1, 1)
        diceGeom.setCollideBits(BitMask32(0x00000001))
        diceGeom.setCategoryBits(BitMask32(0x00000001))
        diceGeom.setBody(diceBody)
        Sound = self.sounds[self.soundIndex]
        self.soundIndex = self.soundIndex + 1
        return die, diceGeom, Sound, diceBody

    # import the dice model base and clone that into nine
    def LoadModels(self):
        self.displayDie = loader.loadModel("models/die")    # for button
        self.displayDie.setDepthTest(True)
        self.displayDie.setDepthWrite(True)

        modelDice = loader.loadModel("models/die")
        self.root = render.attachNewNode("Root")
        self.dice = []

        y = 2.0
        for x in [-1.0, -0.4, 1.0]:
            die, diceGeom, Sound, body = self.buildModel(x, y, modelDice)
            self.dice.append((die, diceGeom, Sound, body))

        y = 3.0
        for x in [-1.0, 0.0, 1.0]:
            die, diceGeom, Sound, body = self.buildModel(x, y, modelDice)
            self.dice.append((die, diceGeom, Sound, body))

        y = 4.0
        for x in [-1.0, 0.4, 1.0]:
            die, diceGeom, Sound, body = self.buildModel(x, y, modelDice)
            self.dice.append((die, diceGeom, Sound, body))

            
    # this loads the room and walls and makes them 'solid' for the dice to bounce off of
    def LoadRoom(self):
        # put dice in a box, model only
        room = loader.loadModel("models/room")
        room.reparentTo(render)
        room.setScale(1, 2, 1)
        room.setPos(0, -1, 0)
        room.setTwoSided(True)

        # install physics - floor
        groundGeom = OdeBoxGeom(self.space, (2000, 2000, 1))
        groundGeom.setCollideBits(BitMask32( 0xffffffff))
        groundGeom.setCategoryBits(BitMask32(0xffffffff))

        # wall left
        wall0Geom = OdeBoxGeom(self.space, (1, 2000, 2000))
        wall0Geom.setPosition(-6.5,0,0)
        wall0Geom.setCollideBits(BitMask32(0xffffffff))
        wall0Geom.setCategoryBits(BitMask32(0xffffffff))

        # wall right
        wall1Geom = OdeBoxGeom(self.space, (1, 2000, 2000))
        wall1Geom.setPosition(6.5,0,0)
        wall1Geom.setCollideBits(BitMask32(0xffffffff))
        wall1Geom.setCategoryBits(BitMask32(0xffffffff))

        # wall back
        wall2Geom = OdeBoxGeom(self.space, (2000, 1, 2000))
        wall2Geom.setPosition(0,6.5,0)
        wall2Geom.setCollideBits(BitMask32(0xffffffff))
        wall2Geom.setCategoryBits(BitMask32(0xffffffff))

        # wall behind cam
        wall3Geom = OdeBoxGeom(self.space, (2000, 1, 2000))
        wall3Geom.setPosition(0,-6.8,0)
        wall3Geom.setCollideBits(BitMask32(0xffffffff))
        wall3Geom.setCategoryBits(BitMask32(0xffffffff))

        # ceiling
        wall4Geom = OdeBoxGeom(self.space, (2000, 2000, 1))
        wall4Geom.setPosition(0,0,11)
        wall4Geom.setCollideBits(BitMask32(0xffffffff))
        wall4Geom.setCategoryBits(BitMask32(0xffffffff))



# MAIN - start everything
panda = PandaApp()
run()