python小游戏

python小游戏_第1张图片

'''
公众号:一行数据,关注领取5T编程资料
'''

import pygame
import sys
import random


class Bird(object):
    """定义一个鸟类"""

    def __init__(self):
        """定义初始化方法"""
        self.birdRect = pygame.Rect(65, 50, 50, 50)  # 鸟的矩形
        # 定义鸟的3种状态列表
        self.birdStatus = [pygame.image.load("assets/1.png"),
                           pygame.image.load("assets/2.png"),
                           pygame.image.load("assets/dead.png")]
        self.status = 0      # 默认飞行状态
        self.birdX = 120     # 鸟所在X轴坐标,即是向右飞行的速度
        self.birdY = 350     # 鸟所在Y轴坐标,即上下飞行高度
        self.jump = False    # 默认情况小鸟自动降落
        self.jumpSpeed = 10  # 跳跃高度
        self.gravity = 5     # 重力
        self.dead = False    # 默认小鸟生命状态为活着

    def birdUpdate(self):
        if self.jump:
            # 小鸟跳跃
            self.jumpSpeed -= 1           # 速度递减,上升越来越慢
            self.birdY -= self.jumpSpeed  # 鸟Y轴坐标减小,小鸟上升
        else:
            # 小鸟坠落
            self.gravity += 0.2           # 重力递增,下降越来越快
            self.birdY += self.gravity    # 鸟Y轴坐标增加,小鸟下降
        self.birdRect[1] = self.birdY     # 更改Y轴位置


class Pipeline(object):
    """定义一个管道类"""

    def __init__(self):
        """定义初始化方法"""
        self.wallx = 400  # 管道所在X轴坐标
        self.pineUp = pygame.image.load("assets/top.png")
        self.pineDown = pygame.image.load("assets/bottom.png")

    def updatePipeline(self):
        """"管道移动方法"""
        self.wallx -= 5  # 管道X轴坐标递减,即管道向左移动
        # 当管道运行到一定位置,即小鸟飞越管道,分数加1,并且重置管道
        if self.wallx < -80:
            global score
            score += 1
            self.wallx = 400


def createMap():
    """定义创建地图的方法"""
    screen.fill((255, 255, 255))     # 填充颜色
    screen.blit(background, (0, 0))  # 填入到背景

    # 显示管道
    screen.blit(Pipeline.pineUp, (Pipeline.wallx, -300))   # 上管道坐标位置
    screen.blit(Pipeline.pineDown, (Pipeline.wallx, 500))  # 下管道坐标位置
    Pipeline.updatePipeline()  # 管道移动

    # 显示小鸟
    if Bird.dead:              # 撞管道状态
        Bird.status = 2
    elif Bird.jump:            # 起飞状态
        Bird.status = 1
    screen.blit(Bird.birdStatus[Bird.status], (Bird.birdX, Bird.birdY))              # 设置小鸟的坐标
    Bird.birdUpdate()          # 鸟移动

    # 显示分数
    screen.blit(font.render('Score:' + str(score), -1, (255, 255, 255)), (100, 50))  # 设置颜色及坐标位置
    pygame.display.update()    # 更新显示


def checkDead():
    # 上方管子的矩形位置
    upRect = pygame.Rect(Pipeline.wallx, -300,
                         Pipeline.pineUp.get_width() - 10,
                         Pipeline.pineUp.get_height())

    # 下方管子的矩形位置
    downRect = pygame.Rect(Pipeline.wallx, 500,
                           Pipeline.pineDown.get_width() - 10,
                           Pipeline.pineDown.get_height())
    # 检测小鸟与上下方管子是否碰撞
    if upRect.colliderect(Bird.birdRect) or downRect.colliderect(Bird.birdRect):
        Bird.dead = True
    # 检测小鸟是否飞出上下边界
    if not 0 < Bird.birdRect[1] < height:
        Bird.dead = True
        return True
    else:
        return False


def getResutl():
    final_text1 = "Game Over"
    final_text2 = "Your final score is:  " + str(score)
    ft1_font = pygame.font.SysFont("Arial", 70)                                      # 设置第一行文字字体
    ft1_surf = font.render(final_text1, 1, (242, 3, 36))                             # 设置第一行文字颜色
    ft2_font = pygame.font.SysFont("Arial", 50)                                      # 设置第二行文字字体
    ft2_surf = font.render(final_text2, 1, (253, 177, 6))                            # 设置第二行文字颜色
    screen.blit(ft1_surf, [screen.get_width() / 2 - ft1_surf.get_width() / 2, 100])  # 设置第一行文字显示位置
    screen.blit(ft2_surf, [screen.get_width() / 2 - ft2_surf.get_width() / 2, 200])  # 设置第二行文字显示位置
    pygame.display.flip()                                                            # 更新整个待显示的Surface对象到屏幕上


if __name__ == '__main__':
    """主程序"""
    pygame.init()                            # 初始化pygame
    pygame.font.init()                       # 初始化字体
    font = pygame.font.SysFont("ziti.ttf", 50)  # 设置字体和大小
    size = width, height = 400, 650          # 设置窗口
    screen = pygame.display.set_mode(size)   # 显示窗口
    clock = pygame.time.Clock()              # 设置时钟
    Pipeline = Pipeline()                    # 实例化管道类
    Bird = Bird()                            # 实例化鸟类
    score = 0
    while True:
        clock.tick(60)                       # 每秒执行60次
        # 轮询事件
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            if (event.type == pygame.KEYDOWN or event.type == pygame.MOUSEBUTTONDOWN) and not Bird.dead:
                Bird.jump = True             # 跳跃
                Bird.gravity = 5             # 重力
                Bird.jumpSpeed = 10          # 跳跃速度

        background = pygame.image.load("assets/background.png")  # 加载背景图片
        if checkDead():                      # 检测小鸟生命状态
            getResutl()                      # 如果小鸟死亡,显示游戏总分数
        else:
            createMap()                      # 创建地图
    pygame.quit()



俄罗斯方块

# -*- coding:utf-8 -*-
'''
公众号:【一行数据】,关注领取5T编程资料
'''
import sys
import random, copy
import pygame as pg
from pygame.locals import *

'''
常量声明
'''
EMPTY_CELL = 0  # 空区标识,表示没有方块
FALLING_BLOCK = 1  # 下落中的方块标识,也就是活动方块。
STATIC_BLOCK = 2  # 固实方块标识

'''
全局变量声明
变量值以sysInit函数中初始化后的结果为准
'''
defaultFont = None  # 默认字体
screen = None  # 屏幕输出对象
backSurface = None  # 图像输出缓冲画板
score = 0  # 玩家得分记录
clearLineScore = 0  # 玩家清除的方块行数
level = 1  # 关卡等级
clock = None  # 游戏时钟
nowBlock = None  # 当前下落中的方块
nextBlock = None  # 下一个将出现的方块
fallSpeed = 10  # 当前方块下落速度
beginFallSpeed = fallSpeed  # 游戏初始时方块下落速度
speedBuff = 0  # 下落速度缓冲变量
keyBuff = None  # 上一次按键记录
maxBlockWidth = 10  # 舞台堆叠区X轴最大可容纳基础方块数
maxBlockHeight = 18  # 舞台堆叠区Y轴最大可容纳基础方块数
blockWidth = 30  # 以像素为单位的基础方块宽度
blockHeight = 30  # 以像素为单位的基础方块高度
blocks = []  # 方块形状矩阵四维列表。第一维为不同的方块形状,第二维为每个方块形状不同的方向(以0下标起始,一共四个方向),第三维为Y轴方块形状占用情况,第四维为X轴方块形状占用情况。矩阵中0表示没有方块,1表示有方块。
stage = []  # 舞台堆叠区矩阵二维列表,第一维为Y轴方块占用情况,第二维为X轴方块占用情况。矩阵中0表示没有方块,1表示有固实方块,2表示有活动方块。
gameOver = False  # 游戏结束标志
pause = False  # 游戏暂停标志


def printTxt(content, x, y, font, screen, color=(255, 255, 255)):
    '''显示文本
    args:
        content:待显示文本内容
        x,y:显示坐标
        font:字体
        screen:输出的screen
        color:颜色
    '''
    imgTxt = font.render(content, True, color)
    screen.blit(imgTxt, (x, y))


class point(object):
    '''平面坐标点类
    attributes:
        x,y:坐标值
    '''

    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    def getx(self):
        return self.__x

    def setx(self, x):
        self.__x = x

    x = property(getx, setx)

    def gety(self):
        return self.__y

    def sety(self, y):
        self.__y = y

    y = property(gety, sety)

    def __str__(self):
        return "{x:" + "{:.0f}".format(self.__x) + ",y:" + "{:.0f}".format(self.__y) + "}"


class blockSprite(object):
    '''
    方块形状精灵类
    下落方块的定义全靠它了。
    attributes:
        shape:方块形状编号
        direction:方块方向编号
        xy,方块形状左上角方块坐标
        block:方块形状矩阵
    '''

    def __init__(self, shape, direction, xy):
        self.shape = shape
        self.direction = direction
        self.xy = xy

    def chgDirection(self, direction):
        '''
        改变方块的方向
        args:
            direction:1为向右转,0为向左转。
        '''
        dirNumb = len(blocks[self.shape]) - 1
        if direction == 1:
            self.direction += 1
            if self.direction > dirNumb:
                self.direction = 0
        else:
            self.direction -= 1
            if self.direction < 0:
                self.direction = dirNumb

    def clone(self):
        '''
        克隆本体
        return:
            返回自身的克隆
        '''
        return blockSprite(self.shape, self.direction, point(self.xy.x, self.xy.y))

    def _getBlock(self):
        return blocks[self.shape][self.direction]

    block = property(_getBlock)


def getConf(fileName):
    '''
    从配置文件中读取方块形状数据
    每个方块以4*4矩阵表示形状,配置文件每行代表一个方块,用分号分隔矩阵行,用逗号分隔矩阵列,0表示没有方块,1表示有方块。
    因为此程序只针对俄罗斯方块的经典版,所以方块矩阵大小以硬编码的形式写死为4*4。
    args:
        fileName:配置文件名
    '''
    global blocks  # blocks记录方块形状。
    with open(fileName, 'rt') as fp:
        for temp in fp.readlines():
            blocks.append([])
            blocksNumb = len(blocks) - 1
            blocks[blocksNumb] = []
            # 每种方块形状有四个方向,以0~3表示。配置文件中只记录一个方向形状,另外三个方向的矩阵排列在sysInit中通过调用transform计算出来。
            blocks[blocksNumb].append([])
            row = temp.split(";")
            for r in range(len(row)):
                col = []
                ct = row[r].split(",")
                # 对矩阵列数据做规整,首先将非“1”的值全修正成“0”以过滤空字串或回车符。
                for c in range(len(ct)):
                    if ct[c] != "1":
                        col.append(0)
                    else:
                        col.append(1)
                # 将不足4列的矩阵通过补“0”的方式,补足4列。
                for c in range(len(ct) - 1, 3):
                    col.append(0)
                blocks[blocksNumb][0].append(col)
            # 如果矩阵某行没有方块,则配置文件中可以省略此行,程序会在末尾补上空行数据。
            for r in range(len(row) - 1, 3):
                blocks[blocksNumb][0].append([0, 0, 0, 0])
            blocks[blocksNumb][0] = formatBlock(blocks[blocksNumb][0])


def sysInit():
    '''
    系统初始化
    包括pygame环境初始化,全局变量赋值,生成每个方块形状的四个方向矩阵。
    '''
    global defaultFont, screen, backSurface, clock, blocks, stage, gameOver, fallSpeed, beginFallSpeed, nowBlock, nextBlock, score, level, clearLineScore, pause

    # pygame运行环境初始化
    pg.init()
    screen = pg.display.set_mode((500, 550))
    backSurface = pg.Surface((screen.get_rect().width, screen.get_rect().height))
    pg.display.set_caption("block")
    clock = pg.time.Clock()
    pg.mouse.set_visible(False)

    # 游戏全局变量初始化
    defaultFont = pg.font.Font(None, 16)  # yh.ttf这个字体文件请自行上网搜索下载,如果找不到就随便用个ttf格式字体文件替换一下。
    nowBlock = None
    nextBlock = None
    gameOver = False
    pause = False
    score = 0
    level = 1
    clearLineScore = 0
    beginFallSpeed = 20
    fallSpeed = beginFallSpeed - level * 2

    # 初始化游戏舞台
    stage = []
    for y in range(maxBlockHeight):
        stage.append([])
        for x in range(maxBlockWidth):
            stage[y].append(EMPTY_CELL)

    # 生成每个方块形状4个方向的矩阵数据
    for x in range(len(blocks)):
        # 因为重新开始游戏时会调用sysinit对系统所有参数重新初始化,为了避免方向矩阵数据重新生成,需要在此判断是否已经生成,如果已经生成则跳过。
        if len(blocks[x]) < 2:
            t = blocks[x][0]
            for i in range(3):
                t = transform(t, 1)
                blocks[x].append(formatBlock(t))


# transform,removeTopBlank,formatBlock这三个函数只为生成方块形状4个方向矩阵使用,在游戏其他环节无作用,在阅读程序时可以先跳过。
def transform(block, direction=0):
    '''
    生成指定方块形状转换方向后的矩阵数据
    args:
        block:方块形状矩阵参数
        direction:转换的方向,0代表向左,1代表向右
    return:
        变换方向后的方块形状矩阵参数
    '''
    result = []
    for y in range(4):
        result.append([])
        for x in range(4):
            if direction == 0:
                result[y].append(block[x][3 - y])
            else:
                result[y].append(block[3 - x][y])
    return result


def removeTopBlank(block):
    '''
    清除方块矩阵顶部空行数据
    args:
        block:方块开关矩阵
    return:
        整理后的方块矩阵数据
    '''
    result = copy.deepcopy(block)
    blankNumb = 0
    while sum(result[0]) < 1 and blankNumb < 4:
        del result[0]
        result.append([0, 0, 0, 0])
        blankNumb += 1
    return result


def formatBlock(block):
    '''
    整理方块矩阵数据,使方块在矩阵中处于左上角的位置
    args:
        block:方块开关矩阵
    return:
        整理后的方块矩阵数据
    '''
    result = removeTopBlank(block)
    # 将矩阵右转,用于计算左侧X轴线空行,计算完成后再转回
    result = transform(result, 1)
    result = removeTopBlank(result)
    result = transform(result, 0)
    return result


def checkDeany(sprite):
    '''
    检查下落方块是否与舞台堆叠区中固实方块发生碰撞
    args:
        sprite:下落方块
    return:
        如果发生碰撞则返回True
    '''
    topX = sprite.xy.x
    topY = sprite.xy.y
    for y in range(len(sprite.block)):
        for x in range(len(sprite.block[y])):
            if sprite.block[y][x] == 1:
                yInStage = topY + y
                xInStage = topX + x
                if yInStage > maxBlockHeight - 1 or yInStage < 0:
                    return True
                if xInStage > maxBlockWidth - 1 or xInStage < 0:
                    return True
                if stage[yInStage][xInStage] == STATIC_BLOCK:
                    return True
    return False


def checkLine():
    '''
    检测堆叠区是否有可消除的整行固实方块
    根据检测结果重新生成堆叠区矩阵数据,调用updateScore函数更新玩家积分等数据。
    return:
        本轮下落周期消除的固实方块行数
    '''
    global stage
    clearCount = 0  # 本轮下落周期消除的固实方块行数
    tmpStage = []  # 根据消除情况新生成的堆叠区矩阵,在有更新的情况下会替换全局的堆叠区矩阵。

    for y in stage:
        # 因为固实方块在堆叠矩阵里以2表示,所以判断方块是否已经满一整行只要计算矩阵行数值合计是否等于堆叠区X轴最大方块数*2就可以。
        if sum(y) >= maxBlockWidth * 2:
            tmpStage.insert(0, maxBlockWidth * [0])
            clearCount += 1
        else:
            tmpStage.append(y)
    if clearCount > 0:
        stage = tmpStage
        updateScore(clearCount)
    return clearCount


def updateStage(sprite, updateType=1):
    '''
    将下落方块坐标数据更新到堆叠区数据中。下落方块涉及的坐标在堆叠区中用数字1标识,固实方块在堆叠区中用数字2标识。
    args:
        sprite:下落方块形状
        updateType:更新方式,0代表清除,1代表动态加入,2代表固实加入。
    '''

    global stage
    topX = sprite.xy.x
    topY = sprite.xy.y
    for y in range(len(sprite.block)):
        for x in range(len(sprite.block[y])):
            if sprite.block[y][x] == 1:
                if updateType == 0:
                    if stage[topY + y][topX + x] == FALLING_BLOCK:
                        stage[topY + y][topX + x] = EMPTY_CELL
                elif updateType == 1:
                    if stage[topY + y][topX + x] == EMPTY_CELL:
                        stage[topY + y][topX + x] = FALLING_BLOCK
                else:
                    stage[topY + y][topX + x] = STATIC_BLOCK


def updateScore(clearCount):
    '''
    更新玩家游戏记录,包括积分、关卡、消除方块行数,并且根据关卡数更新方块下落速度。
    args:
        clearCount:本轮下落周期内清除的方块行数。
    return:
        当前游戏的最新积分
    '''
    global score, fallSpeed, level, clearLineScore

    prizePoint = 0  # 额外奖励分数,同时消除的行数越多,奖励分值越高。
    if clearCount > 1:
        if clearCount < 4:
            prizePoint = clearCount ** clearCount
        else:
            prizePoint = clearCount * 5
    score += (clearCount + prizePoint) * level + 10
    # 玩得再牛又有何用? :)
    if score > 99999999:
        score = 0
    clearLineScore += clearCount
    if clearLineScore > 10:
        clearLineScore = 0
        level += 1
        if level > (beginFallSpeed / 2):
            level = 1
            fallSpeed = beginFallSpeed
        fallSpeed = beginFallSpeed - level * 2
    return score


def drawStage(drawScreen):
    '''
    在给定的画布上绘制舞台
    args:
        drawScreen:待绘制的画布
    '''
    staticColor = 30, 102, 76  # 固实方块颜色
    activeColor = 255, 239, 0  # 方块形状颜色
    fontColor = 200, 10, 120  # 文字颜色
    baseRect = 0, 0, blockWidth * maxBlockWidth + 1, blockHeight * maxBlockHeight + 1  # 堆叠区方框

    # 绘制堆叠区外框
    drawScreen.fill((180, 200, 170))
    pg.draw.rect(drawScreen, staticColor, baseRect, 1)

    # 绘制堆叠区内的所有方块,包括下落方块形状
    for y in range(len(stage)):
        for x in range(len(stage[y])):
            baseRect = x * blockWidth, y * blockHeight, blockWidth, blockHeight
            if stage[y][x] == 2:
                pg.draw.rect(drawScreen, staticColor, baseRect)
            elif stage[y][x] == 1:
                pg.draw.rect(drawScreen, activeColor, baseRect)

    # 绘制下一个登场的下落方块形状
    printTxt("Next:", 320, 350, defaultFont, backSurface, fontColor)
    if nextBlock != None:
        for y in range(len(nextBlock.block)):
            for x in range(len(nextBlock.block[y])):
                baseRect = 320 + x * blockWidth, 380 + y * blockHeight, blockWidth, blockHeight
                if nextBlock.block[y][x] == 1:
                    pg.draw.rect(drawScreen, activeColor, baseRect)

    # 绘制关卡、积分、当前关卡消除整行数
    printTxt("Level:%d" % level, 320, 40, defaultFont, backSurface, fontColor)
    printTxt("Score:%d" % score, 320, 70, defaultFont, backSurface, fontColor)
    printTxt("Clear:%d" % clearLineScore, 320, 100, defaultFont, backSurface, fontColor)

    # 特殊游戏状态的输出
    if gameOver:
        printTxt("GAME OVER", 230, 200, defaultFont, backSurface, fontColor)
        printTxt("", 200, 260, defaultFont, backSurface, fontColor)
    if pause:
        printTxt("Game pausing", 230, 200, defaultFont, backSurface, fontColor)
        printTxt("", 200, 260, defaultFont, backSurface, fontColor)


def process():
    '''
    游戏控制及逻辑处理
    '''
    global gameOver, nowBlock, nextBlock, speedBuff, backSurface, keyBuff, pause

    if nextBlock is None:
        nextBlock = blockSprite(random.randint(0, len(blocks) - 1), random.randint(0, 3),
                                point(maxBlockWidth + 4, maxBlockHeight))
    if nowBlock is None:
        nowBlock = nextBlock.clone()
        nowBlock.xy = point(maxBlockWidth // 2, 0)
        nextBlock = blockSprite(random.randint(0, len(blocks) - 1), random.randint(0, 3),
                                point(maxBlockWidth + 4, maxBlockHeight))
        # 每次生成新的下落方块形状时检测碰撞,如果新的方块形状一出现就发生碰撞,则显然玩家已经没有机会了。
        gameOver = checkDeany(nowBlock)
        # 游戏失败后,要将活动方块形状做固实处理
        if gameOver:
            updateStage(nowBlock, 2)

    '''
    对于下落方块形状操控以及移动,采用影子形状进行预判断。如果没有碰撞则将变化应用到下落方块形状上,否则不变化。
    '''
    tmpBlock = nowBlock.clone()  # 影子方块形状
    '''
    处理用户输入
    对于用户输入分为两部分处理。
    第一部分,将退出、暂停、重新开始以及形状变换的操作以敲击事件处理。
    这样做的好处是只对敲击一次键盘做出处理,避免用户按住单一按键后程序反复处理影响操控,特别是形状变换操作,敲击一次键盘换变一次方向,玩家很容易控制。
    '''
    for event in pg.event.get():
        if event.type == pg.QUIT:
            sys.exit()
            pg.quit()
        elif event.type == pg.KEYDOWN:
            if event.key == pg.K_ESCAPE:
                sys.exit()
                pg.quit()
            elif event.key == pg.K_RETURN:
                if gameOver:
                    sysInit()
                    return
                elif pause:
                    pause = False
                else:
                    pause = True
                    return
            elif not gameOver and not pause:
                if event.key == pg.K_SPACE:
                    tmpBlock.chgDirection(1)
                elif event.key == pg.K_UP:
                    tmpBlock.chgDirection(0)

    if not gameOver and not pause:
        '''
        用户输入处理第二部分,将左右移动和快速下落的操作以按下事件处理。
        这样做的好处是不需要玩家反复敲击键盘进行操作,保证了操作的连贯性。
        由于连续移动的速度太快,不利于定位。所以在程序中采用了简单的输入减缓处理,即通过keyBuff保存上一次操作按键,如果此次按键与上一次按键相同,则跳过此轮按键处理。
        '''
        keys = pg.key.get_pressed()
        if keys[K_DOWN]:
            tmpBlock.xy = point(tmpBlock.xy.x, tmpBlock.xy.y + 1)
            keyBuff = None
        elif keys[K_LEFT]:
            if keyBuff != pg.K_LEFT:
                tmpBlock.xy = point(tmpBlock.xy.x - 1, tmpBlock.xy.y)
                keyBuff = pg.K_LEFT
            else:
                keyBuff = None
        elif keys[K_RIGHT]:
            if keyBuff != pg.K_RIGHT:
                tmpBlock.xy = point(tmpBlock.xy.x + 1, tmpBlock.xy.y)
                keyBuff = pg.K_RIGHT
            else:
                keyBuff = None
        if not checkDeany(tmpBlock):
            updateStage(nowBlock, 0)
            nowBlock = tmpBlock.clone()

        # 处理自动下落
        speedBuff += 1
        if speedBuff >= fallSpeed:
            speedBuff = 0
            tmpBlock = nowBlock.clone()
            tmpBlock.xy = point(nowBlock.xy.x, nowBlock.xy.y + 1)
            if not checkDeany(tmpBlock):
                updateStage(nowBlock, 0)
                nowBlock = tmpBlock.clone()
                updateStage(nowBlock, 1)
            else:
                # 在自动下落过程中一但发生活动方块形状的碰撞,则将活动方块形状做固实处理,并检测是否有可消除的整行方块
                updateStage(nowBlock, 2)
                checkLine()
                nowBlock = None
        else:
            updateStage(nowBlock, 1)
    drawStage(backSurface)
    screen.blit(backSurface, (0, 0))
    pg.display.update()
    clock.tick(40)


def main():
    '''
    主程序
    '''
    getConf("elsfk.cfg")
    sysInit()
    while True:
        process()


if __name__ == "__main__":
    main()

我的世界
python小游戏_第2张图片

'''
公众号:【一行数据】,关注领取5T编程资料
'''
from __future__ import division

import sys
import math
import random
import time

from collections import deque
from pyglet import image
from pyglet.gl import *
from pyglet.graphics import TextureGroup
from pyglet.window import key, mouse

TICKS_PER_SEC = 60

# Size of sectors used to ease block loading.
SECTOR_SIZE = 16

WALKING_SPEED = 5
FLYING_SPEED = 15

GRAVITY = 20.0
MAX_JUMP_HEIGHT = 1.0 # About the height of a block.
# To derive the formula for calculating jump speed, first solve
#    v_t = v_0 + a * t
# for the time at which you achieve maximum height, where a is the acceleration
# due to gravity and v_t = 0. This gives:
#    t = - v_0 / a
# Use t and the desired MAX_JUMP_HEIGHT to solve for v_0 (jump speed) in
#    s = s_0 + v_0 * t + (a * t^2) / 2
JUMP_SPEED = math.sqrt(2 * GRAVITY * MAX_JUMP_HEIGHT)
TERMINAL_VELOCITY = 50

PLAYER_HEIGHT = 2

if sys.version_info[0] >= 3:
    xrange = range

def cube_vertices(x, y, z, n):
    """ Return the vertices of the cube at position x, y, z with size 2*n.

    """
    return [
        x-n,y+n,z-n, x-n,y+n,z+n, x+n,y+n,z+n, x+n,y+n,z-n,  # top
        x-n,y-n,z-n, x+n,y-n,z-n, x+n,y-n,z+n, x-n,y-n,z+n,  # bottom
        x-n,y-n,z-n, x-n,y-n,z+n, x-n,y+n,z+n, x-n,y+n,z-n,  # left
        x+n,y-n,z+n, x+n,y-n,z-n, x+n,y+n,z-n, x+n,y+n,z+n,  # right
        x-n,y-n,z+n, x+n,y-n,z+n, x+n,y+n,z+n, x-n,y+n,z+n,  # front
        x+n,y-n,z-n, x-n,y-n,z-n, x-n,y+n,z-n, x+n,y+n,z-n,  # back
    ]


def tex_coord(x, y, n=4):
    """ Return the bounding vertices of the texture square.

    """
    m = 1.0 / n
    dx = x * m
    dy = y * m
    return dx, dy, dx + m, dy, dx + m, dy + m, dx, dy + m


def tex_coords(top, bottom, side):
    """ Return a list of the texture squares for the top, bottom and side.

    """
    top = tex_coord(*top)
    bottom = tex_coord(*bottom)
    side = tex_coord(*side)
    result = []
    result.extend(top)
    result.extend(bottom)
    result.extend(side * 4)
    return result


TEXTURE_PATH = 'texture.png'

GRASS = tex_coords((1, 0), (0, 1), (0, 0))
SAND = tex_coords((1, 1), (1, 1), (1, 1))
BRICK = tex_coords((2, 0), (2, 0), (2, 0))
STONE = tex_coords((2, 1), (2, 1), (2, 1))

FACES = [
    ( 0, 1, 0),
    ( 0,-1, 0),
    (-1, 0, 0),
    ( 1, 0, 0),
    ( 0, 0, 1),
    ( 0, 0,-1),
]


def normalize(position):
    """ Accepts `position` of arbitrary precision and returns the block
    containing that position.

    Parameters
    ----------
    position : tuple of len 3

    Returns
    -------
    block_position : tuple of ints of len 3

    """
    x, y, z = position
    x, y, z = (int(round(x)), int(round(y)), int(round(z)))
    return (x, y, z)


def sectorize(position):
    """ Returns a tuple representing the sector for the given `position`.

    Parameters
    ----------
    position : tuple of len 3

    Returns
    -------
    sector : tuple of len 3

    """
    x, y, z = normalize(position)
    x, y, z = x // SECTOR_SIZE, y // SECTOR_SIZE, z // SECTOR_SIZE
    return (x, 0, z)


class Model(object):

    def __init__(self):

        # A Batch is a collection of vertex lists for batched rendering.
        self.batch = pyglet.graphics.Batch()

        # A TextureGroup manages an OpenGL texture.
        self.group = TextureGroup(image.load(TEXTURE_PATH).get_texture())

        # A mapping from position to the texture of the block at that position.
        # This defines all the blocks that are currently in the world.
        self.world = {}

        # Same mapping as `world` but only contains blocks that are shown.
        self.shown = {}

        # Mapping from position to a pyglet `VertextList` for all shown blocks.
        self._shown = {}

        # Mapping from sector to a list of positions inside that sector.
        self.sectors = {}

        # Simple function queue implementation. The queue is populated with
        # _show_block() and _hide_block() calls
        self.queue = deque()

        self._initialize()

    def _initialize(self):
        """ Initialize the world by placing all the blocks.

        """
        n = 80  # 1/2 width and height of world
        s = 1  # step size
        y = 0  # initial y height
        for x in xrange(-n, n + 1, s):
            for z in xrange(-n, n + 1, s):
                # create a layer stone an grass everywhere.
                self.add_block((x, y - 2, z), GRASS, immediate=False)
                self.add_block((x, y - 3, z), STONE, immediate=False)
                if x in (-n, n) or z in (-n, n):
                    # create outer walls.
                    for dy in xrange(-2, 3):
                        self.add_block((x, y + dy, z), STONE, immediate=False)

        # generate the hills randomly
        o = n - 10
        for _ in xrange(120):
            a = random.randint(-o, o)  # x position of the hill
            b = random.randint(-o, o)  # z position of the hill
            c = -1  # base of the hill
            h = random.randint(1, 6)  # height of the hill
            s = random.randint(4, 8)  # 2 * s is the side length of the hill
            d = 1  # how quickly to taper off the hills
            t = random.choice([GRASS, SAND, BRICK])
            for y in xrange(c, c + h):
                for x in xrange(a - s, a + s + 1):
                    for z in xrange(b - s, b + s + 1):
                        if (x - a) ** 2 + (z - b) ** 2 > (s + 1) ** 2:
                            continue
                        if (x - 0) ** 2 + (z - 0) ** 2 < 5 ** 2:
                            continue
                        self.add_block((x, y, z), t, immediate=False)
                s -= d  # decrement side lenth so hills taper off

    def hit_test(self, position, vector, max_distance=8):
        """ Line of sight search from current position. If a block is
        intersected it is returned, along with the block previously in the line
        of sight. If no block is found, return None, None.

        Parameters
        ----------
        position : tuple of len 3
            The (x, y, z) position to check visibility from.
        vector : tuple of len 3
            The line of sight vector.
        max_distance : int
            How many blocks away to search for a hit.

        """
        m = 8
        x, y, z = position
        dx, dy, dz = vector
        previous = None
        for _ in xrange(max_distance * m):
            key = normalize((x, y, z))
            if key != previous and key in self.world:
                return key, previous
            previous = key
            x, y, z = x + dx / m, y + dy / m, z + dz / m
        return None, None

    def exposed(self, position):
        """ Returns False is given `position` is surrounded on all 6 sides by
        blocks, True otherwise.

        """
        x, y, z = position
        for dx, dy, dz in FACES:
            if (x + dx, y + dy, z + dz) not in self.world:
                return True
        return False

    def add_block(self, position, texture, immediate=True):
        """ Add a block with the given `texture` and `position` to the world.

        Parameters
        ----------
        position : tuple of len 3
            The (x, y, z) position of the block to add.
        texture : list of len 3
            The coordinates of the texture squares. Use `tex_coords()` to
            generate.
        immediate : bool
            Whether or not to draw the block immediately.

        """
        if position in self.world:
            self.remove_block(position, immediate)
        self.world[position] = texture
        self.sectors.setdefault(sectorize(position), []).append(position)
        if immediate:
            if self.exposed(position):
                self.show_block(position)
            self.check_neighbors(position)

    def remove_block(self, position, immediate=True):
        """ Remove the block at the given `position`.

        Parameters
        ----------
        position : tuple of len 3
            The (x, y, z) position of the block to remove.
        immediate : bool
            Whether or not to immediately remove block from canvas.

        """
        del self.world[position]
        self.sectors[sectorize(position)].remove(position)
        if immediate:
            if position in self.shown:
                self.hide_block(position)
            self.check_neighbors(position)

    def check_neighbors(self, position):
        """ Check all blocks surrounding `position` and ensure their visual
        state is current. This means hiding blocks that are not exposed and
        ensuring that all exposed blocks are shown. Usually used after a block
        is added or removed.

        """
        x, y, z = position
        for dx, dy, dz in FACES:
            key = (x + dx, y + dy, z + dz)
            if key not in self.world:
                continue
            if self.exposed(key):
                if key not in self.shown:
                    self.show_block(key)
            else:
                if key in self.shown:
                    self.hide_block(key)

    def show_block(self, position, immediate=True):
        """ Show the block at the given `position`. This method assumes the
        block has already been added with add_block()

        Parameters
        ----------
        position : tuple of len 3
            The (x, y, z) position of the block to show.
        immediate : bool
            Whether or not to show the block immediately.

        """
        texture = self.world[position]
        self.shown[position] = texture
        if immediate:
            self._show_block(position, texture)
        else:
            self._enqueue(self._show_block, position, texture)

    def _show_block(self, position, texture):
        """ Private implementation of the `show_block()` method.

        Parameters
        ----------
        position : tuple of len 3
            The (x, y, z) position of the block to show.
        texture : list of len 3
            The coordinates of the texture squares. Use `tex_coords()` to
            generate.

        """
        x, y, z = position
        vertex_data = cube_vertices(x, y, z, 0.5)
        texture_data = list(texture)
        # create vertex list
        # FIXME Maybe `add_indexed()` should be used instead
        self._shown[position] = self.batch.add(24, GL_QUADS, self.group,
            ('v3f/static', vertex_data),
            ('t2f/static', texture_data))

    def hide_block(self, position, immediate=True):
        """ Hide the block at the given `position`. Hiding does not remove the
        block from the world.

        Parameters
        ----------
        position : tuple of len 3
            The (x, y, z) position of the block to hide.
        immediate : bool
            Whether or not to immediately remove the block from the canvas.

        """
        self.shown.pop(position)
        if immediate:
            self._hide_block(position)
        else:
            self._enqueue(self._hide_block, position)

    def _hide_block(self, position):
        """ Private implementation of the 'hide_block()` method.

        """
        self._shown.pop(position).delete()

    def show_sector(self, sector):
        """ Ensure all blocks in the given sector that should be shown are
        drawn to the canvas.

        """
        for position in self.sectors.get(sector, []):
            if position not in self.shown and self.exposed(position):
                self.show_block(position, False)

    def hide_sector(self, sector):
        """ Ensure all blocks in the given sector that should be hidden are
        removed from the canvas.

        """
        for position in self.sectors.get(sector, []):
            if position in self.shown:
                self.hide_block(position, False)

    def change_sectors(self, before, after):
        """ Move from sector `before` to sector `after`. A sector is a
        contiguous x, y sub-region of world. Sectors are used to speed up
        world rendering.

        """
        before_set = set()
        after_set = set()
        pad = 4
        for dx in xrange(-pad, pad + 1):
            for dy in [0]:  # xrange(-pad, pad + 1):
                for dz in xrange(-pad, pad + 1):
                    if dx ** 2 + dy ** 2 + dz ** 2 > (pad + 1) ** 2:
                        continue
                    if before:
                        x, y, z = before
                        before_set.add((x + dx, y + dy, z + dz))
                    if after:
                        x, y, z = after
                        after_set.add((x + dx, y + dy, z + dz))
        show = after_set - before_set
        hide = before_set - after_set
        for sector in show:
            self.show_sector(sector)
        for sector in hide:
            self.hide_sector(sector)

    def _enqueue(self, func, *args):
        """ Add `func` to the internal queue.

        """
        self.queue.append((func, args))

    def _dequeue(self):
        """ Pop the top function from the internal queue and call it.

        """
        func, args = self.queue.popleft()
        func(*args)

    def process_queue(self):
        """ Process the entire queue while taking periodic breaks. This allows
        the game loop to run smoothly. The queue contains calls to
        _show_block() and _hide_block() so this method should be called if
        add_block() or remove_block() was called with immediate=False

        """
        start = time.clock()
        while self.queue and time.clock() - start < 1.0 / TICKS_PER_SEC:
            self._dequeue()

    def process_entire_queue(self):
        """ Process the entire queue with no breaks.

        """
        while self.queue:
            self._dequeue()


class Window(pyglet.window.Window):

    def __init__(self, *args, **kwargs):
        super(Window, self).__init__(*args, **kwargs)

        # Whether or not the window exclusively captures the mouse.
        self.exclusive = False

        # When flying gravity has no effect and speed is increased.
        self.flying = False

        # Strafing is moving lateral to the direction you are facing,
        # e.g. moving to the left or right while continuing to face forward.
        #
        # First element is -1 when moving forward, 1 when moving back, and 0
        # otherwise. The second element is -1 when moving left, 1 when moving
        # right, and 0 otherwise.
        self.strafe = [0, 0]

        # Current (x, y, z) position in the world, specified with floats. Note
        # that, perhaps unlike in math class, the y-axis is the vertical axis.
        self.position = (0, 0, 0)

        # First element is rotation of the player in the x-z plane (ground
        # plane) measured from the z-axis down. The second is the rotation
        # angle from the ground plane up. Rotation is in degrees.
        #
        # The vertical plane rotation ranges from -90 (looking straight down) to
        # 90 (looking straight up). The horizontal rotation range is unbounded.
        self.rotation = (0, 0)

        # Which sector the player is currently in.
        self.sector = None

        # The crosshairs at the center of the screen.
        self.reticle = None

        # Velocity in the y (upward) direction.
        self.dy = 0

        # A list of blocks the player can place. Hit num keys to cycle.
        self.inventory = [BRICK, GRASS, SAND]

        # The current block the user can place. Hit num keys to cycle.
        self.block = self.inventory[0]

        # Convenience list of num keys.
        self.num_keys = [
            key._1, key._2, key._3, key._4, key._5,
            key._6, key._7, key._8, key._9, key._0]

        # Instance of the model that handles the world.
        self.model = Model()

        # The label that is displayed in the top left of the canvas.
        self.label = pyglet.text.Label('', font_name='Arial', font_size=18,
            x=10, y=self.height - 10, anchor_x='left', anchor_y='top',
            color=(0, 0, 0, 255))

        # This call schedules the `update()` method to be called
        # TICKS_PER_SEC. This is the main game event loop.
        pyglet.clock.schedule_interval(self.update, 1.0 / TICKS_PER_SEC)

    def set_exclusive_mouse(self, exclusive):
        """ If `exclusive` is True, the game will capture the mouse, if False
        the game will ignore the mouse.

        """
        super(Window, self).set_exclusive_mouse(exclusive)
        self.exclusive = exclusive

    def get_sight_vector(self):
        """ Returns the current line of sight vector indicating the direction
        the player is looking.

        """
        x, y = self.rotation
        # y ranges from -90 to 90, or -pi/2 to pi/2, so m ranges from 0 to 1 and
        # is 1 when looking ahead parallel to the ground and 0 when looking
        # straight up or down.
        m = math.cos(math.radians(y))
        # dy ranges from -1 to 1 and is -1 when looking straight down and 1 when
        # looking straight up.
        dy = math.sin(math.radians(y))
        dx = math.cos(math.radians(x - 90)) * m
        dz = math.sin(math.radians(x - 90)) * m
        return (dx, dy, dz)

    def get_motion_vector(self):
        """ Returns the current motion vector indicating the velocity of the
        player.

        Returns
        -------
        vector : tuple of len 3
            Tuple containing the velocity in x, y, and z respectively.

        """
        if any(self.strafe):
            x, y = self.rotation
            strafe = math.degrees(math.atan2(*self.strafe))
            y_angle = math.radians(y)
            x_angle = math.radians(x + strafe)
            if self.flying:
                m = math.cos(y_angle)
                dy = math.sin(y_angle)
                if self.strafe[1]:
                    # Moving left or right.
                    dy = 0.0
                    m = 1
                if self.strafe[0] > 0:
                    # Moving backwards.
                    dy *= -1
                # When you are flying up or down, you have less left and right
                # motion.
                dx = math.cos(x_angle) * m
                dz = math.sin(x_angle) * m
            else:
                dy = 0.0
                dx = math.cos(x_angle)
                dz = math.sin(x_angle)
        else:
            dy = 0.0
            dx = 0.0
            dz = 0.0
        return (dx, dy, dz)

    def update(self, dt):
        """ This method is scheduled to be called repeatedly by the pyglet
        clock.

        Parameters
        ----------
        dt : float
            The change in time since the last call.

        """
        self.model.process_queue()
        sector = sectorize(self.position)
        if sector != self.sector:
            self.model.change_sectors(self.sector, sector)
            if self.sector is None:
                self.model.process_entire_queue()
            self.sector = sector
        m = 8
        dt = min(dt, 0.2)
        for _ in xrange(m):
            self._update(dt / m)

    def _update(self, dt):
        """ Private implementation of the `update()` method. This is where most
        of the motion logic lives, along with gravity and collision detection.

        Parameters
        ----------
        dt : float
            The change in time since the last call.

        """
        # walking
        speed = FLYING_SPEED if self.flying else WALKING_SPEED
        d = dt * speed # distance covered this tick.
        dx, dy, dz = self.get_motion_vector()
        # New position in space, before accounting for gravity.
        dx, dy, dz = dx * d, dy * d, dz * d
        # gravity
        if not self.flying:
            # Update your vertical speed: if you are falling, speed up until you
            # hit terminal velocity; if you are jumping, slow down until you
            # start falling.
            self.dy -= dt * GRAVITY
            self.dy = max(self.dy, -TERMINAL_VELOCITY)
            dy += self.dy * dt
        # collisions
        x, y, z = self.position
        x, y, z = self.collide((x + dx, y + dy, z + dz), PLAYER_HEIGHT)
        self.position = (x, y, z)

    def collide(self, position, height):
        """ Checks to see if the player at the given `position` and `height`
        is colliding with any blocks in the world.

        Parameters
        ----------
        position : tuple of len 3
            The (x, y, z) position to check for collisions at.
        height : int or float
            The height of the player.

        Returns
        -------
        position : tuple of len 3
            The new position of the player taking into account collisions.

        """
        # How much overlap with a dimension of a surrounding block you need to
        # have to count as a collision. If 0, touching terrain at all counts as
        # a collision. If .49, you sink into the ground, as if walking through
        # tall grass. If >= .5, you'll fall through the ground.
        pad = 0.25
        p = list(position)
        np = normalize(position)
        for face in FACES:  # check all surrounding blocks
            for i in xrange(3):  # check each dimension independently
                if not face[i]:
                    continue
                # How much overlap you have with this dimension.
                d = (p[i] - np[i]) * face[i]
                if d < pad:
                    continue
                for dy in xrange(height):  # check each height
                    op = list(np)
                    op[1] -= dy
                    op[i] += face[i]
                    if tuple(op) not in self.model.world:
                        continue
                    p[i] -= (d - pad) * face[i]
                    if face == (0, -1, 0) or face == (0, 1, 0):
                        # You are colliding with the ground or ceiling, so stop
                        # falling / rising.
                        self.dy = 0
                    break
        return tuple(p)

    def on_mouse_press(self, x, y, button, modifiers):
        """ Called when a mouse button is pressed. See pyglet docs for button
        amd modifier mappings.

        Parameters
        ----------
        x, y : int
            The coordinates of the mouse click. Always center of the screen if
            the mouse is captured.
        button : int
            Number representing mouse button that was clicked. 1 = left button,
            4 = right button.
        modifiers : int
            Number representing any modifying keys that were pressed when the
            mouse button was clicked.

        """
        if self.exclusive:
            vector = self.get_sight_vector()
            block, previous = self.model.hit_test(self.position, vector)
            if (button == mouse.RIGHT) or \
                    ((button == mouse.LEFT) and (modifiers & key.MOD_CTRL)):
                # ON OSX, control + left click = right click.
                if previous:
                    self.model.add_block(previous, self.block)
            elif button == pyglet.window.mouse.LEFT and block:
                texture = self.model.world[block]
                if texture != STONE:
                    self.model.remove_block(block)
        else:
            self.set_exclusive_mouse(True)

    def on_mouse_motion(self, x, y, dx, dy):
        """ Called when the player moves the mouse.

        Parameters
        ----------
        x, y : int
            The coordinates of the mouse click. Always center of the screen if
            the mouse is captured.
        dx, dy : float
            The movement of the mouse.

        """
        if self.exclusive:
            m = 0.15
            x, y = self.rotation
            x, y = x + dx * m, y + dy * m
            y = max(-90, min(90, y))
            self.rotation = (x, y)

    def on_key_press(self, symbol, modifiers):
        """ Called when the player presses a key. See pyglet docs for key
        mappings.

        Parameters
        ----------
        symbol : int
            Number representing the key that was pressed.
        modifiers : int
            Number representing any modifying keys that were pressed.

        """
        if symbol == key.W:
            self.strafe[0] -= 1
        elif symbol == key.S:
            self.strafe[0] += 1
        elif symbol == key.A:
            self.strafe[1] -= 1
        elif symbol == key.D:
            self.strafe[1] += 1
        elif symbol == key.SPACE:
            if self.dy == 0:
                self.dy = JUMP_SPEED
        elif symbol == key.ESCAPE:
            self.set_exclusive_mouse(False)
        elif symbol == key.TAB:
            self.flying = not self.flying
        elif symbol in self.num_keys:
            index = (symbol - self.num_keys[0]) % len(self.inventory)
            self.block = self.inventory[index]

    def on_key_release(self, symbol, modifiers):
        """ Called when the player releases a key. See pyglet docs for key
        mappings.

        Parameters
        ----------
        symbol : int
            Number representing the key that was pressed.
        modifiers : int
            Number representing any modifying keys that were pressed.

        """
        if symbol == key.W:
            self.strafe[0] += 1
        elif symbol == key.S:
            self.strafe[0] -= 1
        elif symbol == key.A:
            self.strafe[1] += 1
        elif symbol == key.D:
            self.strafe[1] -= 1

    def on_resize(self, width, height):
        """ Called when the window is resized to a new `width` and `height`.

        """
        # label
        self.label.y = height - 10
        # reticle
        if self.reticle:
            self.reticle.delete()
        x, y = self.width // 2, self.height // 2
        n = 10
        self.reticle = pyglet.graphics.vertex_list(4,
            ('v2i', (x - n, y, x + n, y, x, y - n, x, y + n))
        )

    def set_2d(self):
        """ Configure OpenGL to draw in 2d.

        """
        width, height = self.get_size()
        glDisable(GL_DEPTH_TEST)
        viewport = self.get_viewport_size()
        glViewport(0, 0, max(1, viewport[0]), max(1, viewport[1]))
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(0, max(1, width), 0, max(1, height), -1, 1)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()

    def set_3d(self):
        """ Configure OpenGL to draw in 3d.

        """
        width, height = self.get_size()
        glEnable(GL_DEPTH_TEST)
        viewport = self.get_viewport_size()
        glViewport(0, 0, max(1, viewport[0]), max(1, viewport[1]))
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(65.0, width / float(height), 0.1, 60.0)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        x, y = self.rotation
        glRotatef(x, 0, 1, 0)
        glRotatef(-y, math.cos(math.radians(x)), 0, math.sin(math.radians(x)))
        x, y, z = self.position
        glTranslatef(-x, -y, -z)

    def on_draw(self):
        """ Called by pyglet to draw the canvas.

        """
        self.clear()
        self.set_3d()
        glColor3d(1, 1, 1)
        self.model.batch.draw()
        self.draw_focused_block()
        self.set_2d()
        self.draw_label()
        self.draw_reticle()

    def draw_focused_block(self):
        """ Draw black edges around the block that is currently under the
        crosshairs.

        """
        vector = self.get_sight_vector()
        block = self.model.hit_test(self.position, vector)[0]
        if block:
            x, y, z = block
            vertex_data = cube_vertices(x, y, z, 0.51)
            glColor3d(0, 0, 0)
            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
            pyglet.graphics.draw(24, GL_QUADS, ('v3f/static', vertex_data))
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)

    def draw_label(self):
        """ Draw the label in the top left of the screen.

        """
        x, y, z = self.position
        self.label.text = '%02d (%.2f, %.2f, %.2f) %d / %d' % (
            pyglet.clock.get_fps(), x, y, z,
            len(self.model._shown), len(self.model.world))
        self.label.draw()

    def draw_reticle(self):
        """ Draw the crosshairs in the center of the screen.

        """
        glColor3d(0, 0, 0)
        self.reticle.draw(GL_LINES)


def setup_fog():
    """ Configure the OpenGL fog properties.

    """
    # Enable fog. Fog "blends a fog color with each rasterized pixel fragment's
    # post-texturing color."
    glEnable(GL_FOG)
    # Set the fog color.
    glFogfv(GL_FOG_COLOR, (GLfloat * 4)(0.5, 0.69, 1.0, 1))
    # Say we have no preference between rendering speed and quality.
    glHint(GL_FOG_HINT, GL_DONT_CARE)
    # Specify the equation used to compute the blending factor.
    glFogi(GL_FOG_MODE, GL_LINEAR)
    # How close and far away fog starts and ends. The closer the start and end,
    # the denser the fog in the fog range.
    glFogf(GL_FOG_START, 20.0)
    glFogf(GL_FOG_END, 60.0)


def setup():
    """ Basic OpenGL configuration.

    """
    # Set the color of "clear", i.e. the sky, in rgba.
    glClearColor(0.5, 0.69, 1.0, 1)
    # Enable culling (not rendering) of back-facing facets -- facets that aren't
    # visible to you.
    glEnable(GL_CULL_FACE)
    # Set the texture minification/magnification function to GL_NEAREST (nearest
    # in Manhattan distance) to the specified texture coordinates. GL_NEAREST
    # "is generally faster than GL_LINEAR, but it can produce textured 图片
    # with sharper edges because the transition between texture elements is not
    # as smooth."
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
    setup_fog()


def main():
    window = Window(width=1800, height=1600, caption='Pyglet', resizable=True)
    # Hide the mouse cursor and prevent the mouse from leaving the window.
    window.set_exclusive_mouse(True)
    setup()
    pyglet.app.run()


if __name__ == '__main__':
    main()



python小游戏_第3张图片

# coding: utf-8
'''
公众号:一行数据,关注领取5T编程资料
'''
import sys
import pygame
import scene
import bullet
import food
import tanks
import home
from pygame.locals import *


# 开始界面显示
def show_start_interface(screen, width, height):
	tfont = pygame.font.Font('./font/simkai.ttf', width//4)
	cfont = pygame.font.Font('./font/simkai.ttf', width//20)
	title = tfont.render(u'坦克大战', True, (255, 0, 0))
	content1 = cfont.render(u'按1键进入单人游戏', True, (0, 0, 255))
	content2 = cfont.render(u'按2键进入双人人游戏', True, (0, 0, 255))
	trect = title.get_rect()
	trect.midtop = (width/2, height/4)
	crect1 = content1.get_rect()
	crect1.midtop = (width/2, height/1.8)
	crect2 = content2.get_rect()
	crect2.midtop = (width/2, height/1.6)
	screen.blit(title, trect)
	screen.blit(content1, crect1)
	screen.blit(content2, crect2)
	pygame.display.update()
	while True:
		for event in pygame.event.get():
			if event.type == QUIT:
				sys.exit()
			elif event.type == pygame.KEYDOWN:
				if event.key == pygame.K_1:
					return 1
				if event.key == pygame.K_2:
					return 2


# 结束界面显示
def show_end_interface(screen, width, height, is_win):
	bg_img = pygame.image.load("./图片/others/background.png")
	screen.blit(bg_img, (0, 0))
	if is_win:
		font = pygame.font.Font('./font/simkai.ttf', width//10)
		content = font.render(u'恭喜通关!', True, (255, 0, 0))
		rect = content.get_rect()
		rect.midtop = (width/2, height/2)
		screen.blit(content, rect)
	else:
		fail_img = pygame.image.load("./图片/others/gameover.png")
		rect = fail_img.get_rect()
		rect.midtop = (width/2, height/2)
		screen.blit(fail_img, rect)
	pygame.display.update()
	while True:
		for event in pygame.event.get():
			if event.type == QUIT:
				sys.exit()


# 关卡切换
def show_switch_stage(screen, width, height, stage):
	bg_img = pygame.image.load("./图片/others/background.png")
	screen.blit(bg_img, (0, 0))
	font = pygame.font.Font('./font/simkai.ttf', width//10)
	content = font.render(u'第%d关' % stage, True, (0, 255, 0))
	rect = content.get_rect()
	rect.midtop = (width/2, height/2)
	screen.blit(content, rect)
	pygame.display.update()
	delay_event = pygame.constants.USEREVENT
	pygame.time.set_timer(delay_event, 1000)
	while True:
		for event in pygame.event.get():
			if event.type == QUIT:
				sys.exit()
			if event.type == delay_event:
				return


# 主函数
def main():
	# 初始化
	pygame.init()
	screen = pygame.display.set_mode((630, 630))
	pygame.display.set_caption("坦克大战")
	# 加载图片
	bg_img = pygame.image.load("./图片/others/background.png")
	# 开始界面
	num_player = show_start_interface(screen, 630, 630)
	# 关卡
	stage = 0
	num_stage = 2
	# 游戏是否结束
	is_gameover = False
	# 时钟
	clock = pygame.time.Clock()
	# 主循环
	while not is_gameover:
		# 关卡
		stage += 1
		if stage > num_stage:
			break
		show_switch_stage(screen, 630, 630, stage)
		# 该关卡坦克总数量
		enemytanks_total = min(stage * 18, 80)
		# 场上存在的敌方坦克总数量
		enemytanks_now = 0
		# 场上可以存在的敌方坦克总数量
		enemytanks_now_max = min(max(stage * 2, 4), 8)
		# 精灵组
		tanksGroup = pygame.sprite.Group()
		mytanksGroup = pygame.sprite.Group()
		enemytanksGroup = pygame.sprite.Group()
		bulletsGroup = pygame.sprite.Group()
		mybulletsGroup = pygame.sprite.Group()
		enemybulletsGroup = pygame.sprite.Group()
		myfoodsGroup = pygame.sprite.Group()
		# 自定义事件
		# 	-生成敌方坦克事件
		genEnemyEvent = pygame.constants.USEREVENT + 0
		pygame.time.set_timer(genEnemyEvent, 100)
		# 	-敌方坦克静止恢复事件
		recoverEnemyEvent = pygame.constants.USEREVENT + 1
		pygame.time.set_timer(recoverEnemyEvent, 8000)
		# 	-我方坦克无敌恢复事件
		noprotectMytankEvent = pygame.constants.USEREVENT + 2
		pygame.time.set_timer(noprotectMytankEvent, 8000)
		# 关卡地图
		map_stage = scene.Map(stage)
		# 我方坦克
		tank_player1 = tanks.myTank(1)
		tanksGroup.add(tank_player1)
		mytanksGroup.add(tank_player1)
		if num_player > 1:
			tank_player2 = tanks.myTank(2)
			tanksGroup.add(tank_player2)
			mytanksGroup.add(tank_player2)
		is_switch_tank = True
		player1_moving = False
		player2_moving = False
		# 为了轮胎的动画效果
		time = 0
		# 敌方坦克
		for i in range(0, 3):
			if enemytanks_total > 0:
				enemytank = tanks.enemyTank(i)
				tanksGroup.add(enemytank)
				enemytanksGroup.add(enemytank)
				enemytanks_now += 1
				enemytanks_total -= 1
		# 大本营
		myhome = home.Home()
		# 出场特效
		appearance_img = pygame.image.load("./图片/others/appear.png").convert_alpha()
		appearances = []
		appearances.append(appearance_img.subsurface((0, 0), (48, 48)))
		appearances.append(appearance_img.subsurface((48, 0), (48, 48)))
		appearances.append(appearance_img.subsurface((96, 0), (48, 48)))
		# 关卡主循环
		while True:
			if is_gameover is True:
				break
			if enemytanks_total < 1 and enemytanks_now < 1:
				is_gameover = False
				break
			for event in pygame.event.get():
				if event.type == pygame.QUIT:
					pygame.quit()
					sys.exit()
				if event.type == genEnemyEvent:
					if enemytanks_total > 0:
						if enemytanks_now < enemytanks_now_max:
							enemytank = tanks.enemyTank()
							if not pygame.sprite.spritecollide(enemytank, tanksGroup, False, None):
								tanksGroup.add(enemytank)
								enemytanksGroup.add(enemytank)
								enemytanks_now += 1
								enemytanks_total -= 1
				if event.type == recoverEnemyEvent:
					for each in enemytanksGroup:
						each.can_move = True
				if event.type == noprotectMytankEvent:
					for each in mytanksGroup:
						mytanksGroup.protected = False
			# 检查用户键盘操作
			key_pressed = pygame.key.get_pressed()
			# 玩家一
			# WSAD -> 上下左右
			# 空格键射击
			if key_pressed[pygame.K_w]:
				tanksGroup.remove(tank_player1)
				tank_player1.move_up(tanksGroup, map_stage.brickGroup, map_stage.ironGroup, myhome)
				tanksGroup.add(tank_player1)
				player1_moving = True
			elif key_pressed[pygame.K_s]:
				tanksGroup.remove(tank_player1)
				tank_player1.move_down(tanksGroup, map_stage.brickGroup, map_stage.ironGroup, myhome)
				tanksGroup.add(tank_player1)
				player1_moving = True
			elif key_pressed[pygame.K_a]:
				tanksGroup.remove(tank_player1)
				tank_player1.move_left(tanksGroup, map_stage.brickGroup, map_stage.ironGroup, myhome)
				tanksGroup.add(tank_player1)
				player1_moving = True
			elif key_pressed[pygame.K_d]:
				tanksGroup.remove(tank_player1)
				tank_player1.move_right(tanksGroup, map_stage.brickGroup, map_stage.ironGroup, myhome)
				tanksGroup.add(tank_player1)
				player1_moving = True
			elif key_pressed[pygame.K_SPACE]:
				if not tank_player1.bullet.being:
					tank_player1.shoot()
			# 玩家二
			# ↑↓←→ -> 上下左右
			# 小键盘0键射击
			if num_player > 1:
				if key_pressed[pygame.K_UP]:
					tanksGroup.remove(tank_player2)
					tank_player2.move_up(tanksGroup, map_stage.brickGroup, map_stage.ironGroup, myhome)
					tanksGroup.add(tank_player2)
					player2_moving = True
				elif key_pressed[pygame.K_DOWN]:
					tanksGroup.remove(tank_player2)
					tank_player2.move_down(tanksGroup, map_stage.brickGroup, map_stage.ironGroup, myhome)
					tanksGroup.add(tank_player2)
					player2_moving = True
				elif key_pressed[pygame.K_LEFT]:
					tanksGroup.remove(tank_player2)
					tank_player2.move_left(tanksGroup, map_stage.brickGroup, map_stage.ironGroup, myhome)
					tanksGroup.add(tank_player2)
					player2_moving = True
				elif key_pressed[pygame.K_RIGHT]:
					tanksGroup.remove(tank_player2)
					tank_player2.move_right(tanksGroup, map_stage.brickGroup, map_stage.ironGroup, myhome)
					tanksGroup.add(tank_player2)
					player2_moving = True
				elif key_pressed[pygame.K_KP0]:
					if not tank_player2.bullet.being:
						tank_player2.shoot()
			# 背景
			screen.blit(bg_img, (0, 0))
			# 石头墙
			for each in map_stage.brickGroup:
				screen.blit(each.brick, each.rect)
			# 钢墙
			for each in map_stage.ironGroup:
				screen.blit(each.iron, each.rect)
			# 冰
			for each in map_stage.iceGroup:
				screen.blit(each.ice, each.rect)
			# 河流
			for each in map_stage.riverGroup:
				screen.blit(each.river, each.rect)
			# 树
			for each in map_stage.treeGroup:
				screen.blit(each.tree, each.rect)
			time += 1
			if time == 5:
				time = 0
				is_switch_tank = not is_switch_tank
			# 我方坦克
			if tank_player1 in mytanksGroup:
				if is_switch_tank and player1_moving:
					screen.blit(tank_player1.tank_0, (tank_player1.rect.left, tank_player1.rect.top))
					player1_moving = False
				else:
					screen.blit(tank_player1.tank_1, (tank_player1.rect.left, tank_player1.rect.top))
				if tank_player1.protected:
					screen.blit(tank_player1.protected_mask1, (tank_player1.rect.left, tank_player1.rect.top))
			if num_player > 1:
				if tank_player2 in mytanksGroup:
					if is_switch_tank and player2_moving:
						screen.blit(tank_player2.tank_0, (tank_player2.rect.left, tank_player2.rect.top))
						player1_moving = False
					else:
						screen.blit(tank_player2.tank_1, (tank_player2.rect.left, tank_player2.rect.top))
					if tank_player2.protected:
						screen.blit(tank_player1.protected_mask1, (tank_player2.rect.left, tank_player2.rect.top))
			# 敌方坦克
			for each in enemytanksGroup:
				# 出生特效
				if each.born:
					if each.times > 0:
						each.times -= 1
						if each.times <= 10:
							screen.blit(appearances[2], (3+each.x*12*24, 3))
						elif each.times <= 20:
							screen.blit(appearances[1], (3+each.x*12*24, 3))
						elif each.times <= 30:
							screen.blit(appearances[0], (3+each.x*12*24, 3))
						elif each.times <= 40:
							screen.blit(appearances[2], (3+each.x*12*24, 3))
						elif each.times <= 50:
							screen.blit(appearances[1], (3+each.x*12*24, 3))
						elif each.times <= 60:
							screen.blit(appearances[0], (3+each.x*12*24, 3))
						elif each.times <= 70:
							screen.blit(appearances[2], (3+each.x*12*24, 3))
						elif each.times <= 80:
							screen.blit(appearances[1], (3+each.x*12*24, 3))
						elif each.times <= 90:
							screen.blit(appearances[0], (3+each.x*12*24, 3))
					else:
						each.born = False
				else:
					if is_switch_tank:
						screen.blit(each.tank_0, (each.rect.left, each.rect.top))
					else:
						screen.blit(each.tank_1, (each.rect.left, each.rect.top))
					if each.can_move:
						tanksGroup.remove(each)
						each.move(tanksGroup, map_stage.brickGroup, map_stage.ironGroup, myhome)
						tanksGroup.add(each)
			# 我方子弹
			for tank_player in mytanksGroup:
				if tank_player.bullet.being:
					tank_player.bullet.move()
					screen.blit(tank_player.bullet.bullet, tank_player.bullet.rect)
					# 子弹碰撞敌方子弹
					for each in enemybulletsGroup:
						if each.being:
							if pygame.sprite.collide_rect(tank_player.bullet, each):
								tank_player.bullet.being = False
								each.being = False
								enemybulletsGroup.remove(each)
								break
						else:
							enemybulletsGroup.remove(each)	
					# 子弹碰撞敌方坦克
					for each in enemytanksGroup:
						if each.being:
							if pygame.sprite.collide_rect(tank_player.bullet, each):
								if each.is_red == True:
									myfood = food.Food()
									myfood.generate()
									myfoodsGroup.add(myfood)
									each.is_red = False
								each.blood -= 1
								each.color -= 1
								if each.blood < 0:
									each.being = False
									enemytanksGroup.remove(each)
									enemytanks_now -= 1
									tanksGroup.remove(each)
								else:
									each.reload()
								tank_player.bullet.being = False
								break
						else:
							enemytanksGroup.remove(each)
							tanksGroup.remove(each)
					# 子弹碰撞石头墙
					if pygame.sprite.spritecollide(tank_player.bullet, map_stage.brickGroup, True, None):
						tank_player.bullet.being = False
					'''
					# 等价方案(更科学点)
					for each in map_stage.brickGroup:
						if pygame.sprite.collide_rect(tank_player.bullet, each):
							tank_player.bullet.being = False
							each.being = False
							map_stage.brickGroup.remove(each)
							break
					'''
					# 子弹碰钢墙
					if tank_player.bullet.stronger:
						if pygame.sprite.spritecollide(tank_player.bullet, map_stage.ironGroup, True, None):
							tank_player.bullet.being = False
					else:
						if pygame.sprite.spritecollide(tank_player.bullet, map_stage.ironGroup, False, None):
							tank_player.bullet.being = False
					'''
					# 等价方案(更科学点)
					for each in map_stage.ironGroup:
						if pygame.sprite.collide_rect(tank_player.bullet, each):
							tank_player.bullet.being = False
							if tank_player.bullet.stronger:
								each.being = False
								map_stage.ironGroup.remove(each)
							break
					'''
					# 子弹碰大本营
					if pygame.sprite.collide_rect(tank_player.bullet, myhome):
						tank_player.bullet.being = False
						myhome.set_dead()
						is_gameover = True
			# 敌方子弹
			for each in enemytanksGroup:
				if each.being:
					if each.can_move and not each.bullet.being:
						enemybulletsGroup.remove(each.bullet)
						each.shoot()
						enemybulletsGroup.add(each.bullet)
					if not each.born:
						if each.bullet.being:
							each.bullet.move()
							screen.blit(each.bullet.bullet, each.bullet.rect)
							# 子弹碰撞我方坦克
							for tank_player in mytanksGroup:
								if pygame.sprite.collide_rect(each.bullet, tank_player):
									if not tank_player.protected:
										tank_player.life -= 1
										if tank_player.life < 0:
											mytanksGroup.remove(tank_player)
											tanksGroup.remove(tank_player)
											if len(mytanksGroup) < 1:
												is_gameover = True
										else:
											tank_player.reset()
									each.bullet.being = False
									enemybulletsGroup.remove(each.bullet)
									break
							# 子弹碰撞石头墙
							if pygame.sprite.spritecollide(each.bullet, map_stage.brickGroup, True, None):
								each.bullet.being = False
								enemybulletsGroup.remove(each.bullet)
							'''
							# 等价方案(更科学点)
							for one in map_stage.brickGroup:
								if pygame.sprite.collide_rect(each.bullet, one):
									each.bullet.being = False
									one.being = False
									enemybulletsGroup.remove(one)
									break
							'''
							# 子弹碰钢墙
							if each.bullet.stronger:
								if pygame.sprite.spritecollide(each.bullet, map_stage.ironGroup, True, None):
									each.bullet.being = False
							else:
								if pygame.sprite.spritecollide(each.bullet, map_stage.ironGroup, False, None):
									each.bullet.being = False
							'''
							# 等价方案(更科学点)
							for one in map_stage.ironGroup:
								if pygame.sprite.collide_rect(each.bullet, one):
									each.bullet.being = False
									if each.bullet.stronger:
										one.being = False
										map_stage.ironGroup.remove(one)
									break
							'''
							# 子弹碰大本营
							if pygame.sprite.collide_rect(each.bullet, myhome):
								each.bullet.being = False
								myhome.set_dead()
								is_gameover = True
				else:
					enemytanksGroup.remove(each)
					tanksGroup.remove(each)
			# 家
			screen.blit(myhome.home, myhome.rect)
			# 食物
			for myfood in myfoodsGroup:
				if myfood.being and myfood.time > 0:
					screen.blit(myfood.food, myfood.rect)
					myfood.time -= 1
					for tank_player in mytanksGroup:
						if pygame.sprite.collide_rect(tank_player, myfood):
							# 消灭当前所有敌人
							if myfood.kind == 0:
								enemytanksGroup = pygame.sprite.Group()
								enemytanks_total -= enemytanks_now
								enemytanks_now = 0
							# 敌人静止
							if myfood.kind == 1:
								for each in enemytanksGroup:
									each.can_move = False
							# 子弹增强
							if myfood.kind == 2:
								tank_player.bullet.stronger = True
							# 使得大本营的墙变为钢板
							if myfood.kind == 3:
								map_stage.protect_home()
							# 坦克获得一段时间的保护罩
							if myfood.kind == 4:
								for tank_player in mytanksGroup:
									tank_player.protected = True
							# 坦克升级
							if myfood.kind == 5:
								tank_player.up_level()
							# 坦克生命+1
							if myfood.kind == 6:
								tank_player.life += 1
							myfood.being = False
							myfoodsGroup.remove(myfood)
							break
				else:
					myfood.being = False
					myfoodsGroup.remove(myfood)
			pygame.display.flip()
			clock.tick(60)
	if not is_gameover:
		show_end_interface(screen, 630, 630, True)
	else:
		show_end_interface(screen, 630, 630, False)


if __name__ == '__main__':
	main()






你可能感兴趣的:(游戏开发)