这是华为举办的一个软件竞赛,华为提供一个德州扑克台桌的server,我们要根据牌型等因素,给出出牌的策略,类似模拟牌手的程序。从知道挑战的题目到提交最终版本的程序中间只有一个月的时间,刚看到这个题目一点头绪没有,看了论文有用蒙特卡洛模拟,决策树等,各种没听过的词汇,感觉写出这个程序会很难,和我一个教研室的小伙伴们看到这个题目的时候陆续都放弃了。
思考了半天我也放弃了,因为接下来几个星期还有实习的面试和小论文等着我完成,就这样过了2个星期,期间我把小论文与实习面试都完成了,在一天早晨无意中又点开了比赛的页面,我想试一试,最起码调通一个与sever通信的socket,说动手就动手,先选定编程语言,用的java,java我每天都会编一些小程序,所以用的比较熟练。中午就开始搭建环境,用的unbuntu的环境,以前用的都是VMware来搭建虚拟环境,这次华为指定用的是VirtualBox,很快第一个问题就来了,我用远程登陆工具登陆linux的22端口怎么也登不上去。期间上网查了各种原因,都是无用功,最后只有使用终极大招,将网络连接方式改为host
only,但是这样的方式linux虚拟机就不能联网了。
接下来就要写socket部分的软件了,原以为这是最简单的部分,但就是这部分我改了不下10遍。首先华为的server是用c++写的,所以与java进行socket通信的方式只能用字节流的方式,很快利用最简单的带参数newsocket方法,就与server端建立了链接.问题1很快就来了,由于server端可能比client端启动要慢所以当client端检测不到远端sever端口的时候,就直接跪了。解决的方法很简单在client端加了个延时sleep方法,这个问题就解决了。问题2就是每次启动完后一个client后,运行结束后,立即再运行就不可以,报出unconnect错,这个问题搜了一下就解决掉了,原来是client端可能有端口复用的问题,于是将socket改为无参数newsocket的方法,然后将socket的setReuseAddress设为true,同时为了防止其他的情况,我写了一个while循环,来判断socket是否已经建立,如果没有建立,每隔450毫秒就重新建立,这样的socket就完美了。socket部分就这样完成了,最起码能够与server稳定的建立连接收到注册成功消息。我将策略设为每次都fold,也就是无论什么牌都弃牌,这样除了大小盲要付出筹码,其他的情况都不需要付出筹码,所以全fold的情况最起码能玩到200多局,这时候最诡异的错误出现了,每次到100多局就会出现unconnected。华为提供的server有个机制,就是每次询问你的出牌策略后(notify消息),如果10次没有应答就会把你unconnect,然而我设置的是每次收到notify就发送fold消息,怎么会出错呢。我把每次收到的消息打印到本地文件,才发现原来有的消息发送了一般,有的把下一个消息也发送了,原来这是tcp的通病,叫做粘包。很快我就找到了解决的思路,我把收到的消息统一存储到一个大字符串里,每次截取一个有用信息,这样就解决了粘包的问题。
在解决socket的同时还有一个问题,就是每次启动我的本地程序,都是利用一个game的脚本,由于对shell编程基本是只闻其名,所以看了几小时的博客,算是将就着把shell简单了解下,其实我的shell脚本只要实现一个功能,根据相对路径找到Game.class启动程序,原来linux的简单指令可以直接用在shell脚本中,cd到我的主程序目录,启动我的java程序,就这样搞定了。顺带着把makefile文件的脚本编了,这样每次只需使用makefile脚本就可以完成编译与编译文件的转移了,省的每次都敲一大串javac指令。
完成搭建环境与socket部分已经过了3天了,接着就开始进入出牌策略的环节了,我自己建立了一个模型,包括扑克和对牌局的抽象。扑克的抽象主要是花色和大小,较为困难的是判断牌型的问题。牌局的抽象也很复杂,由于之前根本就没玩过德州扑克,也不知道要提取哪些信息,于是下载了个天天德州,每天睡觉前都会晚上几个小时,与此同时通过看别人的论文,知道了一些德州扑克的常用技巧,建完模型,还没有写出牌策略离提交代码就只剩下不到4天了,而中还没有调通过一次。我把出牌的策略分为两个部分,一个是在拿到两张手牌之前,这个阶段的出牌策略较为简单。一个是在拿到手牌之后,这个阶段的出牌策略就比较复杂,由于时间紧凑,我没有写一些高端的机器学习等策略。就对一些情况做出判断后,给出了不同的出牌策略。这部分也是很难调试,期间遇到各种NullPoint,各种摸不着头脑的错误。但是每遇到一个困难,通过debug都能很好地找到出错原因,java就是比c++好调试啊。
第一次将socket和策略部分联调,竟然奇迹般的成功,没有一个bug。然后就与华为提供的葫芦娃玩了几局,所谓的葫芦娃就是一群只会一个策略的无脑程序,然而就是简单地无脑娃把我得程序各种花式吊打,最终花了一天的时间调试各种参数,总算是能玩过葫芦娃了。最后的一段时间真的很紧迫,要不是组委会决定程序可以缓两天交,我真的就没法完成了。组后的几天基本都是一起床就开始搞,看游戏log,调程序,改bug。某天在贴吧里看到可以把程序发给某个官方的邮箱,就可以在实际的环境中与其他人对决。规则是这样的,随机抽取8人一个牌桌,前四名进如下一轮,我总共比了三次,每次都在round3,看来很稳定啊,谋事在人,成事在天。不可强也!坐等比赛的结果了。
过了大约一个星期出了结果,进入了32强。然而只有区域的4强才能进入决赛,从知道进入复赛到复赛开始只有一个星期的时间。复赛的内容不变,只是我们有了和别人比赛的log,可以作为自己制定策略的参考。复赛的水平肯定比预赛的要强不少,用我的初赛if else程序肯定是没戏的,这时候我看到了一篇哈工大的硕士论文,用的是蒙特卡洛模拟。于是仔细搜了一下这个东东,原来也没有听上去那么高大上,就是尽量多的模拟,来求出你获胜的概率,模拟的次数越多也就越接近真实的获胜概率。就像在一个正方形中有一个内切圆,然后向其中丢黄豆,就可已根据掉在圆中的黄豆的比例求出圆的面积。很快我就把模拟的程序写好了,并且作为一个线程调用,这样就可以开多个线程进行牌桌的模拟了,当然线程数也不是越多越好,在我本机上测试4线程的模拟效率最高。然而结果并不是我想象的那样,每次都被我原来的程序吊打,综合分析原因,还是模拟次数太少,于是开始各种搜,终于在github上看到一个很牛逼的想法,只需32位int整数就可以存储并判断你的牌型,而且所有的牌型都已经存在数组里,判断牌型快的飞起,于是果断的移植,模拟次数瞬间增加了100倍。原来的程序也是各种被花式吊打,然而写完这套程序离复赛只有一个晚上了,并没有在实际环境中与别人比过,当要用这个新程序比赛,其实开始我心里是拒绝的。第一:我的新程序要多线程模拟,所以必须开一个守护线程控制模拟时间,发回消息的时间只有500ms,但是我要尽量多的时间去模拟,所以实际环境中socket的发送,接收延时是我要考虑的因素,毕竟在我的机器上server与cliernt是在一起的,所以延时可以不作为考虑因素。第二:模拟的时间可以从知道自己手牌开始,而不一定是从接收到notify信息开始,但是这存在线程参数的传递与何时结束的问题,因为我知道手牌的时候并不知道牌桌上还有多少人,而我的模拟要知道牌桌上剩的人数,所以在知道牌型的时候,要在不同的人数同时模拟,同时在接到nontify消息的时候找到人数符合的线程获得相应的参数,最终我没有找到好的解决方案。
复赛的结果没给我惊喜,我还是被淘汰了,但是经过这一段时间的编程给我的帮助真的很大,我独立一个人完成了整个项目的建模,编写,调试,测试。这是最让我自豪的一件事。
版权声明:本文为博主原创文章,未经博主允许不得转载。