java 围棋_Java.awt实现一个简单的围棋

本文介绍了如何使用Java实现一个简单的围棋游戏,包括绘制棋盘、黑白棋交替落子、标记手数、提子及高亮显示等功能。程序结构清晰,包含棋盘类、棋子类、玩家类等,实现了基础的围棋规则判断。虽然未实现复杂规则如打劫,但提供了提子的深度优先搜索算法实现。
摘要由CSDN通过智能技术生成

目录

0.前言

我小时候学过一段时间的围棋,可惜脑子不好使,是个臭棋篓子,到现在也有十多年的时间没有下过棋了,但是近几年围棋AI的出现,又让我重新关注了围棋

围棋真的很有意思,千变万化,有人简明的围空,有人进行复杂的战斗,在高手的对局里,一手棋都会对全局产生十分大的影响

最近上班摸鱼,闲来无事,打开了IDEA,想着写一个围棋程序

选择使用Java的原因完全是因为这台电脑的Visual Studio打开太卡了,其实感觉可能用C++或者C#写会更好一点

网上关于围棋基础规则的文章不是很多,基本都是讲述围棋AI的实现,因此在完成了这个简单的围棋小游戏之后,特此记录一下

1.概述

这里先说明本文实现的内容,本文实现了绘制棋盘、黑白两棋交替落子、标记手数、提子、高亮最后一手棋的功能

交替落子只实现了在棋盘内、不能落在有子的交叉点的判断

没有实现对打劫、自杀的判断(其实自杀的判断好像加一两行if判断就好了,但是临近周五下班,赶紧写了一篇记录出来,有兴趣的读者可以自己实现)

提子的算法是参考

的算法实现

他讲提子描述为一个迷宫问题,将相领的相同颜色的棋子视为通路,将不同颜色的棋子视为“墙”,用深度优先搜索算法,只要能搜索出出口,便是有气

这应该是本文中最难实现的一个部分了,其他部分都很简单

不过我的代码中,有些代码在打完之后让我觉得自己很愚蠢,在一开始没有计划好,后来为了不把“屎山”推倒重来,还引入了一些三维数组来实现功能

如果想看其他内容的话,可以去搜索其他的文章了,没必要看这篇入门级的文章

2.结构

e61009686e25011c5b7423ec7b77c94b.png

我的项目结构如下,其中有些英文是网上查来的,可能不够标准

draw中实现绘图类,跟绘图有关的实现在该包中

BackGround类是一个Frame

ChessPad类是一个Frame里的Panel,绘制了棋盘

HighLight类实现了高亮最后一手的功能

Place类实现了绘制落子、提子

TeNum类实现了绘制手数

main中Main只简单创建BackGround

player中的Player类存储对局棋手的信息,只有两个简单的属性,棋子的颜色和是否轮到他下

stone的Stone类只有一个简单的属性,就是棋子的颜色----黑、白、无

rules中实现了一些简单落子规则的判断

AlreadyHadStone判断该落子点是否已有子

InBoard判断落子点是否在棋盘内

Ko本来想实现打劫的判断,但是还没有实现

Liberty判断是否有气

Take通过Liberty判断是否有气,进而判断是否可以提子

详细的描述在代码注释中,这里不过多赘述

3.代码实现

3.1 main

3.1.1 main.Main

package com.krumitz.main;

import com.krumitz.draw.BackGround;

import com.krumitz.player.Player;

public class Main {

public static void main(String args[]){

//调用创建棋盘

new BackGround();

}

}

3.2 stone

3.2.1 stone.Stone 棋子类

package com.krumitz.stone;

/**

* 棋子类

*/

public class Stone {

public enum StoneColor

{

BLACK,WHITE,NONE

}

private StoneColor stoneColor;

public Stone()

{

this.stoneColor = StoneColor.NONE;

}

public void setStoneColor(StoneColor stoneColor)

{

this.stoneColor = stoneColor;

}

public StoneColor getStoneColor()

{

return this.stoneColor;

}

}

3.3 player

3.3.1 player.Player 棋手类

package com.krumitz.player;

import com.krumitz.stone.Stone;

public class Player {

private Stone stone;

private boolean isMoving;

public Player()

{

stone = new Stone();

stone.setStoneColor(Stone.StoneColor.NONE);

this.isMoving = false;

}

public Stone getStone() { return this.stone; }

public void setStone(Stone.StoneColor stoneColor){ this.stone.setStoneColor(stoneColor); }

public boolean getIsMoving()

{

return this.isMoving;

}

public void setIsMoving(boolean isMoving)

{

this.isMoving = isMoving;

}

}

3.4 draw

3.4.1 draw.BackGround 背景类

package com.krumitz.draw;

import java.awt.*;

import java.awt.event.WindowAdapter;

import java.awt.event.WindowEvent;

public class BackGround extends Frame {

ChessPad chessPad;

public BackGround()

{

chessPad = new ChessPad();

this.add(chessPad);

this.setSize(600,600);

this.setVisible(true);

this.addWindowListener(new WindowAdapter() {

@Override

public void windowClosing(WindowEvent e) {

System.exit(0);

}

});

}

}

3.4.2 draw.ChessPad 棋盘类

package com.krumitz.draw;

import com.krumitz.player.Player;

import com.krumitz.rules.AlreadyHadStone;

import com.krumitz.rules.InBoard;

import com.krumitz.rules.Take;

import com.krumitz.stone.Stone;

import java.awt.*;

import java.awt.event.*;

public class ChessPad extends Panel implements MouseListener, ActionListener {

/**

* 声明Player类存储棋手下棋顺序

* 声明落子绘图类用于绘制棋子

* 声明teNum类用于绘制手数

* 声明highLight高亮最后一手

* 声明19*19 move数组,存储已落子的信息

* 声明teNum记录手数

* 声明move_teNum 记录每一个坐标的棋子是第几手棋

* 声明上一手的坐标last_coordinate_x,last_coordinate_y

*/

Player BLACK_PLAYER ;

Player WHITE_PLAYER ;

Place BLACK_STONE;

Place WHITE_STONE;

TeNum class_teNum;

HighLight highLight;

Stone.StoneColor move[][] ;

int teNum;

int move_teNum[][][];

int last_coordinate_x,last_coordinate_y;

/**

*构造棋盘大小、背景、鼠标监听器

*/

ChessPad()

{

// 初始化执黑棋手

BLACK_PLAYER = new Player();

BLACK_PLAYER.setIsMoving(true);

BLACK_PLAYER.setStone(Stone.StoneColor.BLACK);

// 初始化执白棋手

WHITE_PLAYER = new Player();

WHITE_PLAYER.setIsMoving(false);

WHITE_PLAYER.setStone(Stone.StoneColor.WHITE);

// 初始化手数类

class_teNum = new TeNum();

// 初始化高亮最后一手类

highLight = new HighLight();

// 初始化黑棋、白棋

BLACK_STONE = new Place(this);

WHITE_STONE = new Place(this);

// 初始化棋谱数组、手数数组

move = new Stone.StoneColor[19][19];

move_teNum = new int[19][19][1];

for (int i = 0; i < 19; i++)

{

for (int j = 0; j < 19; j++)

{

move[i][j] = Stone.StoneColor.NONE;

move_teNum[i][j][0] = -1;

}

}

//初始化手数、最后一手的坐标

teNum = 1;

last_coordinate_x = 0;

last_coordinate_y = 0;

this.add(BLACK_STONE);

this.add(WHITE_STONE);

this.add(class_teNum);

this.setSize(600,600);

this.setLayout(null);

this.setBackground(Color.ORANGE);

this.addMouseListener(this);

}

/**

* 画棋盘的线和点

* @param g

*/

public void paint(Graphics g)

{

for (int i = 45; i <= 495; i += 25)

{

g.drawLine(i, 45, i, 495);

}

for (int i = 45; i <= 495; i += 25)

{

g.drawLine(45, i, 495, i);

}

//D16

g.fillOval(116,116,8,8);

//Q4

g.fillOval(416,416,8,8);

//D4

g.fillOval(116,416,8,8);

//Q16

g.fillOval(416,116,8,8);

//D10

g.fillOval(116,266,8,8);

//K16

g.fillOval(266,116,8,8);

//Q10

g.fillOval(416,266,8,8);

//K4

g.fillOval(266,416,8,8);

//天元

g.fillOval(266,266,8,8);

}

/**

* 按下鼠标,调用落子类绘图方法

* @param mouseEvent

*/

@Override

public void mouseClicked(MouseEvent mouseEvent)

{

if((mouseEvent.getModifiers() == InputEvent.BUTTON1_MASK))

{

// 这里减数是棋子的宽度、高度的一半 -- 10

int x = (int)mouseEvent.getX()-10;

int y = (int)mouseEvent.getY()-10;

// 这里先求余、相减、再除

// 求余数和除数是棋盘每路之间的宽度 -- 25

// 得到的是棋盘坐标

// -1 为了跟数组对应

int coordinate_x = (x-(x%25))/25-1;

int coordinate_y = (y-(y%25))/25-1;

// 这里用棋盘坐标乘以棋盘每路之间的宽度 -- 25

// 再加上棋子的宽度、高度的一半 -- 10

// 得到的是落子类绘图方法需要的坐标

int place_x = (coordinate_x+1)*25 + 10;

int place_y = (coordinate_y+1)*25 + 10;

// 判断是否在棋盘内

if(InBoard.ifInBoard(coordinate_x,coordinate_y))

{

if(!AlreadyHadStone.ifAlreadyHadStone(move,coordinate_x,coordinate_y))

{

// 黑棋

if(this.BLACK_PLAYER.getIsMoving())

{

// 落子、绘图

Place.placeStone(this.BLACK_PLAYER,place_x, place_y, this.getGraphics());

// 绘制手数

class_teNum.drawTeNum(place_x,place_y,teNum,this.BLACK_PLAYER.getStone().getStoneColor(),this.getGraphics());

// 设置有子

move[coordinate_x][coordinate_y] = this.BLACK_PLAYER.getStone().getStoneColor();

}

// 白棋

if(this.WHITE_PLAYER.getIsMoving())

{

// 落子、绘图

Place.placeStone(this.WHITE_PLAYER,place_x, place_y, this.getGraphics());

// 绘制手数

class_teNum.drawTeNum(place_x,place_y,teNum,this.WHITE_PLAYER.getStone().getStoneColor(),this.getGraphics());

// 设置有子

move[coordinate_x][coordinate_y] = this.WHITE_PLAYER.getStone().getStoneColor();

}

// 手数加1

move_teNum[coordinate_x][coordinate_y][0] = teNum;

teNum ++;

// 如果可以提子

if(Take.takeStones(move,coordinate_x,coordinate_y))

{

takeStones(this.getGraphics());

System.out.println("提子");

}

else

{

System.out.println("落子");

}

// 高亮最后一手,并将倒数第二手的高亮去除

highLight.highLightLastStone(coordinate_x,coordinate_y,last_coordinate_x,last_coordinate_y,move,teNum-1,this.getGraphics());

last_coordinate_x = coordinate_x;

last_coordinate_y = coordinate_y;

// 两级反转.表明包

BLACK_PLAYER.setIsMoving(!(BLACK_PLAYER.getIsMoving()));

WHITE_PLAYER.setIsMoving(!(WHITE_PLAYER.getIsMoving()));

}

else

{

System.out.println("已有子");

}

}

else

{

System.out.println("棋盘外");

}

}

if((mouseEvent.getModifiers() == InputEvent.BUTTON3_MASK))

{

System.out.println("右键");

}

}

@Override

public void actionPerformed(ActionEvent actionEvent) {

}

@Override

public void mousePressed(MouseEvent mouseEvent) {

}

@Override

public void mouseReleased(MouseEvent mouseEvent) {

}

@Override

public void mouseEntered(MouseEvent mouseEvent) {

}

@Override

public void mouseExited(MouseEvent mouseEvent) {

}

// 提子

public void takeStones(Graphics graphics)

{

int coordinate_x,coordinate_y,remove_x,remove_y;

// 获得提子数量

int length[][] = Take.getLength();

// 获得提子坐标

int takeStones[][][] = Take.getTakeStones();

for(int i=0;i<4;i++)

{

// 如果记录的数量不为0,有子可提

if(length[i][0] != 0)

{

for(int j=0;j

{

// 获得要提的子的坐标

coordinate_x = takeStones[i][j][0];

coordinate_y = takeStones[i][j][1];

// 将坐标转换为绘图坐标

remove_x = (coordinate_x+1)*25 + 10;

remove_y = (coordinate_y+1)*25 + 10;

// 去除棋谱上该子

move[coordinate_x][coordinate_y] = Stone.StoneColor.NONE;

// 提子

Place.takeStone(remove_x,remove_y,graphics);

}

}

}

// 重绘

removeAll();

paint(graphics);

// 重绘仍在棋盘上的棋子

for (int i = 0; i < 19; i++)

{

for (int j = 0; j < 19; j++)

{

if (move[i][j] == Stone.StoneColor.BLACK)

{

Place.placeStone(this.BLACK_PLAYER,((i+1)*25 + 10),((j+1)*25 + 10),this.getGraphics());

class_teNum.drawTeNum(((i+1)*25 + 10),((j+1)*25 + 10),move_teNum[i][j][0],move[i][j],this.getGraphics());

}

if (move[i][j] == Stone.StoneColor.WHITE)

{

Place.placeStone(this.WHITE_PLAYER,((i+1)*25 + 10),((j+1)*25 + 10),this.getGraphics());

class_teNum.drawTeNum(((i+1)*25 + 10),((j+1)*25 + 10),move_teNum[i][j][0],move[i][j],this.getGraphics());

}

if(move_teNum[i][j][0] == teNum)

{

highLight.highLightLastStone(i,j,0,0,move,0,this.getGraphics());

}

}

}

}

}

3.4.3 draw.Place 落子类

package com.krumitz.draw;

import com.krumitz.player.Player;

import com.krumitz.stone.Stone;

import java.awt.*;

public class Place extends Panel {

ChessPad chessPad;

public Place(ChessPad chessPad)

{

setSize(20,20);

this.chessPad = chessPad;

}

// 落子

public static void placeStone(Player player,int x,int y,Graphics graphics)

{

if(player.getStone().getStoneColor() == Stone.StoneColor.BLACK)

{

graphics.setColor(Color.BLACK);

graphics.fillOval(x,y,20,20);

}

if(player.getStone().getStoneColor() == Stone.StoneColor.WHITE)

{

graphics.setColor(Color.WHITE);

graphics.fillOval(x,y,20,20);

}

}

// 提子

public static void takeStone(int x,int y,Graphics graphics)

{

graphics.clearRect(x,y,20,20);

}

}

3.4.4 draw.TeNum 手数类

绘制手数,让手数的文字居中是根据

实现的

package com.krumitz.draw;

import com.krumitz.stone.Stone;

import java.awt.*;

public class TeNum extends Panel {

public static void drawTeNum(int place_x, int place_y, int teNum, Stone.StoneColor color, Graphics graphics)

{

if(color == Stone.StoneColor.BLACK)

{

graphics.setColor(Color.WHITE);

}

if(color == Stone.StoneColor.WHITE)

{

graphics.setColor(Color.BLACK);

}

Font font = graphics.getFont();

FontMetrics metrics = graphics.getFontMetrics(font);

// Determine the X coordinate for the text

int teNum_x = place_x + (20 - metrics.stringWidth(String.valueOf(teNum))) / 2;

// Determine the Y coordinate for the text (note we add the ascent, as in java 2d 0 is top of the screen)

int teNum_y = place_y + ((20 - metrics.getHeight()) / 2) + metrics.getAscent();

// Set the font

graphics.setFont(font);

// Draw the String

graphics.drawString(String.valueOf(teNum),teNum_x,teNum_y);

}

}

3.4.5 draw.HighLight 高亮类

这个方法实现的时候十分的偷懒,去除倒数第二颗棋子的高亮的时候,直接用棋盘底色覆盖了

package com.krumitz.draw;

import com.krumitz.stone.Stone;

import java.awt.*;

/**

* 高亮最后一手

*/

public class HighLight{

private static BasicStroke strokeLine = new BasicStroke(1.5f);

// 给最后一手棋子加一圈红色边框

public static void highLightLastStone(int coordinate_x, int coordinate_y,

int last_coordinate_x, int last_coordinate_y,

Stone.StoneColor move[][],int teNum, Graphics graphics)

{

graphics.setColor(Color.RED);

int draw_x = (coordinate_x+1)*25 + 10;

int draw_y = (coordinate_y+1)*25 + 10;

//

Graphics2D g = (Graphics2D) graphics;

g.setStroke(strokeLine);

g.drawOval(draw_x,draw_y,20,20);

// 如果手数大于1,把倒数第二手的红色边框去除

if(teNum > 1)

{

removeLastButOneLight(last_coordinate_x,last_coordinate_y,move,g);

}

}

// 直接偷懒,用棋盘底色在原来的那一圈上面再画一圈

public static void removeLastButOneLight(int last_coordinate_x, int last_coordinate_y, Stone.StoneColor move[][], Graphics g)

{

int draw_x = (last_coordinate_x + 1) * 25 + 10;

int draw_y = (last_coordinate_y + 1) * 25 + 10;

if (move[last_coordinate_x][last_coordinate_y] == Stone.StoneColor.BLACK) {

g.setColor(Color.BLACK);

}

if (move[last_coordinate_x][last_coordinate_y] == Stone.StoneColor.WHITE) {

g.setColor(Color.WHITE);

}

g.drawOval(draw_x, draw_y, 20, 20);

g.setColor(Color.ORANGE);

g.drawOval(draw_x,draw_y,20,20);

}

}

3.5 rules

3.5.1 rules.AlreadyHadStone 判断已有子

package com.krumitz.rules;

import com.krumitz.stone.Stone;

/**

* 判断落子点是否已有子

*/

public class AlreadyHadStone {

public static boolean ifAlreadyHadStone(Stone.StoneColor move[][],int coordinate_x,int coordinate_y)

{

if(move[coordinate_x][coordinate_y] == Stone.StoneColor.NONE)

{

return false;

}

return true;

}

}

3.5.2 rules.Inboard 判断棋盘内

package com.krumitz.rules;

/**

* 判断是否在棋盘内

*/

public class InBoard {

public static boolean ifInBoard(int coordinate_x, int coordinate_y)

{

if((coordinate_x>=0 && coordinate_x<=18) && (coordinate_y>=0 && coordinate_y<=18))

{

return true;

}

else

{

return false;

}

}

}

3.5.3 rules.Liberty 判断有气

package com.krumitz.rules;

import com.krumitz.stone.Stone;

/**

* 气

*/

public class Liberty {

// 声明记录数组

private static int[][] visited = new int[19][19];

// 声明上下左右四个方向

private static int[][] directions = {{0,1},{1,0},{-1,0},{0,-1}};

// 声明记录提子的坐标的二维数组

private static int[][] liberty_takeStones = new int[19][2];

// 声明记录二维数组的长度

private static int liberty_length;

// 记录数组初始化函数

private static void setUpVisited()

{

for (int i = 0; i < 19; i++)

{

for (int j = 0; j < 19; j++)

{

visited[i][j] = 0;

}

}

}

private static void setUpTakeStones()

{

for (int i = 0; i < 19; i++)

{

for (int j = 0; j < 2; j++)

{

liberty_takeStones[i][j] = 0;

}

}

}

private static boolean DFS(Stone.StoneColor move[][], int coordinate_x, int coordinate_y)

{

int direction_x,direction_y;

// 设置已访问标志1

visited[coordinate_x][coordinate_y] = 1;

// 将当前子的坐标存入提子数组,数组长度+1

liberty_takeStones[liberty_length][0] = coordinate_x;

liberty_takeStones[liberty_length][1] = coordinate_y;

liberty_length++;

// 遍历上下左右四个方向

for(int i = 0;i < 4;i++)

{

direction_x = coordinate_x + directions[i][0];

direction_y = coordinate_y + directions[i][1];

// 判断是否在棋盘内

if(!(InBoard.ifInBoard(direction_x,direction_y)))

{

// 不在棋盘内就遍历下一个点

continue;

}

// 如果在棋盘内,且没访问过

else if(visited[direction_x][direction_y] == 0)

{

// 如果该位置无子,则有气,返回true

if(move[direction_x][direction_y] == Stone.StoneColor.NONE)

{

// 这些输出是在debug的时候用的,可以删掉

System.out.println("有气: "+direction_x+" "+direction_y);

return true;

}

// 如果该位置有子,且子的颜色不同,就遍历下一个点

if(move[direction_x][direction_y] != move[coordinate_x][coordinate_y])

{

System.out.println("不同色: "+direction_x+" "+direction_y);

continue;

}

// 如果该位置有子,且颜色相同,递归遍历该子

if(move[direction_x][direction_y] == move[coordinate_x][coordinate_y])

{

System.out.println("同色: "+direction_x+" "+direction_y);

//如果下一个子返回true

if(DFS(move,direction_x,direction_y))

{

return true;

}

}

}

}

// 如果遍历完都没气,返回false

return false;

}

// 判断是否有气函数

public static boolean hasLiberty(Stone.StoneColor move[][], int coordinate_x, int coordinate_y)

{

// 初始化遍历记录访问数组

setUpVisited();

setUpTakeStones();

// 重置记录长度

liberty_length = 0;

System.out.println("hasLiberty开始: "+coordinate_x+" "+coordinate_y);

if(DFS(move,coordinate_x,coordinate_y))

{

System.out.println("hasLiberty结束,返回true");

return true;

}

else

{

System.out.println("hasLiberty结束,返回false");

return false;

}

}

public static int[][] getTakeStones()

{

return liberty_takeStones;

}

public static int getLength()

{

return liberty_length;

}

}

3.5.4 rules.Take 判断提子

package com.krumitz.rules;

import com.krumitz.stone.Stone;

/**

* 提子

*/

public class Take {

// 声明上下左右四个方向

private static int[][] directions = {{0,1},{1,0},{-1,0},{0,-1}};

// 记录上下左右四颗子的hasLiberty返回的长度

private static int[][] take_length=new int[4][1];

// 记录上下左右四颗子的hasLiberty返回的提子数组,这里感觉提的子不会很多,因此长度只有19

// 4表示4个方向

// 19表示第N个要提的子

// 最后两位表示第N个要提的子的x、y坐标

private static int[][][] take_takeStones=new int[4][19][2];

// 初始化length

private static void setUpLength()

{

for(int i=0;i<4;i++)

{

take_length[i][0] = 0;

}

}

// 初始化takeStones

private static void setUpTakeStones()

{

for (int i = 0; i < 4; i++)

{

for (int j = 0; j < 19; j++)

{

for (int k = 0; k < 2; k++)

{

take_takeStones[i][j][k] = 0;

}

}

}

}

// 提子函数

public static boolean takeStones(Stone.StoneColor move[][],int coordinate_x,int coordinate_y)

{

// flag为1则有子可提

int flag = 0;

// 初始化记录数组

setUpLength();

setUpTakeStones();

int direction_x,direction_y;

// 获得当前局面最后一手棋的颜色

Stone.StoneColor color = move[coordinate_x][coordinate_y];

// 判断该手棋上下左右四个方向的相领棋子

for(int i=0;i<4;i++)

{

direction_x = coordinate_x + directions[i][0];

direction_y = coordinate_y + directions[i][1];

// 如果不在棋盘内,继续下一个循环

if(!(InBoard.ifInBoard(direction_x,direction_y)))

{

continue;

}

// 如果该方向上的有棋

// 且棋子颜色与当前局面最后一手棋颜色不同

else if(move[direction_x][direction_y] != color && move[direction_x][direction_y] != Stone.StoneColor.NONE)

{

// 如果该棋子所在的块有气,继续下一个循环

if(Liberty.hasLiberty(move,direction_x,direction_y))

{

continue;

}

// 如果没气,flag为1

else

{

flag = 1;

// 记录第i个方向上的提子的数量

take_length[i][0] = Liberty.getLength();

// 记录第i个方向上的提子的坐标

int temp[][] = Liberty.getTakeStones();

for(int j=0;j<19;j++)

{

for(int k=0;k<2;k++)

{

take_takeStones[i][j][k] = temp[j][k];

}

}

}

}

}

// flag不为0,可提子,返回true

if(flag!=0)

{

return true;

}

else

{

return false;

}

}

public static int[][][] getTakeStones()

{

return take_takeStones;

}

public static int[][] getLength()

{

return take_length;

}

}

4. 运行结果 & 小结

7a7347899ffc6ee4efe75c851213db8e.png

本次实现的功能还是很简单的,赶在周五下班之前完成了这个程序并且记录了下来

后续仍有继续改进已有功能和增加新的功能的意愿,如果有空实现了的话,将会继续更新

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值