import random
import heapq
import os
import time
import json
import math
import numpy as np
from datetime import datetime

class InvalidMapError(Exception):
    """自定义异常,用于表示地图设置无效"""
    pass

# -------------------------- 柏林噪声核心代码 --------------------------
def generate_permutation(seed=None):
    """生成带种子的置换表,长度为256"""
    permutation = list(range(256))
    random.seed(seed)
    for i in range(255, 0, -1):
        j = random.randint(0, i)
        permutation[i], permutation[j] = permutation[j], permutation[i]
    return permutation

def fade(t):
    return t * t * t * (t * (t * 6 - 15) + 10)

def lerp(t, a, b):
    return a + t * (b - a)

def grad(hash_val, x, y):
    h = hash_val & 3
    if h == 0:
        return x + y
    elif h == 1:
        return -x + y
    elif h == 2:
        return x - y
    else:
        return -x - y

def perlin_noise_2d(x, y, permutation):
    p = permutation * 2
    X = math.floor(x) & 255
    Y = math.floor(y) & 255
    x -= math.floor(x)
    y -= math.floor(y)
    u = fade(x)
    v = fade(y)

    A = p[X] + Y
    B = p[X + 1] + Y

    n0 = grad(p[A], x, y)
    n1 = grad(p[B], x - 1, y)
    nx0 = lerp(u, n0, n1)
    n2 = grad(p[A + 1], x, y - 1)
    n3 = grad(p[B + 1], x - 1, y - 1)
    nx1 = lerp(u, n2, n3)
    return lerp(v, nx0, nx1)

def octave_perlin_2d(x, y, permutation, octaves=4, persistence=0.5, lacunarity=2.0):
    total = 0
    frequency = 1
    amplitude = 1
    max_value = 0
    for _ in range(octaves):
        total += perlin_noise_2d(x * frequency, y * frequency, permutation) * amplitude
        max_value += amplitude
        amplitude *= persistence
        frequency *= lacunarity
    return total / max_value

# -------------------------- 迷宫地图类 --------------------------
class CharacterMap:
    def __init__(self, width, a, height, sections, obstacles=None, special_obstacles=None, goal=None, player_position=None, use_file_settings=False, auto_save=False, noise_seed=123):
        print("正在生成地图,设置数据. . .")
        self.width = width
        self.height = a
        self.section_width = sections
        self.section_height = height
        self.view_center = [self.section_width // 2, self.section_height // 2]
        self.map = [['.' for _ in range(width)] for _ in range(a)]
        self.player_position = player_position if player_position else [random.randint(0, self.view_center[0]), random.randint(0, self.view_center[1])]
        self.goal = goal if goal else [random.randint(0, width - 1), random.randint(0, a - 1)]
        while self.goal == self.player_position:
            self.goal = [random.randint(0, width - 1), random.randint(0, a - 1)]
        self.steps = 0
        self.obstacles = obstacles if obstacles else []
        self.special_obstacles = special_obstacles if special_obstacles else []
        self.path = []
        self.auto_save = auto_save
        self.noise_seed = noise_seed  # 柏林噪声种子
        self.permutation = generate_permutation(noise_seed)  # 生成置换表

        if not use_file_settings:
            while not self.generate_obstacles():
                
                pass
        else:
            if not self.a_star(tuple(self.player_position), tuple(self.goal)):
                print("%--警告:从文件加载的地图无法到达终点!--%")
            try:
                self.update_map()
            except:
                print('此文件更新错误')
                raise InvalidMapError("地图设置无效")
        print("完成")
        time.sleep(0.5)
        self.update_map()

    def generate_obstacles(self):
        """使用柏林噪声生成自然分布的障碍物"""
        self.map = [['.' for _ in range(self.width)] for _ in range(self.height)]
        self.obstacles.clear()
        self.special_obstacles.clear()

        # ========== 已调好的参数 ==========
        noise_scale = 15.0       # 控制纹理粗细
        octaves = 3              # 减少细节,避免过密
        obstacle_threshold = 0.5 # 普通障碍物阈值
        special_threshold = 0.7  # 特殊障碍物阈值

        # 遍历地图生成障碍物
        for y in range(self.height):
            for x in range(self.width):
                noise_val = octave_perlin_2d(
                    x / noise_scale, 
                    y / noise_scale, 
                    self.permutation, 
                    octaves=octaves
                )
                noise_val = (noise_val + 1) / 2  # 归一化到0-1

                if [x, y] == self.player_position or [x, y] == self.goal:
                    continue

                if noise_val < obstacle_threshold:
                    self.obstacles.append([x, y])
                elif noise_val > special_threshold:
                    self.special_obstacles.append([x, y])

        # ========== 新增的障碍物保底机制 ==========
        min_obstacle_count = int(self.width * self.height * 0.50)
        if len(self.obstacles) < min_obstacle_count:
            while len(self.obstacles) < min_obstacle_count:
                x = random.randint(0, self.width-1)
                y = random.randint(0, self.height-1)
                if [x,y] not in self.obstacles and [x,y] != self.player_position and [x,y] != self.goal:
                    self.obstacles.append([x,y])

        # 检查路径是否可行
        path = self.a_star(tuple(self.player_position), tuple(self.goal))
        return path is not None

    def zdyd(self):
        if self.path == []:
            self.show_shortest_path()
        szx = len(self.path)
        szx_pd = 0
        for x, y in self.path:
            print('自动寻路中...')
            os.system('clear')
            if szx_pd == szx - 1:
                self.path = []
                self.map[self.player_position[1]][self.player_position[0]] = '♛'
                self.update_map()
                break
            self.player_position = [x, y]
            self.map = [['.' for _ in range(self.width)] for _ in range(self.height)]
            for x, y in self.obstacles:
                self.map[y][x] = '#'
            for x, y in self.special_obstacles:
                self.map[y][x] = 'ᐃ'
            self.map[self.player_position[1]][self.player_position[0]] = '●'
            self.map[self.goal[1]][self.goal[0]] = '♞'
            self.print_map()
            self.steps += 1
            szx_pd += 1
            time.sleep(0.2)
        print('完成')

    def update_map(self):
        self.map = [['.' for _ in range(self.width)] for _ in range(self.height)]
        for x, y in self.obstacles:
            self.map[y][x] = '#'
        for x, y in self.special_obstacles:
            self.map[y][x] = 'ᐃ'
        for x, y in self.path:
            self.map[y][x] = '■'
        self.map[self.player_position[1]][self.player_position[0]] = '♛'
        self.map[self.goal[1]][self.goal[0]] = '♞'

    def print_map(self):
        start_x = max(0, self.player_position[0] - self.view_center[0])
        end_x = start_x + self.section_width
        if end_x > self.width:
            end_x = self.width
            start_x = self.width - self.section_width

        start_y = max(0, self.player_position[1] - self.view_center[1])
        end_y = start_y + self.section_height
        if end_y > self.height:
            end_y = self.height
            start_y = self.height - self.section_height

        top_border = '╔' + '═' * int((self.section_width) * 2) + '╗'
        print(top_border)

        for row in self.map[start_y:end_y]:
            print('║ ' + ' '.join(row[start_x:end_x]) + '║')

        bottom_border = '╚' + '═' * int((self.section_width) * 2) + '╝'
        print(bottom_border)
        print(f"步数:{self.steps}")
        print()

    def move(self, directionx):
        x, y = self.player_position
        new_x, new_y = x, y
        for direction in directionx:
            if direction == '4':
                new_x -= 1
            elif direction == '8':
                new_y += 1
            elif direction == '6':
                new_x += 1
            elif direction == '2':
                new_y -= 1
            elif direction == '1':
                new_x -= 1
                new_y -= 1
            elif direction == '3':
                new_x += 1
                new_y -= 1
            elif direction == '7':
                new_x -= 1
                new_y += 1
            elif direction == '9':
                new_x += 1
                new_y += 1
            else:
                print("无效的方向!")
                return

            if not (0 <= new_x < self.width and 0 <= new_y < self.height):
                print("你不能走出地图边界!")
                return

            if [new_x, new_y] in self.obstacles:
                print("前面有障碍物,不能通过!")
                return

            if [new_x, new_y] in self.special_obstacles:
                print("碰到特殊障碍物,你被弹回了!")
                self.player_position = [random.randint(0, self.view_center[0]), random.randint(0, self.view_center[1])]
                self.update_map()
                self.steps += 1
                return

            self.player_position = [new_x, new_y]
            self.steps += 1

            if self.player_position == self.goal:
                print("恭喜你到达终点!游戏结束!")
                print(f"总步数:{self.steps}")
                return self.steps

            self.random_move_goal()
            self.path = []
            self.update_map()
            self.print_map()
            time.sleep(0.1)
        os.system('clear')

    def random_move_goal(self):
        directions = [
            (-1, -1), (-1, 0), (-1, 1),
            (0, -1), (0, 1),
            (1, -1), (1, 0), (1, 1)
        ]
        possible_positions = []
        for dx, dy in directions:
            new_goal_x = self.goal[0] + dx
            new_goal_y = self.goal[1] + dy
            if 0 <= new_goal_x < self.width and 0 <= new_goal_y < self.height:
                if [new_goal_x, new_goal_y] not in self.obstacles and \
                   [new_goal_x, new_goal_y] not in self.special_obstacles and \
                   [new_goal_x, new_goal_y] != self.player_position:
                    possible_positions.append([new_goal_x, new_goal_y])
        if possible_positions:
            self.goal = random.choice(possible_positions)

    def run(self):
        print("欢迎来到迷宫地图!")
        print("使用数字控制方向:")
        print("1 - 左上,2 - 上,3 - 右上,4 - 左,6 - 右,7 - 左下,8 - 下,9 - 右下")
        print("输入'0'退出游戏。")
        print("输入'5'获取当前位置到终点的最短路径。")
        print("输入'@'自动寻路到终点。")
        while True:
            self.print_map()
            direction = input("请输入方向:").strip().lower()
            os.system('clear')
            if direction == '0':
                print("退出游戏。")
                return -1
            elif direction == '5':
                self.show_shortest_path()
            elif direction == '@':
                self.zdyd()
            else:
                bs = self.move(direction)
                if bs is not None:
                    if self.auto_save:
                        self.save_map_data()
                    return bs

    def save_map_data(self):
        current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
        file_name = f"{current_time}.json"
        file_path = f"/storage/emulated/0/Download/python.wj/gwzm_wj/{file_name}"
        os.makedirs(os.path.dirname(file_path), exist_ok=True)
        data = {
            "width": self.width,
            "a": self.height,
            "height": self.section_height,
            "sections": self.section_width,
            "obstacles": self.obstacles,
            "special_obstacles": self.special_obstacles,
            "player_position": self.player_position,
            "goal": self.goal,
            "noise_seed": self.noise_seed  # 保存噪声种子
        }
        with open(file_path, 'w') as file:
            json.dump(data, file, indent=4)
        print(f"地图数据已保存到 {file_path}")

    def show_shortest_path(self):
        start = tuple(self.player_position)
        goal = tuple(self.goal)
        self.path = self.a_star(start, goal)

        if self.path:
            self.update_map()
            self.print_map()
            print("已显示从当前位置到终点的最短路径。")
        else:
            self.path = []
            self.update_map()
            self.print_map()
            print("无法到达终点!")


    def a_star(self, start, goal):
        open_set = []
        heapq.heappush(open_set, (0, start))
        came_from = {}
        g_score = {start: 0}
        f_score = {start: self.heuristic(start, goal)}

        while open_set:
            # 弹出f_score最小的节点
            current_f, current = heapq.heappop(open_set)

            # 避免重复处理已最优的节点
            if current in f_score and current_f > f_score[current]:
                continue

            # 到达终点,重构路径
            if current == goal:
                return self.reconstruct_path(came_from, current)

            # 遍历所有邻居并校验合法性
            for neighbor in self.get_neighbors(current):
                # 跳过障碍物和特殊障碍物
                if list(neighbor) in self.obstacles or list(neighbor) in self.special_obstacles:
                    continue

                # 计算移动代价:斜向√2≈1.414,正交1
                dx = abs(neighbor[0] - current[0])
                dy = abs(neighbor[1] - current[1])
                move_cost = 1.414 if dx == 1 and dy == 1 else 1

                # 计算从起点到邻居的总代价
                tentative_g_score = g_score[current] + move_cost

                # 仅当新路径更优时,才更新节点信息
                if neighbor not in g_score or tentative_g_score < g_score[neighbor]:
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g_score
                    f_score[neighbor] = tentative_g_score + self.heuristic(neighbor, goal)
                    heapq.heappush(open_set, (f_score[neighbor], neighbor))

        # 无路径时返回空列表
        return []


    def heuristic(self, a, b):
        # 切比雪夫距离:适配8方向移动,更精准
        dx = abs(a[0] - b[0])
        dy = abs(a[1] - b[1])
        return max(dx, dy)


    def get_neighbors(self, position):
        x, y = position
        # 8方向邻居
        directions = [(-1,-1), (-1,0), (-1,1),
                      (0,-1),          (0,1),
                      (1,-1),  (1,0), (1,1)]
        valid_neighbors = []
        for dx, dy in directions:
            nx = x + dx
            ny = y + dy
            # 严格校验边界
            if 0 <= nx < self.width and 0 <= ny < self.height:
                valid_neighbors.append((nx, ny))
        return valid_neighbors


    def reconstruct_path(self, came_from, current):
        path = []
        # 从终点回溯到起点
        while current in came_from:
            path.append(list(current))
            current = came_from[current]
        # 添加起点
        path.append(list(current))
        # 反转路径,变为 起点→终点 顺序
        path.reverse()
        # 去重(防止回溯时的重复节点)
        path = [list(item) for item in list(dict.fromkeys(tuple(p) for p in path))]
        return path


# -------------------------- 原有辅助函数 --------------------------
def 初始设置():
    settings = {
        "width": {"default": 60, "prompt": "地图总宽,默认60>>>", "validator": lambda x: int(x) > 0},
        "a": {"default": 60, "prompt": "地图总高,默认60>>>", "validator": lambda x: int(x) > 0},
        "sections": {"default": 19, "prompt": "可视图宽,此项不建议更改,默认19>>>", "validator": lambda x: int(x) <= settings["width"]["value"]},
        "height": {"default": 15, "prompt": "可视图高,此项不建议更改,默认15>>>", "validator": lambda x: int(x) <= settings["a"]["value"]}
    }
    for key, config in settings.items():
        while True:
            os.system('clear')
            print("         欢迎\n让我们来做一些基础设置,回车使用默认")
            value = input(config["prompt"])
            if value == "":
                value = str(config["default"])
            if config["validator"](value):
                settings[key]["value"] = int(value)
                break
            else:
                print("注意,此设置不符合要求")
                time.sleep(0.5)
    return settings["width"]["value"], settings["a"]["value"], settings["height"]["value"], settings["sections"]["value"]

def 从文件加载设置(file_path):
    try:
        with open(file_path, 'r') as file:
            data = json.load(file)
            width = data.get('width', 60)
            a = data.get('a', 60)
            if int(width) < 0 or int(a) < 0:
                print("地图总不为负数")
                return
            height = data.get('height', 15)
            sections = data.get('sections', 19)
            if int(height) > width or int(sections) > a:
                print("可视图大小大于地图")
                return
            obstacles = data.get('obstacles', [])
            special_obstacles = data.get('special_obstacles', [])
            player_position = data.get('player_position', [random.randint(0, sections // 2), random.randint(0, height // 2)])
            goal = data.get('goal', [random.randint(0, width - 1), random.randint(0, a - 1)])
            noise_seed = data.get('noise_seed', 123)
            return width, a, height, sections, obstacles, special_obstacles, goal, player_position, noise_seed
    except FileNotFoundError:
        return -1
    except json.JSONDecodeError:
        return -2
    except:
        return -3

def yrun(bcmr, auto_save):
    if bcmr:
        print("已使用默认设置")
        width, a, height, sections, obstacles, special_obstacles, goal, player_position, noise_seed = 60, 60, 15, 19, [], [], [random.randint(0, 59), random.randint(0, 59)], [random.randint(0, 9), random.randint(0, 7)], 123
        use_file_settings = False
    else:
        use_file_settings = False
        width, a, height, sections = 初始设置()
        os.system('clear')
        noise_seed = random.randint(0, 9999)  # 随机生成噪声种子
    print(f"加载完成: \n宽度={width}\n高度={a}\n可视图高={height}\n可视图宽={sections}\n噪声种子={noise_seed}")
    os.system('clear')
    times, bs = zrun(width, a, height, sections, obstacles=None, special_obstacles=None, goal=None, player_position=None, use_file_settings=use_file_settings, auto_save=auto_save, noise_seed=noise_seed)
    return times, bs

def zrun(width, a, height, sections, obstacles, special_obstacles, goal, player_position, use_file_settings, auto_save, noise_seed=123):
    try:
        timea = time.time()
        game_map = CharacterMap(width, a, height, sections, obstacles, special_obstacles, goal, player_position, use_file_settings, auto_save, noise_seed)
        bs = game_map.run()
        timeb = time.time() - timea
    except InvalidMapError as e:
        print("错误", e)
        return None, None
    return timeb, bs

def 设置(bcmr, auto_save):
    while True:
        print('''
    ╔══════════════════···
    ║1.保持地图默认设置:{}
    ║2.自动保存地图:{}
    ║回车.保存并退出
    ╚══════════════════···'''.format(bcmr, auto_save))
        srsz = input('请选择>>>')
        if srsz == "1":
            os.system('clear')
            bcmr = not bcmr
        elif srsz == "2":
            os.system('clear')
            auto_save = not auto_save
        elif srsz == "":
            os.system('clear')
            break
        else:
            os.system('clear')
            print("输入错误")
    return bcmr, auto_save

def kgx(bs):
    if bs > -1:
        kgx = len(str(bs))
        if kgx == 1:
            kgy = 2
        elif kgx == 2:
            kgy = 1
        elif kgx == 3:
            kgy = 0
        kg = " " * kgy
        return kg
    return "  "

def 选择文件界面():
    print('''
    ╔══════════════════════╗
    ║  1.手动输入路径      ║
    ╠══════════════════════╣
    ''')
    file_path = "/storage/emulated/0/Download/python.wj/gwzm_wj/"
    if not os.path.exists(file_path):
        os.makedirs(file_path)
    files = os.listdir(file_path)
    json_files = [f for f in files if f.endswith('.json')]
    for idx, file_name in enumerate(json_files, start=2):
        print(f"║ {idx}. {file_name}{' ' * (30 - len(file_name))}║")
    print('''
    ╚══════════════════════╝
    ''')
    return json_files

def xrun():
    bs = -1
    kg = " "
    bcmr = False
    auto_save = False
    times = -1
    os.system('clear')
    while True:
        print('''
          ╔══════════════════╗
          ║ 1.开始游戏       ║
          ║ 2.进入设置       ║
          ║ 3.从文件加载地图 ║
          ╠══════════════════╣
          ║上一局步数:{}{}    ║
          ║上一局时间:{}s
          ╚══════════════════╝'''.format(bs, kg, round(times, 3)))
        xzxz = input("\n请输入你的选择>>>")
        if xzxz == "1":
            os.system('clear')
            times, bs = yrun(bcmr, auto_save)
            kg = kgx(bs)
        elif xzxz == "2":
            os.system('clear')
            bcmr, auto_save = 设置(bcmr, auto_save)
        elif xzxz == "3":
            os.system('clear')
            json_files = 选择文件界面()
            choice = input("请选择一个选项>>>")
            if choice == "1":
                file_name = input("请输入文件路径>>>")
                file_path = file_name
                if not file_name.endswith('.json'):
                    file_path += ".json"
            else:
                try:
                    choice = int(choice) - 2
                    if 0 <= choice < len(json_files):
                        file_name = json_files[choice]
                        file_path = os.path.join("/storage/emulated/0/Download/python.wj/gwzm_wj/", file_name)
                    else:
                        print("无效的选项")
                        continue
                except ValueError:
                    print("无效的选项")
                    continue
            try:
                path_sc = 从文件加载设置(file_path)
                if path_sc == -3:
                    print("输入错误")
                    continue
                elif path_sc == -1:
                    print("文件未找到")
                    continue
                elif path_sc == -2:
                    print("文件格式错误")
                    continue
                width, a, height, sections, obstacles, special_obstacles, goal, player_position, noise_seed = path_sc
                print(f"设置完成: \n宽度={width}\n高度={a}\n可视图高={height}\n可视图宽={sections}\n噪声种子={noise_seed}")
                times, bs = zrun(width, a, height, sections, obstacles, special_obstacles, goal, player_position, True, auto_save, noise_seed)
            except Exception as e:
                print(f"加载失败: {e}")
                times = None
            if times is None:
                times, bs = -1, -1
            kg = kgx(bs)
            if kg is None:
                kg = " "
        else:
            os.system('clear')
            print("输入错误")

# 开始运行
xrun()