麻将Ai设计思路(go语言实现)

设计思路

由于麻将胡牌规则固定(只是牌型不同):3m+2(3指的是顺子、刻子,2指的是将牌(一对))。比如有个牌型123万789条白白白中中,这个手牌就是胡牌的牌型,其中m=3。
 想要形成胡牌的牌型,我们需要保留当前牌型中的顺子、刻子和对,以及可以形成成顺子的差一张牌的牌(比如13,差一张2。比如34,差一个2或者5。)。这样子拆分后的牌剩下的只有单牌,所以我们出牌时可以先将单牌打出,然后再根据摸得新的牌,重新分析,最后形成听牌牌型。当有事件(吃碰杠)时,我们可以对每一个事件分析一次牌型,然后根据形成的牌型所产生的单牌、以及可以形成单牌的牌的权重比,来进行计算当前事件触发后对于牌型改变的优先级。(比如:对于单牌权重要占用比例比较大,可以是0.6以上;然后13牌比例次之,可以是0.25,最后的23牌可以是0.15)。
 要分析牌型,我们就需要为牌型新建一个结构体:

//分析牌型工具,首先找出所有的顺子,然后再讲所有刻子找到,然后找出可以形成顺子的牌(比如 23 ,13之类的),然后剩余的牌,就是可以打出的牌
//一种牌型(里面的都是索引)
type TileType struct {
	Array_S          []byte //顺子切片,只记录第一个
	Array_k          []byte //刻子切片,只记录第一个
	Array_j          []byte //可以做将牌的牌,只记录第一个
	Array_d          []byte //单牌
	Array_1_3        []byte //间隔相差1张牌的牌,记录第一个
	Array_34         []byte //左右相差一张牌的牌,记录第一个
	Array_tile_index []byte // 所有牌索引
	mask             byte   //什么动作之后的分析牌型。比如,普通出牌时的摸牌操作,或吃碰杠等
	tile             byte   //动作牌值
}

在上面的结构体中,我们是以顺子座位第一顺位,然后才是刻子、对子、34、13。
 接下来就是对于牌型的提取:

//分析牌型
func AnaTileType(tileIndexs []byte, mask byte, tile byte) *TileType {
	tt := &TileType{}
	tt.mask = mask
	tt.tile = tile
	tt.Array_tile_index = append(tt.Array_tile_index, tileIndexs...)
	ti := tt.Array_tile_index
	//提取顺子123 23  13
	for i := byte(0); i < 7; i++ {
		v := ti[i]
		if v != 0 && ti[i+1] != 0 && ti[i+2] != 0 { //顺子
			tt.Array_S = append(tt.Array_S, i)
			ti[i]--
			ti[i+1]--
			ti[i+2]--
		}
	}
	//提取将 刻子
	for i, v := range ti {
		if v == 2 {
			tt.Array_j = append(tt.Array_j, byte(i))
			ti[i] = 0
			continue
		}
		if v == 3 {
			tt.Array_k = append(tt.Array_k, byte(i))
			ti[i] = 0
		}
	}
	//提取 类似 34 的顺子模型
	for i := byte(0); i < 7; i++ {
		if ti[i] != 0 && ti[i+1] != 0 && ti[i+2] == 0 {
			tt.Array_34 = append(tt.Array_34, i)
			ti[i]--
			ti[i+1]--
		}
	}
	//提取类似 13 的顺子模型
	for i := byte(0); i < 7; i++ {
		if ti[i] != 0 && ti[i+1] == 0 && ti[i+2] != 0 {
			tt.Array_1_3 = append(tt.Array_1_3, i)
			ti[i]--
			ti[i+2]--
		}
	}
	//提取单牌
	for i, v := range ti {
		if v == 1 {
			tt.Array_d = append(tt.Array_d, byte(i))
		}
	}
	share.LOG.Debug("---牌型:%s", tt.ToString())
	return tt
}

接下来就是对出牌的分析了:

/*
ai逻辑
1 先检查听牌
2 去掉单个字牌
3 去掉间隔2个空位的不连续的牌
4 去掉间隔1个空位的不连续的牌
5 去除连续牌数为4、7 中的一张牌,让牌型成为无将胡牌型。如2344万,去掉四万
6 去除连续牌数为3、6、9 中的一张牌,有将则打成一吃二,成为无将听牌型(如233万,去掉三万);无将则打成有将胡牌牌型(如233万,去掉2万)
7 去除连续牌数为2、5、8中的一张牌,让牌型成为有将听牌型。如23445万,去掉5万
8 从将牌中打出一张牌
 */

//接牌 出牌逻辑(没有触发事件:胡 杠)
func (this *ErMahjong_AI) PlayLogic(msg *PGAME.Data_Play, play_res *PGAME.Play) {
	t1 := time.Now().Unix()

	//取出剩余的牌
	if msg.Ext != nil && len(msg.Ext) != 0 {
		this.hand.leftTiles = msg.Ext
	}
	share.LOG.Debug("-------剩余的牌:%v", this.hand.leftTiles)

	//是否摸牌
	if msg.GetDraw() { //有摸牌,将牌值放入手牌
		share.LOG.Debug("---------摸牌:%d", msg.Tiles.Value[0])
		if !this.isTing { //如果听牌,直接将摸到的牌打出去
			n := 0
			//将摸到的牌放入手牌
			if num, found := this.hand.tiles_num[msg.Tiles.GetValue()[0]]; found {
				n = num + 1
			}
			this.hand.tiles_num[msg.Tiles.GetValue()[0]] = n
			this.hand.tiles[msg.Tiles.GetId()[0]] = msg.Tiles.Value[0]
			this.hand.tilesIndex[ValueToIndex(msg.Tiles.Value[0])]++
			share.LOG.Debug("------------摸牌之后:%v", this.hand.tilesIndex)
		}
	}

	var drawIds []byte
	if msg.GetDraw() {
		drawIds = msg.GetTiles().Id
	}
	//定义出牌id
	play_id, tingSend := this.PlayByTing(drawIds)

	share.LOG.Debug("玩家手牌tile:%v", this.hand.tiles)
	fmt.Println("--------出牌id:", play_id)
	play_res.Action = PGAME.Action_ACTION_NULL.Enum()
	play_res.Id = proto.Int32(int32(play_id))

	if tingSend {
		play_res.Action = PGAME.Action_ACTION_TING.Enum()
	}
	share.LOG.Debug("检查是否听牌花费时间:%d", time.Now().Unix()-t1)
	return
}

需要根据是否听牌来出牌:

//根据是否听牌,返回出牌id
/*
如果没有听牌则需要先检查听牌,然后根据是否听牌分析出牌
如果已经听牌,则直接打出摸牌的那张牌
params:
	ids:摸牌数组
returns:
	playId:出牌id
	tingSend :是否发送听牌消息
 */
func (this *ErMahjong_AI) PlayByTing(ids []byte) (playId byte, tingSend bool) {
	//检查是否听啤
	if this.isTing {
		playId = ids[0]
	} else { //没有听牌,需要先检查是否听牌
		tingMap := CheckTing(this.hand.tilesIndex)
		index := -1 //出牌索引
		if len(tingMap) > 0 { //如果有听牌,判断该打哪张牌去听牌,一般默认使用可以听最多的牌
			maxLen := 0
			tingSend = true
			for k, v := range tingMap {
				if len(v) > maxLen {
					index = int(k)
					maxLen = len(v)
				}
			}
			this.isTing = true
			this.tilesTing = tingMap[byte(index)]
			share.LOG.Debug("--------机器人听牌,%v", this.tilesTing)
		} else { //没有听牌,需要分析打出哪张牌最好
			this.TileType = AnaTileType(this.hand.tilesIndex, 0, 0)
			//检查应该出哪张牌
			index = PlayByTileType(this.hand.tilesIndex, this.TileType)
		}
		b := byte(index)
		va := IndexToValue(b) //将出牌的索引转换成面值
		flag := false
		//遍历手牌,找到那个出牌的id
		for id, v := range this.hand.tiles {
			if v == va {
				playId = id
				flag = true
				break
			}
		}
		if !flag {//如果上述步骤都没有找到出牌,则打出刚摸得牌,或者打出手牌中最大牌值的牌
			if len(ids) ==0 {//如果没有摸排,打出手牌中牌值最大的牌
				maxValue:=byte(0)
				maxId := byte(0)
				for id,v := range this.hand.tiles {
					if v>maxValue {
						maxId = id
					}
				}
				playId = maxId
			}else {
				playId = ids[0]
			}
		}
	}
	return
}

检查听牌:

//检查听啤
/**
params:
	tileIndexs:电脑手牌索引,已经将摸得牌放入其中的
returns:
	[]byte:返回可以胡牌的牌值
 */
func CheckTing(tileIndexs []byte) map[byte][]byte {
	//key-打出去的牌索引,value:可以听得牌切片
	tingM := make(map[byte][]byte, 0)
	for i, v := range tileIndexs {
		if v != 0 {
			tileIndexs[i]--
			ts := make([]byte, 0)
			//share.LOG.Debug("------checkTing--i:%d",i)
			//fmt.Println("i:",i)
			//遍历所有牌型(将每一个牌型加入到手牌中,然后判断是否胡牌)
			for j := byte(0); j < 34; j++ {
				//去掉除了万牌和字牌的其他牌
				if j == 9 {
					j = 27
				}
				if byte(i) == j { //过滤掉刚打出去的牌
					continue
				}
				tileIndexs[j]++
				//share.LOG.Debug("------*(*(**(**&**(**(**:%d",j)
				//fmt.Println("j:",j)

				isHu := AnalyseTilesIndex(tileIndexs, mahjongTable) //判断是否胡牌
				if isHu { //如果胡牌,将当前牌值,添加到听啤切片中
					ts = append(ts, IndexToValue(j))
				}
				tileIndexs[j]--
			}
			if len(ts) > 0 { //如果听啤切片长度不为0,即有听啤,将听牌切片添加到map中,
				tingM[byte(i)] = ts
			}
			tileIndexs[i]++
		}
	}
	return tingM
}

根据分析的牌型出牌:

//根据TileType选择出牌,返回出牌索引
func PlayByTileType(tilesIndexs []byte, tt *TileType) int {
	share.LOG.Debug("顺子:%v,将:%d,刻子:%v,34:%v,13:%v,单牌:%v", tt.Array_S, tt.Array_j, tt.Array_k, tt.Array_34, tt.Array_1_3, tt.Array_d)
	//判断单牌是否存在
	if len(tt.Array_d) > 0 {
		return int(tt.Array_d[len(tt.Array_d)-1])
	}
	//如果单牌打完,可以打出 13 类型牌
	if len(tt.Array_1_3) > 0 {
		return int(tt.Array_1_3[len(tt.Array_1_3)-1])
	}
	//如果13牌打完,可以打出23
	if len(tt.Array_34) > 0 {
		return int(tt.Array_34[len(tt.Array_34)-1])
	}
	//如果23打完,可以打将牌
	if len(tt.Array_j) > 0 {
		return int(tt.Array_j[len(tt.Array_j)-1])
	}
	//如果上述牌都打完,则可以选择最大的牌打
	i := len(tilesIndexs) - 1
	for ; i >= 0; i-- {
		if tilesIndexs[i] != 0 {
			break
		}
	}
	return i
}

接下来是对事件的处理:

//event事件分析处理,主要是吃 碰 杠,胡牌事件直接胡,不需要处理
/*
	左吃 0x01
	中吃 0x02
	右吃 0x04
	碰 0x08
	杠 0x10
思路:
	1.麻将胡牌方式公式:m*AAA + n*ABC +AA 基本上满足(特殊的牌型不是很多,相比于普通的牌型来说)
	2.机器人操作有个原则,首先尽量形成顺子,对于事件来说,分析每一个事件,然后假设事件成功触发,分析触发后的牌型,要使得单个牌型变少,或者变好。单个牌型指的是1 3、78 或者单牌5,当然78这种牌要比13容易胡,所以最后在
选择上要根据这个方面来考虑。

 */
/**
params
	mask:事件掩码
	tileValues 触发事件那张牌得牌值
 */
func (this *ErMahjong_AI) AiEventLogic(mask int32, tileValues []byte) int {
	value := tileValues[0]       //触发事件牌的面值
	index := ValueToIndex(value) //触发事件牌的索引
	var handIndexs []byte
	handIndexs = append(handIndexs, this.hand.tilesIndex...)
	resultAn := make(map[byte]*TileType, 0) //每种事件牌型判断结果 key值为事件掩码,value为TileTYpe类型
	i := byte(3)
	iMax := byte(8)
	if this.isTing { //如果听牌了,只需要检测是否触发了杠牌事件
		iMax = byte(4)
	}
	for ; i < iMax; i++ {

		if mask&(int32(0x80)>>i) > 0 { //杠 碰 右吃 中吃 左吃
			switch i {
			case 3: //杠
				share.LOG.Debug("杠事件,杠牌值:%d", value)
				handIndexs[index] = 0
				//判断牌型
				tt := AnaTileType(handIndexs, 0x10, value)
				handIndexs[index] = 3
				resultAn[0x10] = tt
			case 4: //碰
				share.LOG.Debug("碰事件,碰牌值:%d", value)
				handIndexs[index] = 0
				tt := AnaTileType(handIndexs, 0x08, value)
				handIndexs[index] = 2
				resultAn[0x08] = tt
			case 5: //右吃
				share.LOG.Debug("右吃事件,右吃牌值:%d", value)
				handIndexs[index] --
				handIndexs[index-1] --
				handIndexs[index-2]--
				tt := AnaTileType(handIndexs, 0x04, value)
				handIndexs[index] ++
				handIndexs[index-1]++
				handIndexs[index-2]++
				resultAn[0x04] = tt
			case 6: //中吃
				share.LOG.Debug("中吃事件,中吃牌值:%d", value)
				handIndexs[index] --
				handIndexs[index-1] --
				handIndexs[index+1]--
				tt := AnaTileType(handIndexs, 0x02, value)
				handIndexs[index] ++
				handIndexs[index-1] ++
				handIndexs[index+1]++
				resultAn[0x02] = tt
			case 7: //左吃
				share.LOG.Debug("左吃事件,左吃牌值:%d", value)
				handIndexs[index] --
				handIndexs[index+1] --
				handIndexs[index+2]--
				tt := AnaTileType(handIndexs, 0x01, value)
				handIndexs[index] ++
				handIndexs[index+1] ++
				handIndexs[index+2]++
				resultAn[0x01] = tt
			}
		}
	}
	return int(AnalysisEventTileTypeMap(resultAn, this.TileType))

}

//分析触发事件形成的牌型TileType
/**
params
	tts: 触发事件形成的牌型map集合
	lastTileType :触发事件之前的牌型
returns
	byte:返回可以出发事件的掩码
 */
func AnalysisEventTileTypeMap(tts map[byte]*TileType, lastTileType *TileType) byte {
	//遍历每个事件触发后的牌型,分析,找出一个最好的牌型
	/**
		最好的牌型其实就是 单牌为0 和23、13类的牌型为0,
	所以最终比较结果还是以这三个为准
		这三个标准中,要以单牌、13、23的优先级进行比较可以给这三个值加一个权值,比如:0.5,0.3,0.2
	最终的计算方式为: 0x5*单牌个数 + 0.3*13个数 + 0.2*23个数
	结果越小牌型越好
	 */
	//事件之前牌型加权计算值
	lastQ := 0.5*float32(len(lastTileType.Array_d)) + 0.3*float32(len(lastTileType.Array_1_3)) + 0.2*float32(len(lastTileType.Array_34))
	share.LOG.Debug("出牌权值:%d", lastQ)
	//最终相应事件掩码
	resultMask := byte(0x00)
	//计算事件牌型的加权值
	for mask, v := range tts {
		nowQ := 0.5*float32(len(v.Array_d)) + 0.3*float32(len(v.Array_1_3)) + 0.2*float32(len(v.Array_34))
		share.LOG.Debug("当前事件权值:%d,上一个事件权值:%d", nowQ, lastQ)
		if nowQ <= lastQ {
			resultMask = mask
			lastQ = nowQ
		}
	}
	share.LOG.Debug("----------ailogic:是否触发事件:%d",resultMask)
	return resultMask
}

下面是一下工具:

//将手牌的tile_num转换成索引切片
func mapToIndex(tile_num map[byte]int) []byte {
	tiles := make([]byte, 0)
	for value, num := range tile_num {
		for i := 0; i < num; i++ {
			tiles = append(tiles, value)
		}
	}
	return ToIndexArray(tiles)
}

//索引转面值切片
func IndexToValues(indexs []byte) ([]byte) {
	res := make([]byte, 0)
	for i := byte(0); i < byte(len(indexs)); i++ {
		v := indexs[i]
		if v == 0 {
			continue
		}
		for j := byte(0); j < v; j++ {
			if i < 27 {
				res = append(res, (i/9)<<4|(i%9+1))
			} else {
				res = append(res, byte(0x30|(i-27+1)))
			}
		}

	}
	return res
}

//索引转牌面
func IndexToValue(index byte) (byte) {
	if index < 27 {
		var color byte = index / 9
		var color2 byte = color << 4
		var val byte = index%9 + 1
		var ret = color2 | val
		return ret
	} else {
		return byte(0x30 | (index - 27 + 1))
	}
}

//牌面转索引
func ValueToIndex(tile byte) (byte) {
	return ((tile&0xF0)>>4)*9 + (tile & 0x0F) - 1
}

//牌值数组转索引切片
func ToIndexArray(array []byte) ([]byte) {
	var indexArray [34]byte
	for _, v := range array {
		i := ((v&0xF0)>>4)*9 + (v & 0x0F) - 1
		indexArray[i] ++
	}
	return indexArray[:]
}

//根据索引,查表判断是否胡牌
/**
params:
	tileIndexs: 要判断的牌的索引
	table;胡牌麻将表
 */
func AnalyseTilesIndex(tileIndexs []byte, table map[uint32][]uint32) (bool) {
	pos := make([]int, 14)
	key := CalCulateKey(tileIndexs, pos)
	_, found := table[key]
	return found
}

//计算牌型key
func CalCulateKey(tiles []byte, pos []int) uint32 {
	p := -1
	var x uint32 = 0
	pos_p := 0
	b := false
	for i := 0; i < 3; i++ {
		for j := 0; j < 9; j++ {
			if tiles[i*9+j] == 0 {
				if b {
					b = false
					x |= 0x1 << uint(p)
					p++
				}
			} else {
				p++
				b = true
				pos[pos_p] = i*9 + j
				pos_p ++
				switch tiles[i*9+j] {
				case 2:
					x |= 0x3 << uint(p)
					p += 2
				case 3:
					x |= 0xF << uint(p)
					p += 4
				case 4:
					x |= 0x3F << uint(p)
					p += 6
				}
			}
		}
		if b {
			b = false
			x |= 0x1 << uint(p)
			p++
		}
	}
	for i := 27; i <= 33; i ++ {
		if tiles[i] > 0 {
			p ++
			pos[pos_p] = i
			pos_p ++
			switch tiles[i] {
			case 2:
				x |= 0x3 << uint(p)
				p += 2
			case 3:
				x |= 0xF << uint(p)
				p += 4
			case 4:
				x |= 0x3F << uint(p)
				p += 6
			}
			x |= 0x1 << uint(p)
			p ++
		}
	}
	return x
}

//将麻将数组转换成汉字
func MahjongSToChinese(tiles []byte) string {
	str := ""
	for _, v := range tiles {
		cbValue := int(v & 0x0F)
		cbColor := int((v & 0xF0) >> 4)
		if cbColor < 3 {
			str += " " + fmt.Sprintf("%s%s", ValueToChinese(cbValue), ColorToChinese(cbColor))
		} else {
			str += " " + HonorToChinese(cbValue)
		}
	}
	return str
}

//将单个麻将牌转换成汉子
func MahjongToChinese(tile byte) string {
	cbValue := int(tile & 0x0F)
	cbColor := int((tile & 0xF0) >> 4)
	if cbColor < 3 {
		return fmt.Sprintf("%s%s ", ValueToChinese(cbValue), ColorToChinese(cbColor))
	} else {
		return HonorToChinese(cbValue)
	}
}

func ValueToChinese(v int) string {
	switch v {
	case 1:
		return "一"
	case 2:
		return "二"
	case 3:
		return "三"
	case 4:
		return "四"
	case 5:
		return "五"
	case 6:
		return "六"
	case 7:
		return "七"
	case 8:
		return "八"
	case 9:
		return "九"
	}
	return "错"
}

func ColorToChinese(c int) string {
	switch c {
	case 0:
		return "万"
	case 1:
		return "条"
	case 2:
		return "筒"
	}
	return "错"
}

func HonorToChinese(h int) string {
	switch h {
	case 1:
		return "东"
	case 2:
		return "南"
	case 3:
		return "西"
	case 4:
		return "北"
	case 5:
		return "中"
	case 6:
		return "发"
	case 7:
		return "白"
	}
	return "错"
}

经过测试ai对人类的比赛中,ai胜率基本为65以上。
以上的算法中,并没有加入对桌面上已经打出的牌的分析,接下来我们可以对桌面上已经打出的牌进行分析,比如分析听牌时选择听剩余比较多的那张。这是的机器人的难度为高,如果还想加强难度,可以在游戏服务器逻辑处对发牌进行控制,可以控制玩家输赢(当然这种就比较贱了,某t经常会这么干)。

  • 5
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
下面是一个使用 TensorFlow 和 Keras 实现麻将AI代码示例,主要使用了深度强化学习算法: ```python import numpy as np import tensorflow as tf from tensorflow import keras class MahjongAI: def __init__(self): self.model = self.build_model() self.target_model = self.build_model() self.target_model.set_weights(self.model.get_weights()) self.replay_buffer = [] self.batch_size = 32 self.gamma = 0.99 self.epsilon = 1.0 self.epsilon_min = 0.01 self.epsilon_decay = 0.995 self.update_freq = 1000 self.steps_since_update = 0 def build_model(self): model = keras.Sequential() model.add(keras.layers.Dense(128, input_dim=34, activation='relu')) model.add(keras.layers.Dense(128, activation='relu')) model.add(keras.layers.Dense(34, activation='linear')) model.compile(loss='mse', optimizer=keras.optimizers.Adam()) return model def select_action(self, state): if np.random.rand() < self.epsilon: return np.random.randint(0, 34) q_values = self.model.predict(state.reshape(1, -1))[0] return np.argmax(q_values) def update_replay_buffer(self, state, action, reward, next_state, done): self.replay_buffer.append((state, action, reward, next_state, done)) if len(self.replay_buffer) > 100000: self.replay_buffer.pop(0) def update_model(self): if len(self.replay_buffer) < self.batch_size: return batch = np.random.choice(self.replay_buffer, self.batch_size) states = np.array([transition[0] for transition in batch]) actions = np.array([transition[1] for transition in batch]) rewards = np.array([transition[2] for transition in batch]) next_states = np.array([transition[3] for transition in batch]) dones = np.array([transition[4] for transition in batch]) q_values = self.model.predict(states) next_q_values = self.target_model.predict(next_states) for i in range(self.batch_size): if dones[i]: q_values[i][actions[i]] = rewards[i] else: q_values[i][actions[i]] = rewards[i] + self.gamma * np.max(next_q_values[i]) self.model.fit(states, q_values, verbose=0) self.steps_since_update += 1 if self.steps_since_update % self.update_freq == 0: self.target_model.set_weights(self.model.get_weights()) def train(self, env, episodes=100): for episode in range(episodes): state = env.reset() done = False total_reward = 0 while not done: action = self.select_action(state) next_state, reward, done, _ = env.step(action) self.update_replay_buffer(state, action, reward, next_state, done) self.update_model() state = next_state total_reward += reward print('Episode', episode+1, 'Total Reward:', total_reward) if self.epsilon > self.epsilon_min: self.epsilon *= self.epsilon_decay def play(self, env): state = env.reset() done = False while not done: action = self.select_action(state) print('Action:', action) state, _, done, _ = env.step(action) if __name__ == '__main__': ai = MahjongAI() # TODO: 实现麻将游戏环境,并传入 train 或 play 方法中进行训练或游戏 ``` 这个示例代码使用了深度强化学习算法,建立了一个神经网络模型来预测每个操作的质量分数,使用经验回放和目标网络来提高训练效率。在实际使用时,需要根据实际情况对代码进行修改和调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值