import copy
import sys
import os
import random
import pygame
from fractions import Fraction


#如下是上一步新增的该项目所需变量
RED = (255, 0, 0)
BLACK = (0, 0, 0)
AZURE = (240, 255, 255)
WHITE = (255, 255, 255)
MISTYROSE = (255, 228, 225)
PALETURQUOISE = (175, 238, 238)
PAPAYAWHIP = (255, 239, 213)
CURRENTPATH = os.path.abspath(os.path.dirname(__file__))
FONTPATH = os.path.join(CURRENTPATH, 'fonts/font.TTF')
AUDIOWINPATH = os.path.join(CURRENTPATH, 'audios/win.wav')
AUDIOLOSEPATH = os.path.join(CURRENTPATH, 'audios/lose.wav')
AUDIOWARNPATH = os.path.join(CURRENTPATH, 'audios/warn.wav')
BGMPATH = os.path.join(CURRENTPATH, 'audios/bgm.mp3')
# 数字卡片
# --数字卡片字体颜色
NUMBERFONT_COLORS = [BLACK, RED]
# --数字卡片背景颜色
NUMBERCARD_COLORS = [MISTYROSE, PALETURQUOISE]
# --数字卡片字体路径与大小
NUMBERFONT = [FONTPATH, 50]
# --数字卡片位置
NUMBERCARD_POSITIONS = [(25, 50, 150, 200), (225, 50, 150, 200), (425, 50, 150, 200), (625, 50, 150, 200)]
# 运算符卡片
# --运算符种类
OPREATORS = ['+', '-', '×', '÷']
# --运算符卡片字体颜色
OPREATORFONT_COLORS = [BLACK, RED]
# --运算符卡片背景颜色
OPERATORCARD_COLORS = [MISTYROSE, PALETURQUOISE]
# --运算符卡片字体路径与大小
OPERATORFONT = [FONTPATH, 30]
# --运算符卡片位置
OPERATORCARD_POSITIONS = [(230, 300, 50, 50), (330, 300, 50, 50), (430, 300, 50, 50), (530, 300, 50, 50)]
# 按钮卡片
# --按钮类型
BUTTONS = ['RESET', 'ANSWERS', 'NEXT']
# --按钮卡片字体颜色
BUTTONFONT_COLORS = [BLACK, BLACK]
# --按钮卡片背景颜色
BUTTONCARD_COLORS = [MISTYROSE, PALETURQUOISE]
# --按钮卡片字体路径与大小
BUTTONFONT = [FONTPATH, 30]
# --按钮卡片位置
BUTTONCARD_POSITIONS = [(25, 400, 700/3, 150), (50+700/3, 400, 700/3, 150), (75+1400/3, 400, 700/3, 150)]
# 屏幕大小
SCREENSIZE = (800, 600)
# 卡片类型
GROUPTYPES = ['NUMBER', 'OPREATOR', 'BUTTON']
#如上是上一步新增的该项目所需变量

class Card(pygame.sprite.Sprite):
    '''
    Function:
        卡片类
    Initial Args:
        --x,y: 左上角坐标
        --width: 宽
        --height: 高
        --text: 文本
        --font: [字体路径, 字体大小]
        --font_colors(list): 字体颜色
        --bg_colors(list): 背景色
    '''
    def __init__(self, x, y, width, height, text, font, font_colors, bg_colors, attribute, **kwargs): # 初始化成员属性
        pygame.sprite.Sprite.__init__(self)
        self.rect = pygame.Rect(x, y, width, height)
        self.text = text
        self.attribute = attribute
        self.font_info = font
        self.font = pygame.font.Font(font[0], font[1])
        self.font_colors = font_colors
        self.is_selected = False
        self.select_order = None
        self.bg_colors = bg_colors

    def draw(self, screen, mouse_pos):
        '''
        画到屏幕上
        '''
        pygame.draw.rect(screen, self.bg_colors[1], self.rect, 0)
        if self.rect.collidepoint(mouse_pos):  # 如果鼠标点击卡片，则改变背景颜色
            pygame.draw.rect(screen, self.bg_colors[0], self.rect, 0)
        font_color = self.font_colors[self.is_selected]
        text_render = self.font.render(self.text, True, font_color)
        font_size = self.font.size(self.text)
        screen.blit(text_render, (self.rect.x+(self.rect.width-font_size[0])/2, self.rect.y+(self.rect.height-font_size[1])/2))

class Button(Card):
    '''
    按钮类，和卡片类似，初识参数都一致
    '''
    def __init__(self, x, y, width, height, text, font, font_colors, bg_colors, attribute, **kwargs):
        Card.__init__(self, x, y, width, height, text, font, font_colors, bg_colors, attribute)

    def do(self, game24_gen, func, sprites_group, objs):
        '''
        根据button function执行响应操作
        '''
        if self.attribute == 'NEXT': # “下一个”按钮，生成新的24点游戏
            for obj in objs:
                obj.font = pygame.font.Font(obj.font_info[0], obj.font_info[1])
                obj.text = obj.attribute
            self.font = pygame.font.Font(self.font_info[0], self.font_info[1])
            self.text = self.attribute
            game24_gen.generate()  # 生成新的24点游戏
            sprites_group = func(game24_gen.numbers_now)
        elif self.attribute == 'RESET':  # ”重置“按钮， 重新开始当前24点游戏
            for obj in objs:
                obj.font = pygame.font.Font(obj.font_info[0], obj.font_info[1])
                obj.text = obj.attribute
            game24_gen.numbers_now = game24_gen.numbers_ori # 数据变为原始数据
            game24_gen.answers_idx = 0  # 答案列表下标变为0
            sprites_group = func(game24_gen.numbers_now) # 重新开始本次游戏
        elif self.attribute == 'ANSWERS':  # ”答案“按钮，提示用户当前24点游戏答案的正解
            self.font = pygame.font.Font(self.font_info[0], 20)
            self.text = '[%d/%d]: ' % (game24_gen.answers_idx+1, len(game24_gen.answers)) + game24_gen.answers[game24_gen.answers_idx]
            game24_gen.answers_idx = (game24_gen.answers_idx+1) % len(game24_gen.answers)
        else:
            raise ValueError('Button.attribute unsupport <%s>, expect <%s>, <%s> or <%s>...' % (self.attribute, 'NEXT', 'RESET', 'ANSWERS'))
        return sprites_group

class Game24Generator():
    '''
    24点游戏生成器
    '''
    def __init__(self):
        self.info = 'game24Generator'

    def __reset(self):
        '''
        重置
        '''
        self.answers = []
        self.numbers_ori = []
        self.numbers_now = []
        self.target = 24.
        self.answers_idx = 0

    def generate(self):
        '''
        生成器
        '''
        self.__reset()
        while True:
            self.numbers_ori = [random.randint(1, 10) for i in range(4)]  # 原始数据
            self.numbers_now = copy.deepcopy(self.numbers_ori)     # 复制来的当前数据
            #self.answers = True # 首先默认为True，这样会输出随机生成的结果
            self.answers = self.__verify()  # 答案列表
            if self.answers:
                break

    def check(self):
        '''
        只剩下一个数字时检查是否为24
        '''
        if len(self.numbers_now) == 1 and float(self.numbers_now[0]) == self.target:
            return True
        return False

    def __verify(self):
        '''
        验证生成的数字是否有答案，主要就是经过不断循环四则运算，检测是否有符合要求的运算
        '''
        answers = []
        for item in self.__iter(self.numbers_ori, len(self.numbers_ori)):
            item_dict = []
            list(map(lambda i: item_dict.append({str(i): i}), item))
            #  一共有两种计算方法如下，由于有乘除法，所以第一组方法是顺序计算最后再计算，第二组方法是两两配对最后再计算
            solution1 = self.__func(self.__func(self.__func(item_dict[0], item_dict[1]), item_dict[2]), item_dict[3])
            solution2 = self.__func(self.__func(item_dict[0], item_dict[1]), self.__func(item_dict[2], item_dict[3]))
            solution = dict()
            solution.update(solution1)
            solution.update(solution2)
            for key, value in solution.items():
                if float(value) == self.target:
                    answers.append(key)  # 添加到答案列表
        # 避免有数字重复时表达式重复
        answers = list(set(answers))
        return answers

    def __iter(self, items, n):
        '''
        递归枚举
        '''
        for idx, item in enumerate(items):
            if n == 1:
                yield [item]
            else:
                for each in self.__iter(items[:idx] + items[idx + 1:], n - 1):
                    yield [item] + each

    def __func(self, a, b):
        '''
        计算函数
        '''
        res = dict()
        for key1, value1 in a.items():
            for key2, value2 in b.items():  # 分别计算两个数的加减乘除
                res.update({'(' + key1 + '+' + key2 + ')': value1 + value2})
                res.update({'(' + key1 + '-' + key2 + ')': value1 - value2})
                res.update({'(' + key2 + '-' + key1 + ')': value2 - value1})
                res.update({'(' + key1 + '×' + key2 + ')': value1 * value2})
                value2 > 0 and res.update({'(' + key1 + '÷' + key2 + ')': value1 / value2})
                value1 > 0 and res.update({'(' + key2 + '÷' + key1 + ')': value2 / value1})
        return res

def getNumberSpritesGroup(numbers):
    '''
    获取数字精灵组
    '''
    number_sprites_group = pygame.sprite.Group()
    for idx, number in enumerate(numbers):
        args = (*NUMBERCARD_POSITIONS[idx], str(number), NUMBERFONT, NUMBERFONT_COLORS, NUMBERCARD_COLORS, str(number))
        number_sprites_group.add(Card(*args))
    return number_sprites_group

def getOperatorSpritesGroup(operators):
    '''
    获取运算符精灵组
    '''
    operator_sprites_group = pygame.sprite.Group()
    for idx, operator in enumerate(operators):
        args = (*OPERATORCARD_POSITIONS[idx], str(operator), OPERATORFONT, OPREATORFONT_COLORS, OPERATORCARD_COLORS, str(operator))
        operator_sprites_group.add(Card(*args))
    return operator_sprites_group

def getButtonSpritesGroup(buttons):
    '''
    获取按钮精灵组
    '''
    button_sprites_group = pygame.sprite.Group()
    for idx, button in enumerate(buttons):
        args = (*BUTTONCARD_POSITIONS[idx], str(button), BUTTONFONT, BUTTONFONT_COLORS, BUTTONCARD_COLORS, str(button))
        button_sprites_group.add(Button(*args))
    return button_sprites_group

def checkClicked(group, mouse_pos, group_type='NUMBER'):
    '''
    检查控件是否被点击
    '''
    selected = []
    # 数字卡片/运算符卡片
    if group_type == GROUPTYPES[0] or group_type == GROUPTYPES[1]:
        max_selected = 2 if group_type == GROUPTYPES[0] else 1  # 如果是数据卡片，则最大选择2个，如果是运算符卡片则最大选择一个
        num_selected = 0
        for each in group:
            num_selected += int(each.is_selected)  # 选择的数字卡片数量
        for each in group:
            if each.rect.collidepoint(mouse_pos):  # 卡片是否被鼠标点击
                if each.is_selected:  # 原始被选中再次被点击
                    each.is_selected = not each.is_selected  # 取消选中
                    num_selected -= 1 # 选择的卡片数量减一
                    each.select_order = None
                else:  # 原始没被选中
                    if num_selected < max_selected: # 没有超过最大可选中数
                        each.is_selected = not each.is_selected # 选中
                        num_selected += 1 # 选中的卡片数量加一
                        each.select_order = str(num_selected)
            if each.is_selected:
                selected.append(each.attribute)
    # 按钮卡片
    elif group_type == GROUPTYPES[2]:
        for each in group:
            if each.rect.collidepoint(mouse_pos):
                each.is_selected = True
                selected.append(each.attribute) # 增加到选择列表
    # 抛出异常
    else:
        raise ValueError('checkClicked.group_type unsupport <%s>, expect <%s>, <%s> or <%s>...' % (group_type, *GROUPTYPES))
    return selected

def calculate(number1, number2, operator):
    '''
    计算
    '''
    operator_map = {'+': '+', '-': '-', '×': '*', '÷': '/'}
    try:
        result = str(eval(number1+operator_map[operator]+number2))
        return result if '.' not in result else str(Fraction(number1+operator_map[operator]+number2))
    except:
        return None

def showInfo(text, screen):
    '''
    在屏幕上显示信息
    '''
    rect = pygame.Rect(200, 180, 400, 200)
    pygame.draw.rect(screen, PAPAYAWHIP, rect)
    font = pygame.font.Font(FONTPATH, 40)
    text_render = font.render(text, True, BLACK)
    font_size = font.size(text)
    screen.blit(text_render, (rect.x+(rect.width-font_size[0])/2,
                              rect.y+(rect.height-font_size[1])/2))

def main():
    '''
    主函数
    '''
    # 初始化, 导入必要的游戏素材
    pygame.init()
    pygame.mixer.init()
    screen = pygame.display.set_mode(SCREENSIZE)
    pygame.display.set_caption('24点小游戏-大师码')
    win_sound = pygame.mixer.Sound(AUDIOWINPATH)
    lose_sound = pygame.mixer.Sound(AUDIOLOSEPATH)
    warn_sound = pygame.mixer.Sound(AUDIOWARNPATH)
    pygame.mixer.music.load(BGMPATH)
    pygame.mixer.music.play(-1, 0.0)

    # 24点游戏生成器
    game24_gen = Game24Generator()
    game24_gen.generate()

    # 精灵组
    # --数字
    number_sprites_group = getNumberSpritesGroup(game24_gen.numbers_now)
    # --运算符
    operator_sprites_group = getOperatorSpritesGroup(OPREATORS)
    # --按钮
    button_sprites_group = getButtonSpritesGroup(BUTTONS)

    # 游戏主循环
    clock = pygame.time.Clock()
    selected_numbers = []
    selected_operators = []
    selected_buttons = []
    is_win = False

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT: # 是否退出
                pygame.quit()
                sys.exit(-1)

            elif event.type == pygame.MOUSEBUTTONUP: # 检测鼠标是否点击三个不同的按钮
                mouse_pos = pygame.mouse.get_pos()
                selected_numbers = checkClicked(number_sprites_group, mouse_pos, 'NUMBER')
                selected_operators = checkClicked(operator_sprites_group, mouse_pos, 'OPREATOR')
                selected_buttons = checkClicked(button_sprites_group, mouse_pos, 'BUTTON')

        screen.fill(AZURE)

        # 更新数字
        if len(selected_numbers) == 2 and len(selected_operators) == 1: # 选择两个操作数和一个操作符
            noselected_numbers = []

            for each in number_sprites_group: # 遍历数字组
                if each.is_selected: # 如果被选中
                    if each.select_order == '1': # 选中的次序
                        selected_number1 = each.attribute
                    elif each.select_order == '2':
                        selected_number2 = each.attribute
                    else:
                        raise ValueError('Unknow select_order <%s>, expect <1> or <2>...' % each.select_order)
                else: # 未被选中则加入未选中列表
                    noselected_numbers.append(each.attribute)
                each.is_selected = False

            for each in operator_sprites_group: # 遍历字符组
                each.is_selected = False

            result = calculate(selected_number1, selected_number2, *selected_operators) # 计算结果
            if result is not None:
                game24_gen.numbers_now = noselected_numbers + [result]  # 更新数字为Result
                is_win = game24_gen.check() # 检测是否胜利
                if is_win:
                    win_sound.play() # 播放胜利音效
                if not is_win and len(game24_gen.numbers_now) == 1: # 如果没赢并且只剩下一个数字还不是24说明游戏失败
                    lose_sound.play() # 播放失败音效
            else:
                warn_sound.play() # 结果为空，播放警告音效

            selected_numbers = []
            selected_operators = []
            number_sprites_group = getNumberSpritesGroup(game24_gen.numbers_now) # 由于数字在逐渐变少，因此需要重新绘出数字精灵组

        # 精灵都画到screen上
        for each in number_sprites_group:
            each.draw(screen, pygame.mouse.get_pos())  # 画出数字精灵
        for each in operator_sprites_group:
            each.draw(screen, pygame.mouse.get_pos())  # 画出操作符精灵
        for each in button_sprites_group:
            if selected_buttons and selected_buttons[0] in ['RESET', 'NEXT']:
                is_win = False # 如果选择按钮是'RESET'或者'NEXT'，则将成功标志设为否
            if selected_buttons and each.attribute == selected_buttons[0]:
                each.is_selected = False
                # 根据button function执行响应操作
                number_sprites_group = each.do(game24_gen, getNumberSpritesGroup, number_sprites_group, button_sprites_group)
                selected_buttons = []
            each.draw(screen, pygame.mouse.get_pos()) # 画出按钮精灵
        # 游戏胜利
        if is_win:
            showInfo('Congratulations', screen)
        # 游戏失败
        if not is_win and len(game24_gen.numbers_now) == 1:
            showInfo('Game Over', screen)
        pygame.display.update()
        clock.tick(30)


if __name__ == '__main__':
    main()