井字棋--课后程序(Python程序开发案例教程-黑马程序员编著-第7章-课后作业)

news2024/11/26 5:39:41

实例2:井字棋

井字棋是一种在3 * 3格子上进行的连珠游戏,又称井字游戏。井字棋的游戏有两名玩家,其中一个玩家画圈,另一个玩家画叉,轮流在3 * 3格子上画上自己的符号,最先在横向、纵向、或斜线方向连成一条线的人为胜利方。如图1所示为画圈的一方为胜利者。

 

图1 井字棋

本实例要求编写程序,实现具有人机交互功能的井字棋。

实例目标

  1. 理解面向对象的思想
  2. 能独立设计类
  3. 掌握类的继承和父类方法的重写

实例分析

根据实例描述的井字棋游戏的规则,下面模拟一次游戏的流程如图2所示。

 

图2 井字棋游戏流程

图2中的描述的游戏流程如下:

  1. 重置棋盘数据,清理之前一轮的对局数据,为本轮对局做好准备。
  2. 显示棋盘上每个格子的编号,让玩家熟悉落子位置。
  3. 根据系统随机产生的结果确定先手玩家(先手使用X)。
  4. 当前落子一方落子。
  5. 显示落子后的棋盘。
  6. 判断落子一方是否胜利?若落子一方取得胜利,修改玩家得分,本轮对局结束,跳转至第(9)步。
  7. 判断是否和棋?若出现和棋,本轮对局结束,跳转至第(9)步。
  8. 交换落子方,跳转至第(4)步,继续本轮游戏。
  9. 显示玩家当前对局比分。

以上流程中,落子是游戏中的核心功能,如何落子则是体现电脑智能的关键步骤,实现智能落子有策略可循的。按照井字棋的游戏规则:当玩家每次落子后,玩家的棋子在棋盘的水平、垂直或者对角线任一方向连成一条直线,则表示玩家获胜。因此,我们可以将电脑的落子位置按照优先级分成以下三种:

(1)必胜落子位置

我方在该位置落子会获胜。一旦出现这种情况,显然应该毫不犹豫在这个位置落子。

(2)必救落子位置

对方在该位置落子会获胜。如果我方暂时没有必胜落子位置,那么应该在必救落子位置落子,以阻止对方获胜。

(3)评估子力价值

评估子力价值,就是如果在该位置落子获胜的几率越高,子力价值就越大;获胜的几率越低,子力价值就越小。

如果当前的棋盘上,既没有必胜落子位置,也没有必救落子位置,那么就应该针对棋盘上的每一个空白位置寻找子力价值最高的位置落子。

要编写一个评估子力价值的程序,需要考虑诸多因素,这里我们选择了一种简单评估子力价值的方式——只考虑某个位置在空棋盘上的价值,而不考虑已有棋子以及落子之后的盘面变化。下面来看一下在空棋盘上不同位置落子的示意图,如图3所示。

 

图3 棋盘落子示意图

观察图3不难发现,玩家在空棋盘上落子的位置可分为以下3种情况:

  1. 中心点,这个位置共有4个方向可能和其它棋子连接成直线,获胜的几率最高。
  2. 4个角位,这4个位置各自有3个方向可能和其它棋子连接成直线,获胜几率中等。
  3. 4个边位,这4个位置各自有2个方向可能和其它棋子连接成直线,获胜几率最低。

综上所述,如果电脑在落子时,既没有必胜落子位置,也没有必救落子位置时,我们就可以让电脑按照胜率的高低来选择落子位置,也就是说,若棋盘的中心点没有棋子,则选择中心点作为落子位置;若中心点已有棋子,而角位没有棋子,则随机选择一个没有棋子的角位作为落子位置;若中心点和四个角位都有棋子,而边位没有棋子,则随机选择一个没有棋子的边位作为落子位置。

井字棋游戏一共需要设计4个类,不同的类创建的对象承担不同的职责,分别是:

(1)游戏类(Game):负责整个游戏流程的控制,是该游戏的入口。

(2)棋盘类(Board):负责显示棋盘、记录本轮对局数据、以及判断胜利等和对弈相关的处理工作。

(3)玩家类(Player):负责记录玩家姓名、棋子类型和得分、以及实现玩家在棋盘上落子。

(4)电脑玩家类(AIPlayer):是玩家类的子类。在电脑玩家类类中重写玩家类的落子方法,在重写的方法中实现电脑智能选择落子位置的功能。

设计后的类图如图4所示。

 

图4 类结构图

本实例中涉及到多个类,为保证程序具有清晰的结构,可以将每个类的相关代码分别放置到与其同名的py文件中。另外,由于Player和AIPlayer类具有继承关系,可以将这两个类的代码放置到player.py文件中。

代码实现

本实例的实现过程如下所示。

  1. 创建项目

使用PyCharm创建一个名为“井字棋V1.0”的文件夹,在该文件夹下分别创建3个py文件,分别为board.py、game.py与player.py,此时程序的目录结构如图5所示。

 

图5 井字棋文件目录

由于棋盘类是井字棋游戏的重点,因此我们先开发Board类。

  1. 设计Board

(1)属性

井字棋的棋盘上共有9个格子落子,落子也是有位置可循的,因此这里使用列表作为棋盘的数据结构,列表中的元素则是棋盘上的棋子,它有以下三种取值:

  1. " " 表示没有落子,是初始值;
  2. "X" 表示玩家在该位置下了一个X的棋子;
  3. "O" 表示玩家在该位置下了一个O的棋子。

其中列表中的元素为" "的位置才允许玩家落子。为了让玩家明确可落子的位置,需要增加可落子列表。根据图4中设计的类图,在board.py文件中定义Board类,并在该类的构造方法中添加属性board_data和movable_list,具体代码如下。

class Board(object):

    """棋盘类"""

    def __init__(self):

        self.board_data = [" "] * 9          # 棋盘数据

        self.movable_list = list(range(9))  # 可移动列表

(2)show_board()方法

show_board()方法实现创建一个九宫格棋盘的功能。游戏过程中显示的棋盘分为两种情况,一种是新一轮游戏开始前显示的有索引的棋盘,让玩家明确棋盘格子与序号的对应关系;另一种是游戏中显示当前落子情况的棋盘,会在玩家每次落子后展示。在Board类中添加show_board()方法,并在该方法中传递一个参数show_index,用于设置是否在棋盘中显示索引(默认为False,表示不显示索引),具体代码如下。

def show_board(self, show_index=False):

    """显示棋盘

    :param show_index: True 表示显示索引 / False 表示显示数据

    """

    for i in (0, 3, 6):

        print("       |       |")

        if show_index:

            print("   %d   |   %d   |   %d" % (i, i + 1, i + 2))

        else:

            print("   %s   |   %s   |   %s" % (self.board_data[i],

                                             self.board_data[i + 1],

                                             self.board_data[i + 2]))

        print("       |       |")

        if i != 6:

            print("-" * 23)

(3)move_down ()方法

move_down ()方法实现在指定的位置落子的功能,该方法接收两个参数,分别是表示落子位置的index和表示落子类型(X或者O)的chess,接收的这些参数都是落子前需要考虑的必要要素,具体代码如下。

def move_down(self, index, chess):

    """在指定位置落子

    :param index: 列表索引

    :param chess: 棋子类型 X O

    """

    # 1. 判断 index 是否在可移动列表中

    if index not in self.movable_list:

        print("%d 位置不允许落子" % index)

        return

    # 2. 修改棋盘数据

    self.board_data[index] = chess

    # 3. 修改可移动列表

    self.movable_list.remove(index)

以上代码首先判断落子位置是否可以落子,如果可以就将棋子添加到board_data列表的对应位置,并从movable_list列表中删除。

(4)is_draw ()方法

is_draw ()方法实现判断游戏是否平局的功能,该方法会查看可落子索引列表中是否有值,若没有值表示棋盘中的棋子已经落满了,说明游戏平局,具体代码如下。

def is_draw(self):

    """是否平局"""

    return not self.movable_list

(5)is_win ()方法

is_draw ()方法实现判断游戏是否胜利的功能,该方法会先定义方向列表,再遍历方向列表判断游戏是否胜利,胜利则返回True,否则返回False,具体代码如下。

def is_win(self, chess, ai_index=-1):

    """是否胜利

    :param chess: 玩家的棋子

    :param ai_index: 预判索引,-1 直接判断当前棋盘数据

    """

    # 1. 定义检查方向列表

    check_dirs = [[0, 1, 2], [3, 4, 5], [6, 7, 8],

                     [0, 3, 6], [1, 4, 7], [2, 5, 8],

                     [0, 4, 8], [2, 4, 6]]

    # 2. 定义局部变量记录棋盘数据副本

    data = self.board_data.copy()

    # 判断是否预判胜利

    if ai_index > 0:

        data[ai_index] = chess

    # 3. 遍历检查方向列表判断是否胜利

    for item in check_dirs:

        if (data[item[0]] == chess and

            data[item[1]] == chess

                and data[item[2]] == chess):

            return True

    return False

注意,is_win()方法的ai_index参数的默认值为-1,表示无需进行预判,即提示玩家最有利的落子位置;若该参数不为-1时,表示需要进行预判。

(6)reset_board ()方法

reset_board ()方法实现清空棋盘的功能,该方法中会先清空movable_list,再将棋盘上的数据全部置为初始值,最后往movable_list中添加0~8的数字,具体代码如下。

def reset_board(self):

    """重置棋盘"""

    # 1. 清空可移动列表数据

    self.movable_list.clear()

    # 2. 重置数据

    for i in range(9):

        self.board_data[i] = " "

        self.movable_list.append(i)

  1. 设计Player

根据图4中设计的类图,在player.py文件中定义Player类,分别在该类中添加属性和方法,具体内容如下。

(1)属性

在Player类中添加name、score、chess属性,具体代码如下。

import board

import random

class Player(object):

    """玩家类"""

    def __init__(self, name):

        self.name = name     # 姓名

        self.score = 0       # 成绩

        self.chess = None   # 棋子

(2)move()方法

move()方法实现玩家在指定位置落子的功能,该方法中会先提示用户棋盘上可落子的位置,之后使棋盘根据用户选择的位置重置棋盘数据后进行显示,具体代码如下。

def move(self, chess_board):

    """在棋盘上落子

    :param chess_board:

    """

    # 1. 由用户输入要落子索引

    index = -1

    while index not in chess_board.movable_list:

        try:

            index = int(input(" %s 输入落子位置 %s" %

                (self.name, chess_board.movable_list)))

        except ValueError:

            pass

    # 2. 在指定位置落子

    chess_board.move_down(index, self.chess)

  1. 设计AIPlayer

根据图4中设计的类图,在player.py文件中定义继承自Player类的子类AIPlayer。AIPlayer类中重写了父类的move()方法,在该方法中需要增加分析中的策略,使得计算机玩家变得更加聪明,具体代码如下。

class AIPlayer(Player):

    """智能玩家"""

    def move(self, chess_board):

        """在棋盘上落子

        :param chess_board:

        """

        print("%s 正在思考落子位置..." % self.name)

        # 1. 查找我方必胜落子位置

        for index in chess_board.movable_list:

            if chess_board.is_win(self.chess, index):

                print("走在 %d 位置必胜!!!" % index)

                chess_board.move_down(index, self.chess)

                return

        # 2. 查找地方必胜落子位置-我方必救位置

        other_chess = "O" if self.chess == "X" else "X"

        for index in chess_board.movable_list:

            if chess_board.is_win(other_chess, index):

                print("敌人走在 %d 位置必输,火速堵上!" % index)

                chess_board.move_down(index, self.chess)

                return

        # 3. 根据子力价值选择落子位置

        index = -1

        # 没有落子的角位置列表

        corners = list(set([0, 2, 6, 8]).intersection(

chess_board.movable_list))

        # 没有落子的边位置列表

        edges = list(set([1, 3, 5, 7]).intersection(

chess_board.movable_list))

        if 4 in chess_board.movable_list:

            index = 4

        elif corners:

            index = random.choice(corners)

        elif edges:

            index = random.choice(edges)

        # 在指定位置落子

        chess_board.move_down(index, self.chess)

  1. 设计Game

根据图4中设计的类图,在game.py文件中定义Game类,分别在该类中添加属性和方法,具体内容如下。

(1)属性

在Game类中添加chess_board、human、computer属性,具体代码如下。

import random

import board

import player

class Game(object):

    """游戏类"""

    def __init__(self):

        self.chess_board = board.Board()          # 棋盘对象

        self.human = player.Player("玩家")        # 人类玩家对象

        self.computer = player.AIPlayer("电脑")  # 电脑玩家对象

(2)random_player()方法

random_player()方法实现随机生成先手玩家的功能,该方法中会先随机生成0和1两个数,选到数字1的玩家为先手玩家,然后再为两个玩家设置棋子类型,即先手玩家为“X”,对手玩家为“O”,具体代码如下。

def random_player(self):

    """随机先手玩家

    :return: 落子先后顺序的玩家元组

    """

    # 随机到 1 表示玩家先手

    if random.randint(0, 1) == 1:

        players = (self.human, self.computer)

    else:

        players = (self.computer, self.human)

    # 设置玩家棋子

    players[0].chess = "X"

    players[1].chess = "O"

    print("根据随机抽取结果 %s 先行" % players[0].name)

    return players

(3)play_round ()方法

play_round ()方法实现一轮完整对局的功能,该方法的逻辑可按照实例分析的一次流程完成,具体代码如下。

def play_round(self):

    """一轮完整对局"""

    # 1. 显示棋盘落子位置

    self.chess_board.show_board(True)

    # 2. 随机决定先手

    current_player, next_player = self.random_player()

    # 3. 两个玩家轮流落子

    while True:

        # 下子方落子

        current_player.move(self.chess_board)

        # 显示落子结果

        self.chess_board.show_board()

        # 是否胜利?

        if self.chess_board.is_win(current_player.chess):

            print("%s 战胜 %s" % (current_player.name, next_player.name))

            current_player.score += 1

            break

        # 是否平局

        if self.chess_board.is_draw():

            print("%s %s 战成平局" % (current_player.name,

next_player.name))

            break

        # 交换落子方

        current_player, next_player = next_player, current_player

    # 4. 显示比分

    print("[%s] 对战 [%s] 比分是 %d : %d" % (self.human.name,

                                        self.computer.name,

                                        self.human.score,

                                        self.computer.score))

从上述代码可以看出,大部分的功能都是通过游戏中各个对象访问属性或调用方法实现的,这正好体现了类的封装性的特点,即每个类分工完成各自的任务。

(4)start ()方法

start ()方法实现循环对局的功能,该方法中会在每轮对局结束之后询问玩家是否再来一局,若玩家选择是,则重置棋盘数据后开始新一轮对局;若玩家选择否,则会退出游戏,具体代码如下。

def start(self):

    """循环开始对局"""

    while True:

        # 一轮完整对局

        self.play_round()

        # 询问是否继续

        is_continue = input("是否再来一盘(Y/N)?").upper()

        # 判断玩家输入

        if is_continue != "Y":

            break

        # 重置棋盘数据

        self.chess_board.reset_board()

最后在game.py文件中通过Game类对象调用start()方法启动井字棋游戏,具体代码如下。

if __name__ == '__main__':

    Game().start()

代码测试

运行程序,对战一局游戏的结果如下所示:

       |       |

   0   |   1   |   2

       |       |

-----------------------

       |       |

   3   |   4   |   5

       |       |

-----------------------

       |       |

   6   |   7   |   8

       |       |

根据随机抽取结果 电脑 先行

电脑 正在思考落子位置...

       |       |

       |       |   

       |       |

-----------------------

       |       |

       |   X   |   

       |       |

-----------------------

       |       |

       |       |   

       |       |

玩家输入落子位置 [0, 1, 2, 3, 5, 6, 7, 8]0

       |       |

   O   |       |   

       |       |

-----------------------

       |       |

       |   X   |   

       |       |

-----------------------

       |       |

       |       |   

       |       |

电脑 正在思考落子位置...

       |       |

   O   |       |   

       |       |

-----------------------

       |       |

       |   X   |   

       |       |

-----------------------

       |       |

   X   |       |   

       |       |

玩家输入落子位置 [1, 2, 3, 5, 7, 8]2

       |       |

   O   |       |   O

       |       |

-----------------------

       |       |

       |   X   |   

       |       |

-----------------------

       |       |

   X   |       |   

       |       |

电脑 正在思考落子位置...

敌人走在 1 位置必输,火速堵上!

       |       |

   O   |   X   |   O

       |       |

-----------------------

       |       |

       |   X   |   

       |       |

-----------------------

       |       |

   X   |       |   

       |       |

玩家输入落子位置 [3, 5, 7, 8]7

       |       |

   O   |   X   |   O

       |       |

-----------------------

       |       |

       |   X   |   

       |       |

-----------------------

       |       |

   X   |   O   |   

       |       |

电脑 正在思考落子位置...

       |       |

   O   |   X   |   O

       |       |

-----------------------

       |       |

       |   X   |   

       |       |

-----------------------

       |       |

   X   |   O   |   X

       |       |

玩家输入落子位置 [3, 5]5

       |       |

   O   |   X   |   O

       |       |

-----------------------

       |       |

       |   X   |   O

       |       |

-----------------------

       |       |

   X   |   O   |   X

       |       |

电脑 正在思考落子位置...

       |       |

   O   |   X   |   O

       |       |

-----------------------

       |       |

   X   |   X   |   O

       |       |

-----------------------

       |       |

   X   |   O   |   X

       |       |

电脑 玩家 战成平局

[玩家] 对战 [电脑] 比分是 0 : 0

是否再来一盘(Y/N)?n

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/403146.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Leetcode】【简单】35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: 输入: nums [1,3,5,6], target 5 输出: 2 示例 2: 输入:…

MSP430F5529单片机入门学习笔记1

本笔记整理自B站教程MSP430F5529单片机学习视频汇总基于库函数的开发—GPIO库函数右边部分写错了&#xff0c;看的时候注意基于库函数的开发—GPIO实践操作LED交替闪烁#include <msp430.h> /** EXP-GPIO-01.C** Created on: 2023年3月10日* Author: ASUS*/#include…

webRtc概念

webRtc概念 以下的文档整理来自此链接 文档整理了一系列实现web通用接口的ECMAScript APIs &#xff0c;这些接口是为了支持浏览器或者一些其他实现了实时交换协议的设备进行媒体信息和程序数据交换。 1、实现点对点通信的规范&#xff1a; NAT穿透实现与远端节点链接比如&a…

WebRTC开源库内部调用abort函数引发程序发生闪退问题的排查

目录 1、初始问题描述 2、使用Process Explorer工具查看到处理音视频业务的rtcmpdll.dll模块没有加载起来 3、使用Dependency Walker工具查看到rtcmpdll.dll依赖的库有问题 4、更新库之后Debug程序启动时就发生异常&#xff0c;程序闪退 5、VS调试时看不到有效的函数调用堆…

hashmap存储方式 hash碰撞及其解决方式

1.Map的存储特点 在Map这个结构中&#xff0c;数据是以键值对&#xff08;key-value&#xff09;的形式进行存储的&#xff0c;每一个存储进map的数据都是一一对应的。 创建一个Map结构可以使用new HashMap()以及new TreeMap()两种方式&#xff0c;两者之间的区别是&#xff1a…

TVS和稳压管的相同点和不同点

大家好,我是记得诚。 文章目录 介绍相同点不同点介绍 TVS和稳压管都是电路中很常用的电子元器件,都是二极管的一个种类。 TVS二极管全称是Transient voltage suppression diode,也叫瞬态电压抑制二极管。 稳压二极管英文名字Zener diode,又叫齐纳二极管。 关于稳压二极…

Jenkins从下载到部署项目的流程

Jenkins安装配置1.1 Jenkins介绍Jenkins 是一款流行的开源持续集成&#xff08;Continuous Integration&#xff09;工具&#xff0c;广泛用于项目开发&#xff0c;具有自动化构建、测试和部署等功能。官网&#xff1a; http://jenkins-ci.org/。Jenkins的特征&#xff1a;开源…

谷粒学院开发(三):统一日志、异常及前端准备工作

特定异常处理 ControllerAdvice public class GlobalExceptionHandler {ExceptionHandler(Exception.class) // 指定出现什么异常会被处理ResponseBody // 为了能够返回数据public R error(Exception e) {e.printStackTrace();return R.error().message("执行了全局异常…

Linux--磁盘存储管理 分区工具 fdisk 分区实操 详解~

上一篇文章介绍了 fdisk 的各个菜单功能&#xff0c;这篇&#xff0c;我们直接实操 管理磁盘 fdisk :分区 &#xff1a; 我们上一篇文章里讲过&#xff0c;上篇文章的 磁盘 /dev/nvme0n1 空间已经满了因此 &#xff0c; 又重新添加了一块儿硬盘~&#xff01;&#xff01;>&g…

传统图像处理之颜色特征

博主简介 博主是一名大二学生&#xff0c;主攻人工智能研究。感谢让我们在CSDN相遇&#xff0c;博主致力于在这里分享关于人工智能&#xff0c;c&#xff0c;Python&#xff0c;爬虫等方面知识的分享。 如果有需要的小伙伴可以关注博主&#xff0c;博主会继续更新的&#xff0c…

UML时序图速查——架构设计必备技能

目录 一、时序图概述 二、时序图元素 1. Actor&#xff08;角色&#xff09;& Object&#xff08;对象&#xff09; 2. Lifeline&#xff08;生命线&#xff09; 3. Message&#xff08;消息&#xff09; 4. Combined Fragment&#xff08;组合片段&#xff09; 5. …

【Linux】多线程---线程控制

进程在前面已经讲过了&#xff0c;所以这次我们来讨论一下多线程。前言&#xff1a;线程的背景进程是Linux中资源及事物管理的基本单位&#xff0c;是系统进行资源分配和调度的一个独立单位。但是实现进程间通信需要借助操作系统中专门的通信机制&#xff0c;但是只这些机制将占…

java并发入门(一)共享模型—Synchronized、Wait/Notify、pack/unpack

一、共享模型—管程 1、共享存在的问题 1.1 共享变量案例 package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;Slf4j(topic "c.MTest1") public class MTest1 {static int counter 0;public static void main(String[] args) throws InterruptedEx…

如何科学管理技术团队的研发交付速率?

每当提及「研发效能」&#xff0c;我们都在谈论什么&#xff1f; 研发效能管理要在保证质量的前提下&#xff0c;思考如何更快地向客户交付价值。在管理实践中&#xff0c;效能度量涉及三大维度&#xff1a;交付速率、交付质量、交付价值。 技术团队对内如何优化开发流程&…

STM32实战项目-基本定时器

前言&#xff1a; 通过基本定时器TIM6&#xff0c;让三个LED灯每间隔1s闪烁一次。 目录 1.基本定时器参数配置 1.1框图分析 1.2参数配置 2.软件程序 2.1整体框架 2.2定时器结构体 2.3定时器回调函数 1.基本定时器参数配置 1.1框图分析 TIM6作为基本定时器 它是挂载…

【Linux】-- 线程池

目录 铺垫 内存 线程的角度 线程池 基本代码结构 对于线程池的生产消费的完善 初步实现线程池生产消费 结合日志完善线程池 铺垫 内存 &#xff08;以STL处理方式&#xff0c;引入提供效率的一种思想&#xff09; 通过进行C语言与C语言的学习中&#xff0c;平时我们使…

C语言 深度剖析数据在内存中的存储(2)

本次博客是继上次博客&#xff0c;继续向下剖析数据在内存当中的存储。练习浮点型在内存中的存储练习代码1&#xff1a;int main() {char a -1;signed char b-1;unsigned char c-1;printf("a%d,b%d,c%d",a,b,c);return 0; }1.在本题中首先我们要知道的是%d打印的是有…

【数据结构之树】——什么是树,树的特点,树的相关概念和表示方法以及在实际的应用。

文章目录一、1.树是什么&#xff1f;2.树的特点二、树的相关概念三、树的表示方法1.常规方法表示树2.使用左孩子右兄弟表示法3. 使用顺序表来存储父亲节点的下标三、树在实际的应用总结一、1.树是什么&#xff1f; 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n&…

MatCap模拟光照效果实现

大家好&#xff0c;我是阿赵 之前介绍过各种光照模型的实现方法。那些光照模型的实现虽然有算法上的不同&#xff0c;但基本上都是灯光方向和法线方向的计算得出的明暗结果。 下面介绍一种叫做MatCap的模拟光照效果&#xff0c;这种方式计算非常简单&#xff0c;脱离灯光的计算…

javaWeb核心05-FilterListenerAjax(Axios)json

文章目录Filter&Listener&Ajax1&#xff0c;Filter1.1 Filter概述1.2 Filter快速入门1.2.1 开发步骤1.2.2 代码演示1.3 Filter执行流程1.4 Filter拦截路径配置1.5 过滤器链1.5.1 概述1.5.2 代码演示1.5.3 问题1.6 案例1.6.1 需求1.6.2 分析1.6.3 代码实现1.6.3.1 创建F…