一个21点游戏的设计与实现_weixin_33936401的博客-程序员宅基地

技术标签: python  java  测试  

21点游戏 v0.1.0

这是一个纸牌的赌博游戏。

这个游戏的规则:每个玩家分别与庄家比点数,但是又不能超过21点。这是最原始,最简单的规则。但是发展到现代。游戏的规则已经变得复杂了很多。比如双倍下注、分牌、五龙等这样例牌的出现。

具体规则可以在维基百科上搜索“21点”。

架构目标

它们的架构与功能是完全独立的。这意味着我们在构建系统时,应该选择最符合当前需求的架构,并在该架构的框架下实现功能。——《恰如其分的软件架构》

虽然这个21点看似简单,其实不然。当然,如果你只是想实现一个只有最简单21点游戏,而不考虑可扩展性可维护性,你大可使用一个大大的if-else实现。这也算是一种架构。

如果你的目标是:当QQ游戏需要添加21点游戏时,只要在你21点游戏底层库上做些配置就可以直接用了(在写本文时,我真不知道QQ游戏到底有没21点游戏)。 或者某家在线赌博网站想运营一个21点游戏,拿来你的21点游戏底层库,在上面进行配置或扩展,就可以直接用了。

说得再细一些。有些赌场对于Blackjack是3倍赔率,而有些是2倍。有些赌场允许分牌后再分, 有些则不允许。有些赌场只允许在首两张牌时进行加注,而有些赌场允许在任何时候进行加注。

而敝人也希望这个底层库尽可能实现所有的场景。

难点及解决方案

难点在于21点游戏的规则没有标准,每个赌场的规则都可能不同。而我们写出的程序必须应对所有的变化。

解决方案是

  1. 实现21点游戏的领域特定语言,各个赌场只需要修改领域特定语言就可以,而不需要关心具体实现技术
  2. 将游戏中相关稳定的部分(如游戏流程,开牌每人手上都会有两张牌)固化下来,使用java实现。而游戏中变化的部分使用 动态语言实现。

而我采用的是第二种解决方案。

分析与设计

首先要知道21点的游戏流程是不会变的,而变的则是例牌的设置、例牌的赔率,而且同一例牌的规则也可能会不同。

所以,游戏流程可以写死,而例牌方面直接使用动态语言Groovy来实现。

设计中有几个重要的概念需要介绍,这样有利于了解我的设计与架构:

玩家(player):首先庄家有可能是人,也有可能是机器。所以,不论机器或人,在赌桌上所有的人、机器都是玩家。 当赌局里的庄家是随机的时候,意味着所有的玩家都可能做庄家。

玩家代理(playerProxy):有些规则是允许一个玩家下多门注的。所以,我们抽象出玩家代理这个概念。实际掌握手上牌和赌注的是玩家代理。 玩家的所有的操作实际上都是由玩家代理执行。这样就可以解决一个玩家有多门注,及分牌的问题。

庄家代理(dealerProxy):当一个玩家做了庄家,那么他在赌局中实际是通过庄家代理来完成操作。

牌型(cardCategory):牌型指的是手上牌所符合的模式。比如首两张牌组成21点,那么这手牌的牌型就是Blackjack。由于赔率很大程度与牌型及玩 家操作决定,而且每个赌场的规则又不一样。所以,这部分使用Groovy实现。

玩家操作(playerAction):玩家在游戏中只有有限的几个操作:要牌(hit),停牌(stand)等。所以,这部分,直接使用java实现。

银行(bank):这是对赌场中钱的管理人的一个抽象。统一管理所有的钱、判断玩家是否还有足够的钱进行加注等。

赌桌(tables):玩家加入赌桌进行赌博。而赌桌则是所有玩家玩牌的接口。

使用Groovy来实现的好处有:

  1. 实时修改游戏规则(例牌等),不需要重启服务器
  2. 提高程序的可扩展性
  3. 提高游戏的可配置性

为什么不使用XML来配置呢?

鉴于很多人喜欢使用XML做配置文件,我的想法如下:

  1. XML对于游戏规则的表达力不够
  2. 还需要自己写解释器,注意,这个解释器并不是JDOM这样的库,而是指语义解释器。而且,使用java处理XML,你懂的~
  3. 为什么还有多一层解释呢?配置即执行不多好?目前ROR,Gradle,Vagrant也都是配置即执行。
  4. 本人不喜欢XML的哆嗦

当然,如果你希望实现一个使用XML配置的,那也可以。本库的扩展非常好。只需要改WinnerLoserCalculateEngine这个 输赢计算引擎就可以了。

关于例牌

由于例牌是21点中最大的变化,而且各类繁多。所以,我有必要在这里说明一下,哪些例牌是我已经实现并测试了的。

已经实现了的

  1. 保险 保险与玩家下的筹码是独立的。是单独结算的。 所以分牌时,保险金不会分

  2. Blackjack: 开牌后就得到了21点(一张面牌为10的牌,一张A)

  3. 分牌(split) : 玩家首两张牌的点数相同,则可以选择分牌,并须加注。分出的每门的下注金额须与原注相同。 注:有些赌场可分牌后再分,有些则不能

  4. 双倍下注: 发牌后,玩家手上两张牌的点数加起来有11点,就可以进行双倍下注。但是双倍下注后,只能要一张牌。

  5. 过五龙: 玩家或庄家手上的牌超过了5张,但还没有爆牌的情况。

  6. 同花顺: 玩家手上有三张牌:牌色相同,面值分别是6,7,8的情况。

实际上,你可以发挥你的想像来实现你的例牌。比如,当庄家为Blackjack而玩家为五龙,则为打平等等。

未实现的操作

  1. 投降(surrender) 首两张牌可以投降,其他情况不可以投降。投降后,玩家只可以拿回原来赌注的一半。

  2. Ace分牌(AceSplit): 这是分牌的特例。当玩家首两张牌都是A牌时,可以选择分牌,但是分牌后只能得到一张牌。

  3. 退出

分层

在这里,你看不到什么MVC这样的分层方式。是因为我们这个库目前做的事情就是21点最核心的业务。所以MVC这样的分层方式不适合。 目前,你也看不到关于持久化的内容。因为,一持久化是客户的事情,二我目前还没有时间写一个默认的实现。

Blackjack-core 核心包

这是一个核。实现目标是将21点游戏的业务逻辑集中在这个核内。

它可以做什么:

  • 技术上:

    1. 提供方便,简单的接口
    2. 可自定义你的例牌及赔率,并且是实时的,不需要重启服务器
    3. 洗牌算法的可替换
  • 业务上:

    1. 庄家可以是真人,也可以是机器
    2. 支持一个玩家可以下多注
    3. 未限定投注大小,注的限额,我觉得不是21点游戏的核心问题,所以,不实现。
    4. 可以设置保险的赔率

它不可以做什么:

  1. 不可以持久化:玩家数据,金钱数据都存在内存里
  2. 不提供界面
  3. 发牌时的那种一张牌,一张牌的显示的那样的效果。那不应该是核心业务问题。

Blackjack-server 服务器端

目前还没时间做

关于持久化

目前,关于持久化的工作几乎为0。因为我在游戏开发方面没有经验,有一些问题没有想清楚。

  1. 游戏过程的数据是否需要持久? 游戏过程是指记录下玩家的每张出牌,每次的操作。这个过程,我觉得1.完全没有必要;2.性能上的问题。
  2. 游戏中的金钱问题 关于钱的数据当然要持久化了。而且,在设计上要求非常的高。在这方面的我经验也不足。需要从别人系统学习后再设计。

要解决的问题

  1. 如何实现全自动化测试? 也就是不需要任何的mock。玩家自动玩牌。

  2. 如果没有实现自动化测试,那么也就没有测试并发情况也会发生什么。

  3. Groovy脚本配置的带来的安全问题

如何阅读代码

有一个CasinoTest测试类。测试类模拟的是一场简单的赌博。从中可以看出如何使用此核心库。

下面是跑测试时的输出:

	12:33:54.568 [main] INFO  c.z.blackjack.core.CasinoTest - 三名玩家分别存入赌场
	12:33:54.573 [main] INFO  c.z.blackjack.core.CasinoTest - 甲 存入:400.0
	12:33:54.576 [main] INFO  c.z.blackjack.core.CasinoTest - 乙 存入:400.0
	12:33:54.577 [main] INFO  c.z.blackjack.core.CasinoTest - 丙 存入:400.0
	12:33:54.580 [main] WARN  c.z.blackjack.core.CasinoTest - 三名玩家加入赌桌:1
	12:33:54.584 [main] INFO  c.z.blackjack.core.CasinoTest - 选举出庄家为:甲 
	12:33:54.586 [main] INFO  c.z.blackjack.core.CasinoTest - 玩家乙 下注:20
	12:33:54.586 [main] INFO  c.z.blackjack.core.CasinoTest - 玩家丙 下注:20
	12:33:56.154 [main] INFO  c.z.blackjack.core.CasinoTest - *****开始******
	12:33:56.154 [main] INFO  c.z.blackjack.core.CasinoTest - 乙 手上的牌,点数:[20] 分别为:[{SPADE-Q}, {SPADE-K}]
	12:33:56.154 [main] INFO  c.z.blackjack.core.CasinoTest - 丙 手上的牌,点数:[17] 分别为:[{HEART-J}, {HEART-7}]
	12:33:56.236 [main] INFO  c.z.blackjack.core.CasinoTest - 乙 停牌,点数:20,分别为:[{SPADE-Q}, {SPADE-K}]
	12:33:56.319 [main] INFO  c.z.blackjack.core.CasinoTest - 丙 停牌,点数:17,分别为:[{HEART-J}, {HEART-7}]
	12:33:56.320 [main] INFO  c.z.blackjack.core.CasinoTest - 庄家甲 点数为14不足17点
	12:33:56.474 [main] INFO  c.z.blackjack.core.CasinoTest - 庄家要牌,最后点数:[24] 牌为:[{HEART-6}, {HEART-8}, {DIAMOND-K}]
	12:33:56.474 [main] INFO  c.z.blackjack.core.CasinoTest - **********所有玩家停牌,开始结算
	12:33:56.737 [main] INFO  c.z.blackjack.core.CasinoTest - 乙  下注 20.0 赢家为PLAYER 最后余额:420.0
	12:33:56.817 [main] INFO  c.z.blackjack.core.CasinoTest - 丙  下注 20.0 赢家为PLAYER 最后余额:420.0
	12:33:56.817 [main] INFO  c.z.blackjack.core.CasinoTest - 庄家甲  最后余额:360.0
	12:33:56.818 [main] WARN  c.z.blackjack.core.CasinoTest - *******结束

附录

  1. 保险规则配置

    • 配置文件为类路径下:Insurance.groovy

    • 例子

        //玩家赢时将得到的彩金
        winMoney = playerFirstBet * 2
      
  2. 结算相关配置

    • 实质是配置根据玩家的牌型及庄家的牌来计算输赢,并返回赔率

    • 配置文件为类路径下:Settlement.groovy

    • 例子

        //如果玩家赢,赢多少
        winMoney = 0
      
        switch (playerCardCategory) {
        //同花顺
            case "royal":
                winner = PLAYER
                winMoney = playerBetSum * 3
                break
            case "Blackjack":
                switch (playerLatestAction) {
                    case stand:
                        if (isDealerBlackjack) {
                            winner = PUSH
                        } else if (!isDealerBlackjack) {
                            winner = PLAYER
                            winMoney = playerBetSum * 3
                        }
                    default:
                        winner = PLAYER
                        winMoney = playerBetSum * 3
                        break
                }
                break
            case "fiveCard":
                winner = PLAYER
                winMoney = playerBetSum * 3
                break
            case "bust":
                winner = DEALER
                break
            default:
                //庄家爆牌
                if (dealerSumPoint > 21) {
                    if(playerSumPoint <= 21){
                        winner = PLAYER
                        winMoney = playerBetSum * 1;
                        break
                    }
      
                    if (playerSumPoint > 21) {
                        winner = PUSH
                        winMoney = 0
                        break;
                    }
      
                }else{
                    if (playerSumPoint > dealerSumPoint) {
                        winner = PLAYER
                        winMoney = playerBetSum * 1
                    } else if (playerSumPoint < dealerSumPoint) {
                        winner = DEALER
                    } else {
                        winner = PUSH
                    }
                }
                break
        }
      
  3. 牌型配置

    • 实质是根据玩家手上的牌,计算出手上牌所属的牌型

    • 配置文件为类路径下:Special.groovy

    • 例子

        switch (_headCard.cardCount) {
            case 2:
                if (_headCard.sumPoint == 21) { // 首两张牌点数为21点时是Blackjack
                    name = "Blackjack"
                    actions = [_report]
                } else if (_headCard.sumPoint == 11) {  // 首两张下牌点数等于11点时可以双倍下注
                    name = "doubleDown"
                    actions = [_hit, _stand, _doubleDown]
                } else if (_headCard.isFirstTwoCardPointEquals()) { //首两张牌的面值相同,可进行分牌
                    //设置分牌后不可以再分。
                    if (_headCard.isSplit()) {
                        return
                    }
                    name = "split"
                    actions = [_hit, _stand, _split]
                }
                break
            case 3:
                // 同花顺
                if (_headCard.containsAllCardFace(6, 7, 8) && _headCard.isCardFaceIdentical()) {
                    name = "royal"
                    actions = [_report]
                }
                break
            case 5:
                if (_headCard.sumPoint <= 21){
                    name = "fiveCard"
                    actions = [_report]
                }
                break
        }
      
        // 爆牌
        if (_headCard.sumPoint > 21) {
            name = "bust"
            actions = [_stop]
        }
      
  4. 在配置脚本中可以使用的变量 :

      binding.setVariable("PLAYER", Winner.PLAYER);
      binding.setVariable("DEALER", Winner.DEALER);
      binding.setVariable("PUSH", Winner.PUSH);
    
      binding.setVariable("playerBetAmounts", playerProxy.getBet().getBetAmounts());
      binding.setVariable("playerFirstBet", playerProxy.getBet().getBetAmounts().get(0));
      binding.setVariable("playerBetSum", playerProxy.getBet().getBetSum());
      binding.setVariable("playerLatestAction", playerProxy.getLatestActionName());
      binding.setVariable("isPlayerBlackjack", playerProxy.isBlackJack());
      binding.setVariable("playerCardCategory", playerProxy.getCardCategoryName());
      binding.setVariable("playerSumPoint", playerProxy.getSumPoint());
      binding.setVariable("playerCardCount", playerProxy.getHeadCard().getCardCount());
      binding.setVariable("dealerCardCategory", dealerProxy.getCardCategoryName());
      binding.setVariable("dealerSumPoint", dealerProxy.getSumPoint());
      binding.setVariable("dealerCardCount", dealerProxy.getHeadCard().getCardCount());
      binding.setVariable("isDealerBlackjack", dealerProxy.isBlackJack());
      binding.setVariable(StandAction._name, StandAction._name);
      binding.setVariable(HitAction._name, HitAction._name);
      binding.setVariable(ReportAction._name, ReportAction._name);
      binding.setVariable(DoubleDownAction._name, DoubleDownAction._name);
      binding.setVariable(SplitAction._name, SplitAction._name);
      binding.setVariable(StopAction._name, StopAction._name);
      binding.setVariable(SurrenderAction._name, SurrenderAction._name);
    

这是虚拟玩游戏时的打印。运行CasinoTest.java就可以了

12:35:31.278 [main] INFO c.z.blackjack.core.CasinoTest - 三名玩家分别存入赌场 12:35:31.283 [main] INFO c.z.blackjack.core.CasinoTest - 甲 存入:400.0 12:35:31.285 [main] INFO c.z.blackjack.core.CasinoTest - 乙 存入:400.0 12:35:31.286 [main] INFO c.z.blackjack.core.CasinoTest - 丙 存入:400.0 12:35:31.289 [main] WARN c.z.blackjack.core.CasinoTest - 三名玩家加入赌桌:1 12:35:31.292 [main] INFO c.z.blackjack.core.CasinoTest - 选举出庄家为:丙 12:35:31.293 [main] INFO c.z.blackjack.core.CasinoTest - 玩家甲 下注:20 12:35:31.293 [main] INFO c.z.blackjack.core.CasinoTest - 玩家乙 下注:20 12:35:32.761 [main] INFO c.z.blackjack.core.CasinoTest - 开始* 12:35:32.762 [main] INFO c.z.blackjack.core.CasinoTest - 甲 手上的牌,点数:[11] 分别为:[{HEART-2}, {DIAMOND-9}] 12:35:32.762 [main] INFO c.z.blackjack.core.CasinoTest - 乙 手上的牌,点数:[15] 分别为:[{DIAMOND-Q}, {SPADE-5}] 12:35:33.092 [main] INFO c.z.blackjack.core.CasinoTest - 甲 双倍下注,最后点数:[20],牌为:[{HEART-2}, {DIAMOND-9}, {DIAMOND-9}] 12:35:33.233 [main] INFO c.z.blackjack.core.CasinoTest - 乙 停牌,点数:15,分别为:[{DIAMOND-Q}, {SPADE-5}] 12:35:33.233 [main] INFO c.z.blackjack.core.CasinoTest - 庄家丙 点数为11不足17点 12:35:33.322 [main] INFO c.z.blackjack.core.CasinoTest - 庄家要牌,最后点数:[20] 牌为:[{DIAMOND-8}, {SPADE-3}, {DIAMOND-9}] 12:35:33.322 [main] INFO c.z.blackjack.core.CasinoTest - **********所有玩家停牌,开始结算 12:35:33.536 [main] INFO c.z.blackjack.core.CasinoTest - 甲 下注 40.0 赢家为PUSH 最后余额:400.0 12:35:33.593 [main] INFO c.z.blackjack.core.CasinoTest - 乙 下注 20.0 赢家为DEALER 最后余额:380.0 12:35:33.594 [main] INFO c.z.blackjack.core.CasinoTest - 庄家丙 最后余额:420.0 12:35:33.594 [main] WARN c.z.blackjack.core.CasinoTest - *******结束

转载于:https://my.oschina.net/zjzhai/blog/203842

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_33936401/article/details/91874241

智能推荐

适合女生的计算机配置,适合送女生的粉色台式电脑 2350元i3-8100配H310组装台式机配置推荐...-程序员宅基地

相信不少用户买电脑都是送女生的,而女生多数都是外观党,对电脑机箱的外观十分注重,尤其是粉色的机箱,绝大数的女生都很喜欢。而今天装机之家分享一套粉色的电脑主机,带来一款2350元i3-8100配H310组装台式机配置推荐,粉色主题,由于预留的电源功率较大,方便后期升级显卡,来看看吧。i3-8100配H310组装台式机配置电脑配置清单:i3-8100配H310组装台式机配置推荐配件名称品牌型号参考价格..._女生用台式电脑

实战:结合Dr.Watson系统日志和Vc6来定位多线程环境下程序异常退出的错误(转)-程序员宅基地

当开发的软件发布以后,在客户那运行时可能会因为各种原因导致程序退出。这种情况很尴尬,很明显我们无法在客户机器上装个Visual Studio调试,所以必须有机制来收集出错的信息。软件本身的运行日志能提供部分信息,但是可能还不够。Windows系统为此提供了解决方案:Dr.Watson工具。Dr.Watson也算是一个小巧的调试器,32位的版本名字是drwtsn32.exe。可用于当

MTK平台LCM驱动详细分析(二)-程序员宅基地

接下来:MTK_FB_XRES = DISP_GetScreenWidth();MTK_FB_YRES =DISP_GetScreenHeight();fb_xres_update= MTK_FB_XRES;fb_yres_update= MTK_FB_YRES;printk("[MTKFB]XRES=%d, YRES=%d\n", MTK_FB

【Linux】深入理解重定向、inode详解与软硬链接的概念及区别_inode位图重新定位至-程序员宅基地

进一步认识基础IO再识重定向重定向基本命令dup2系统调用接口代码示例小结理解文件系统磁盘(硬盘)背景文件元数据文件系统inode详细介绍inode是一个文件的属性集合目录的inode软硬链接软链接介绍硬链接介绍本文将进一步介绍重定向、文件系统、inode以及软硬链接的相关知识。再识重定向在之前我们已经知道了输出重定向的概念,即本来应该写入标准输出中的内容却被写入了其他文件中。常见的输出重定向有:>, >>其中>为“w”方式(覆盖)写入文件,而>>为"a"方式(追_inode位图重新定位至

UI设计师工作日常_ui设计师主要是做什么的呢-程序员宅基地

很多人对UI设计的工作日常不是很了解,今天和大家讲讲理解需求在拿到需求后,首先要做的工作就是要去理解这份需求。这时候我们拿到的需求可能非常详细,是交互设计师加工过的具体需求文档,其中包括完整的交互设计原型。我们要重点去理解交互设计的输出。查看产品使用的每一个流程以及每一个界面的具体细节,其中包括功能、操作、反馈以及信息呈现逻辑。而大多数团队中没有交互设计这样的角色,而是产品经理或老板直接下达的需求,需求的精细程度也会存在非常大的区别,这个时候其实就会比较复杂。设计师此时需要理解需求以及将_ui设计师主要是做什么的呢

计算机组成实验八,计算机组成原理实验八内存系统实验_大魔头-诺铁的博客-程序员宅基地

《计算机组成原理实验》报告八评阅姓名 学号时间 四7-9 地点 行健楼 606机房1.内存系统实验1. 实验内容及要求(1)实验内容:1.手动方式把立即数33H写入内存D1H单元。2.手动方式把D1H单元的内容读出,再送入E1H单元。3.在CP226汇编语言程序集成开发环境下编写程序,调试和单微步(跟踪)运行,完成下面任务,观察数据走向及寄存器的输入输出状态。将初..._cp226mic和dat

随便推点

matlab 中如何对.mat文件中的数据画图_matlab读取mat数据并画图-程序员宅基地

1、先打开.mat文件直接双击你要打开的.mat文件,如下我打开.mat文件会发现做下脚有S.mat文件窗口,同时命令行窗口出现load(,,,,,,,,,)2、查看.mat文件中的数据可在命令行窗口输入:whos命令会发现,这里的数据其实就在工作区显示着3、对.mat文件中的数据画图(1)可直接在命令行窗口输入命令画图直接输入 plot()画图函数如:(2)或者新建.m文件用来画图如:..._matlab读取mat数据并画图

360路由器v2刷第三方固件_路由器刷固件图文教程,刷机OpenWrt第三方固件,路由器升级固件...-程序员宅基地

大家好,我是老盖,首先感谢观看本文,本篇文章做的有视频,视频讲述的比较详细,也可以看我发布的视频。今天我们升级刷机自己的路由器,我自己准备了一个老式型号的路由器作为演示,我们刷机升级固件,首先要去下载,在网上搜索啊openwrt就可以了,大家注意一下它的官方网站是个英文网站,不是中文的,不要进入错了。进入官方网站之后,这个就是它的官方网站的界面,大家可以看到是个英文界面,在右侧有个语言选择,可以选...

oppo reno5pro怎么样?-程序员宅基地

Reno5系列将发布三款不同配置的手机,包括Reno5、Reno5Pro和Reno5Pro+。这次Reno5系列将支持40W AirVOOC无线快充,充电速度得到了前所未有的提升,续航能力将得到保证,这次Reno5搭载骁龙775G芯片,Reno5Pro将搭载骁龙860处理器。oppo手机新品 活动优惠力度空前 http://oppo.com.cnReno5主摄是索尼IMX582的4800万像素镜头,还搭载了1200万像素的IMX708传感器作为超广角镜头+1300万像素的长焦镜头,同时也会带有超级夜景和

Linux Shell 文件的排序、合并和分割-程序员宅基地

Linux的文本处理命令,包含sort、uniq、join、cut、paste、split、tr、tar,这些命令能实现对文件记录排序、统计、合并、提取、粘贴、分割、过滤、压缩和解压缩等,它们与sed和awk一起构成了Linux文本处理的所有命令和工具。 5.1 sort命令# sort [选项] [输入文件]选项意义-c测试文件是否已经排序

滤波器之matlab与vivado的联合仿真_vivado如何关联matlab-程序员宅基地

乘法混频输入信号为0.5MHz和5MHz,采用积化和差转换后的输出信号有两个输出频率,分别为4.5MHz和5.5MHz,由于之前设置的截止频率为4MHz,故理论上是不会产生滤波信号,接下来进行仿真验证。1、产生两个信号2.5MHz和5MHz,然后对其进行混频,注意matlab中混频有两种方法,一种是两个信号相加,另一种是两个信号相乘,这两种混频结果是不同的。采用2.5MHz和5MHz信号进行乘法混频,可知混频后的频率为2.5MHz和7.5MHz,故经过滤波后应可获得2.5MHz的滤波结果。_vivado如何关联matlab

echarts圆柱形带背景图_echarts圆柱图背景美化_js0918may的博客-程序员宅基地

项目有时需要我们将二维的柱状图做出立体的感觉出来,这里是将一个数据柱形放在了后面当背景图使用_echarts圆柱图背景美化