我们一起学程序-五子棋
前言
小时候很喜欢玩电视上自带的积木游戏,那时候电子产品也不少,小学就认识了low和high两个单词,因此攒了零花钱搞到了高级版的游戏--小霸王学习机,说错了,是“游戏机”,特别是一放暑假,插个小霸王游戏机卡就能开始撸几把魂斗罗,坦克大战,比夏天吃根冰棍可香多了,那时候没有空调,不惧严寒酷暑的我们在这些小游戏的陪伴下玩的不亦乐乎,陪我们度过一个又一个快乐冬夏,下图为证,有没有很熟悉的赶脚!
看着这些画面,带着学习的心情的出发了,开始我们的五子棋大战;砍柴磨刀互相不耽误,先看看实现的网络版本效果吧,当然为了你看着舒服,体验的开心,博主花了些功夫大致在客户端(手机)做了适配,不至于你屏幕溢出,你爽我爽大家爽;简单说明下本人主攻后端Java,前端不深入,页面效果将就看看哈!
体验效果
看看你想下盘棋,你需要做什么,首先你需要抢个位子啊【我媳妇说这个举手的小人太丑了,能换个动漫吗,我说我对漫画没啥审美,但是对另一半要求很高,所以我们在一起了,嘻,这个求生欲也特强了吧!】,然后你就到棋盘页面了,点击举手坐下,你就开始等,有人来了也举手了你们就可以开始了,就像你去棋牌室打牌一模一样,如果硬说有啥区别,咱们这是学习,他们那是赌博,好了,好了,扯远了,简单介绍下,五子棋的规则大家都懂,你五个棋子连成一条线就胜利了,一攻一防,胜率就看你的手段了,不知道你有没有发现,博主为了不让你有更多的束缚,连账户密码都默认为你输出了,同时为了让程序不是那么单调,下棋还能发声,还实现了查看复盘,和在线聊天,聊天IP内容已经存档,自动过滤色情信息;博主为你们担心的事情都精心考虑设计实现,是不是暖男你们说了算!下面两个动画分别是“准备过程”和“下棋过程”;
体验地址:棋子棋体验地址,有些功能可能有不完善的地方,功能也不特别太全,但基本功能应该是满足了,如果没有人陪你玩,你可以开两个客户端体验或call me,一毛钱一把,欢迎来战,如果有强迫症的或是有good idea,想要加入我们的修改的童鞋,欢迎留言加入我们,让我们共同成长。
游戏准备过程
下棋过程
关键技术简介
其实在开发完这个小游戏后,回顾总了下,没有啥高精尖的技术,前端页面完全是html,css(样式),js+jquery(脚本语言),像“举手准备”,“画棋盘”等都是用到的这些前端技术,大家看到的淘宝页面基本都是基于这些技术来完成的;后端用到的技术是websocket,很多面试官会问到websocket和http的区别,博主简单介绍下,它是一个双向通信的协议,这个不难理解,如果你打我了,我也可以还手,不像http,它是单向的,比如你打我了,我就只能默默忍着,顶多就能回骂一句,绝不能还手,相信在这个人人平等的社会,你肯定喜欢websocket,他帮助我们实现了在线聊天,咿!我怎么突然想到了QQ,感觉我像腾讯的高级攻城狮一样牛掰;当然了,作为一个Java攻城狮,必须要理解的就是线程啦,我们里面的“查看复盘”的功能就用到了Java读写文件和多线程技术;是不是小小的程序还有大大的奥妙;好啦,就到这里吧,明不明白都不要紧,因为我们后面详细一点你就能明白了!
总结下:前端css+js实现画页面和交互,后台Java+websocket实现存储和通信!相信有一点基础的你,肯定也能实现这个程序;在实际开发中,前后端是两个不同的岗位,前后端各司其职,当然有些小公司要求程序员都要会,所以有些时候在挑选公司时擦眼睛不是眼里进了沙子,是为了看的更清楚!
从无到有思路
这个程序博主一个人断断续续差不多花了两个礼拜完成的,当然你肯定怀疑我是否有一气呵成的能力,可以肯定的回答你,你的怀疑是对的,因为我也是人,我不能忘了吃饭和睡觉;最原始最原始的要追忆到两周前,下图是最初的版本,这是在idea控制台输出的,是的,你没有看错,五子棋程序实现了,“五子棋连线成功,恭喜你赢了”,说实话,打出这句话,连我这么没有追求的人都觉得他太枯燥了,生活它需要仪式感,你上面看到的页面都是我在仪式感基础上添加的;新事物,全新的页面,至少我感受到了程序至少还有那么一丝丝的美,是的,至少此刻我沦陷了,学程序我们要有意识的去追求一下她的美,不然他会教你从入门到放弃;
慢慢的,“仪式感”让我走向一条不归路,首先需要解决的问题,就是棋盘的问题,现实中有木头刻制的棋盘,网络上可以用代码生成的棋盘,现实的中棋盘有她独特的美感,网络中这些棋盘他就更灵活了,每个方格,棋盘大小都是可调整的,成本微乎其微,有了棋盘那就需要把棋子落在棋盘上,这些可以用css+js画出来的,还不用浪费木材,落子了每次都需要判断这次走的这步棋是否连成了一个五子,如果连成五子就赢了,如果没有就换对手下棋,每一步过程组合在一起就组成了棋盘,电脑有超强计算和存储能力,人脑也有,但电脑没有人脑灵活,人工智能尚在起步,未来可期;
下完棋后想看下过程于是就有了“复盘回看”功能;下棋要沟通就有了网络聊天室;棋牌室永远不止一个座位,于是就需要支持多个座位,多人同时娱乐;当然你也可以说自己水平不够,需要人机对战,提升经验,或者有人说游戏赢了或输了要有对应的奖惩机制,没错,你能想到的,有兴趣的事情你都可以动手实现,兴趣或者学习目的都好,总比无聊消磨时光来的更有意义;这就是我从无到有一步步构建出来的程序。欢迎和我一起讨论学习!【公众号:叫练】
代码实现过程
1.棋盘实现
棋盘是用table表格画的,像你看到博主实现的这个表格,横轴是26,纵轴是23,每个格子的大小都是40像素,不信你话可以量量哈,前端循环放置在div中就可以了,格子像素是固定的,因为我们需要通过像素计算最终得到坐标,下棋过程就是通过点击格子,把白或黑的棋子(图片)放置上去,那怎么计算坐标呢?你需要计算相对于棋盘点击的坐标,也就是下图我们的红圈,我们知道棋盘表格(table)是整个页面(body)中的一部分,js中click事件可以获取到全局事件event变量,所以能够获取到相对于body的event.left,event.top,再减去table的top,left就可以得到表格具体的像素,table的top left可以通过getBoundingClientRect方法获取;table的像素再除40px就是具体坐标了;大家在实现的时候还需要注意一个有效点击,只有在这个范围内才算有效范围,像我们这个程序,是以坐标点15px都算有效点击;那么无效点击范围在16px-25px,也就是点击格子中间是无效的。举个例子吧,比如我们现在算出表格具体坐标是top:130px,left:70px,先计算这个坐标是否有效,130%40=10, 70%40=30,都不在无效范围内,说明是有效坐标,我们这里横纵坐标分别记为xy,那么具体坐标就y=130/40 =3,x=70/40=1,此时x坐标需要加上1,因为x坐标大于25,靠右,需要再除的基础上加上1,所以最终横纵坐标为(2,3),这个过程涉及到一点小算法,你对着下图再思考下这个计算过程看看是否有不明白的,万事开头难!下面的图方便大家理解!
2.计算棋子是否获胜
在写这个之前先给大家爆料下,没思考听到这个问题前呼吸都是混乱的,说实话真的好难啊,什么?还要实现,我TM不想学习了!嘿,兄嘚,别怕,有我在呢!怕解决不了问题,我牵着你,我们在一起吧!
兵马未动,图表先行,给大家先上个图吧,其实就四条线;一个"米"子,会写米字,问题就解决了,也就是说每次落子后需要逐一判断这4条线上是否有五个相同颜色的棋子,其实对应Java程序程序来说,他是用一个二维数组形式来存储的棋盘数据的,我们以红色为中心点,其实这个过程就是通过方式1(纵向):先向上找,找红色的棋子,找到一个计数器就+1,不是红色的棋子直接break退出,同理向下找,找红色的棋子,找到一个计数器就+1,不是红色的棋子直接break退出,最终如果满足5个相同颜色的棋就算成功,也不用再通过方式2,3,4找了,如果方式1不满足条件,方式1就结束了,接下来就按方式2(左斜)查找,也就是同样的套路,一直到方式4为止,哈哈,是不是有点感觉了!先以方式1查找举例子吧,对二维数组来说,以先上找后下找,上找:横坐标不变,纵坐标递减;下找:横坐标不变,纵坐标递增。再按方式三举例,先右上后左下查找,右上:横坐标+1,纵坐标-1,左下:横坐标-1,纵坐标+1;你看看原理还是很简单吧!isSuccess方法是按方式1查找的。
public boolean isSuccess(int x, int y,int color,int[][] oriData) { boolean flag = false; int count = 0; //(2)上-下,左-右,左上-右下,右上-左下; 4种方式 // 方式1 :上-下 x相同,y不同 //上 纵坐标递减; for (int i = y-1; i>-1; i--) { //判断同一颜色的子; if (oriData[x][i] != color) break; count++; } //下 纵坐标递增; for (int i = y+1; i<GameManager.Y; i++) { //判断同一颜色的子; if (oriData[x][i] != color) break; count++; } //重置 if(count >=4) return true; else count = 0; // 方式2 // 方式3 // 方式4 return falg }
3.网络聊天室实现
不知道你有没有发现,你有时候浏览网页会弹出一些广告页,上面时长会自动弹出人工客服窗口,不需要登陆什么的,你们能在线对话,其实这个功能可能就是websocket实现的,我们说过,websocket是双向的,在实现上和http一样,都是基于TCP实现的应用层协议,除了http是单向,ws是双向的。在用法上http是以http://头请求的短连接,是一次请求,ws是以ws://请求头的长连接,我们在“五子棋游戏大厅”点击位置进入棋盘页面其实就是一个连接过程,前端通过ws://ip:port/xx形式连接后台暴露的地址,会触发后台onOpen函数,每个连接在后台都是以不同Session对象存在,通过Session可以获取每个每个网页的session的id,就可以调用sendText方法,通知对应的浏览器,这样一个过程就实现了浏览器和后台交互;比如我们的对话,每个桌子是保存两个用户(Session),当其中某一个用户发送了某段话,会通知对应的浏览器,另一个用户也会把这句话发给对应的浏览器,所以你看到的两个页面都出现了一样的文字,其实原理还是比较简单的。
我们画个图来理解下这个过程。
websocket通信过程
下面这两个段代码是websocket客户端和服务器交互的代码,只贴了部分源码,限于篇幅有限,如果需要完整代码,下方【环境部署部分】给了源码下载压缩包
webSocket = new WebSocket("ws://"+serverIp+"/game/game/"+table+"/" + nickname);
1 /** 2 * websocket服务连接入口 3 * update by jiaolian 2020 8 12 4 * 公众号:叫练 5 */ 6 @ServerEndpoint("/game/{table}/{nickname}") 7 public class WebserviceSession { 8 @OnOpen 9 public void onOpen(@PathParam("table") String tableName,@PathParam("nickname") String nickname, Session session) throws IOException {} 10 @OnMessage 11 public void onMessage(@PathParam("table") String tableName,String message, @PathParam("nickname") String nickname) {} 12 @OnClose 13 public void onClose(Session session,@PathParam("nickname") String nickname,@PathParam("table") String roomName) {} 14 }
4.查看复盘实现
这个功能是线程+文件实现的,既然需要查看复盘,那说明我们需要先写复盘,复盘其实就是写一个文件,查看复盘就是读文件,在一局棋中,每次落子就会写一行数据,每行数据会记录棋子的x,y坐标和棋子的颜色,按空格隔开,我们代码中Table【棋盘】对象保存color约定1为黑子,2为白子,当一局游戏结束后,查看复盘会生成对应一个线程,以每秒一行的速度读取保存在服务器的文件,文件名以“桌号+时间戳.wzq”命名,如:1_2020_09_01_15_45_15.wzq,每行数据读取出来后,会单独发给我们的User【Session】,浏览器收到通知后渲染页面就完事了;贴一段简单的代码;其中Table类是用来保存棋盘的;User类是用来保存用户信息的;finishReplayer()方法是读取文件,sendTextSingle()方法负责给浏览器发送信息;可以参考上面websocket通信过程;
/** * @author :jiaolian * @date :Created in 2020-08-14 13:30 * @description:桌号 * @modified By: * 公众号:叫练 */ public class Table { //保存用户信息 @JsonIgnore private List<User> userList = new CopyOnWriteArrayList<>(); //桌号名 private String name; //默认棋盘 private int[][] oriData = new int[GameManager.X][GameManager.Y]; //约定:1 黑子 2 白子 -1 无子(页面选棋子) 此时应该走动棋子颜色; //棋子颜色<默认黑子先走> private int color = CONST.CHEER_COLOR.BLACK; //对应的文件名<复盘> 最近一次 private String fileName; //桌子状态; 已开始/未开始 private int tableStatus; private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss"); private List<User> tempList; }
/** * @author :jiaolian * @date :Created in 2020-08-14 13:56 * @description:在线用户 * @modified By: * 公众号:叫练 */ public class User { //websocket sessoin; private Session session; private String nickname; //棋手是否已准备; private boolean isPrepared; //用户的复盘线程;key规则:桌号_用户名,为什么这么设计? 因为一个用户可能在多个桌子上同时游戏; //val值设计成Boolean,主要是查询对应桌数用户线程是否执行完毕; 如果有疑问,请咨询叫练; //设计这个目的主要实现:不能同时多次点击复盘; private ConcurrentHashMap<String,Boolean> reviewThreadMap = new ConcurrentHashMap(); }
public void finishReplayer(Table table,User user) { inputStream = new FileInputStream(table.getFileName()); inputStreamReader = new InputStreamReader(inputStream); bufferedReader = new BufferedReader(inputStreamReader); while ((res = bufferedReader.readLine()) != null) { System.out.println(res); //谁点谁看 GameManager.sendTextSingle(table,res,user); try { //休眠一秒 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }
遇到问题
- springMVC jackson返回实体对象通过不能修改后不能正常转化为json传递到前端?
在实现上述功能过程中,说没遇到问题是不可能的,很多都是业务的bug,就不详细介绍了 ,该五子棋程序没有写数据库,都是在内存处理的,其中渲染首页桌子页面的数据都是从后台一个大的List<User>获取的,如果List数据在修改后,再刷新首页,会出现栈溢出,不能写json的情况,目前处理的方式是用了@JsonIgnore忽略对象返回到前端,把这个List的User对象重新复制了一份到一个新的List<User>,查看了源码,暂时还没找到原因!找到原因的童鞋欢迎下方留言,一起探讨下!感谢了
Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind
源码下载地址及注意事项
- JDK1.8及以上;有lamda表达式需要支持
- maven;需要下载websocket包
- 源码