无忧启动论坛

标题: js (浏览器版) 中国象棋 [打印本页]

作者: 不点    时间: 2018-9-30 18:08
标题: js (浏览器版) 中国象棋
从这儿 http://web.jobbole.com/90503/ 找到了这里的源码

https://github.com/xqbase/xqwlight

源码里面有很多版本,包括 JavaScript 版本——克隆到本机,只需要双击 index.htm 就可以运行。

JavaScript 很强大啊!

就不贴截图了。

如果你嫌麻烦,不想下载,你可以试试在线与电脑下象棋:

http://www.xiangqiqipu.com/Home/Robot

作者: holley2008    时间: 2018-9-30 18:38
多谢分享 我来学习学习
作者: pcfan120    时间: 2018-9-30 19:22
不错,谢谢分享
作者: 邪恶海盗    时间: 2018-9-30 21:50
技术太水,不试了...
作者: jssqysb    时间: 2018-10-1 08:15
在线游戏?
作者: 不点    时间: 2018-10-1 09:44
jssqysb 发表于 2018-10-1 08:15
在线游戏?

开源的软件,不会像个流氓一样,把用户绑定到网站上。

你下载源代码,无需网络就能运行。它是一个用 js 编写的本地应用程序。我认为,它是 js 的一个典范。

作者: 秋水长天369    时间: 2021-9-25 17:25

不错,谢谢分享
作者: xcajcj    时间: 2021-9-25 18:00
谢谢分享
作者: mooning    时间: 2021-10-21 14:40
那么小的体积,那么强的智能,厉害!谢谢分享!
作者: hbhsdmd    时间: 2021-11-25 08:48
JavaScript 版本的软件越来越少用了
作者: 不点    时间: 2022-8-29 00:01
今天又找到了一个 JS 版的中国象棋:

中国象棋 - in html5

https://github.com/itlwei/chess




作者: kw541    时间: 2022-8-29 16:59
感谢楼主无私的分享。
作者: wuy新菜鸟    时间: 2022-9-1 01:21
试试在线
作者: 不点    时间: 2022-9-1 10:32

今天学习一下
中国象棋 - in html5


中的代码,学习的重点是开局库的结构。有些单词看不懂,这会影响对代码的准确理解,需要查字典。

在字典中,pace 是 “一步,步子,步调,进度,节奏” 的意思。然而,它用在函数的参数中,猜测其含义应该是 “当前局面” 。

在字典中,man 有多种意思,其中有 “棋子、对手” 的意思。在代码中,它的含义好像是 “着法”。

在字典中,bill 有 “清单” 的意思。猜测含义是 “各种着法的列表”。

在字典中,gambit 有 “策略、开局、起始着法” 等意思。

原始代码的排版不够精细,我对其进行了排版优化。

发现了一个拼写错误:length (长度)被误写为 lenght。出错的代码为 AI.setHistoryTable.lenght。拼写错误是否意味着程序错误呢?那要看其他地方的代码是否都是一样的错误,如果都是同样的拼写错误,则不会导致程序错误。如果在全部源代码中,这是唯一出现的一个错误的拼写,那就可以确定这是一个程序错误了。

  1. //人工智能初始化
  2. AI.init = function (pace) {
  3.         var bill = AI.historyBill || com.gambit; //开局库
  4.         if (bill.length) {
  5.                 var len = pace.length;
  6.                 var arr = [];
  7.                 //先搜索棋谱
  8.                 for (var i=0; i < bill.length; i++) {
  9.                         if (bill[i].slice(0, len) == pace) {
  10.                                 arr.push(bill[i]);
  11.                         }
  12.                 }
  13.                 if (arr.length) {
  14.                         var inx = Math.floor ( Math.random() * arr.length );
  15.                         AI.historyBill = arr ;
  16.                         return arr[inx].slice (len, len + 4).split ("");
  17.                 } else {
  18.                         AI.historyBill = [] ;
  19.                 }
  20.         }
  21.          //如果棋谱里面没有,人工智能开始运作
  22.         var initTime = new Date().getTime();
  23.         AI.treeDepth = play.depth;
  24.         //AI.treeDepth = 4;
  25.         
  26.         AI.number = 0;
  27.         AI.setHistoryTable.lenght = 0

  28.         var val = AI.getAlphaBeta (-99999, 99999, AI.treeDepth, com.arr2Clone (play.map), play.my);
  29.         //var val = AI.iterativeSearch (com.arr2Clone(play.map), play.my)
  30.         if (! val || val.value == -8888) {
  31.                 AI.treeDepth = 2;
  32.                 val = AI.getAlphaBeta (-99999 ,99999, AI.treeDepth, com.arr2Clone (play.map), play.my);
  33.         }
  34.         //var val = AI.iterativeSearch (com.arr2Clone(play.map), play.my);
  35.         if (val && val.value != -8888) {
  36.                 var man = play.mans[val.key];
  37.                 var nowTime= new Date().getTime();
  38.                 console.log('最佳着法:' +
  39.                         com.createMove(com.arr2Clone(play.map), man.x, man.y, val.x, val.y) +
  40.                         ' 搜索深度:' + AI.treeDepth +
  41.                         ' 搜索分支:' + AI.number + '个  最佳着法评估:' + val.value + '分' +
  42.                         ' 搜索用时:'+ (nowTime - initTime) + '毫秒');
  43.                 return [man.x, man.y, val.x, val.y];
  44.         } else {
  45.                 return false;
  46.         }
  47. }


复制代码


作者: 不点    时间: 2022-9-1 13:48
上述 lenght 的拼写,在全部代码中,只出现了两处:

AI.setHistoryTable.lenght = 0;
AI.setHistoryTable.lenght ++;

而且, AI.setHistoryTable 是个函数。分两种情况讨论:

1、如果 lenght (拼写错误)是自定义的属性, 它只被赋值,没有被别处的代码引用,这肯定是多余的代码。

2、如果 length (拼写正确) 是函数的属性,它就是只读属性,不可以更改。既然代码更改了它,那也是错的。

综合以上两点,基本可以说,这确实是个错误。

猜测正确的应该分别是这样的:

AI.historyTable.length = 0;
AI.historyTable.length ++;

也就是说,不只是属性名称有拼写错误,就连对象的名称也弄错了!


作者: 不点    时间: 2022-9-1 17:23
length 是数组的一个属性,但不是 object 的一个属性。

由于 AI.historyTable = {} 是一个 object 而不是数组,所以,前面说的

AI.historyTable.length = 0;
AI.historyTable.length ++;

也是没有用的。上述两条语句,相当于为 object 添加了个 length 属性变量,没有实际意义。

若把第一条改为 AI.historyTable = {}; 则有意义,那就是,清空 object。

而第二条语句 AI.historyTable.length ++; 干脆就注释掉吧!

作者: 大宝剑    时间: 2022-9-1 17:36
关注下
作者: 不点    时间: 2022-9-2 11:40
本帖最后由 不点 于 2022-11-15 14:46 编辑

本楼提供修改版的 中国象棋 in html5 的下载


2022-09-02 解决了 lenght 拼写错误以及相关问题;纠正了 “车、马” 两个汉字的乱码;把 gambit.all.js 的内容移植到 common.js 中,解决了新版浏览器拒绝加载本地文件(浏览器给出 CORS 错误)的问题。


2022-10-01 修正了基础算法中的一些毛病,现在应该不会再出现明显的错误了。

2022-10-05 调整了兵、士、相的价值。删除了开局库,目的是暴露出算法的毛病。增加了调试代码,跟踪局面评估过程。

2022-10-06 由于无法解决局面评估函数暴露出来的问题,因此,代码不再更新了。此处保存目前最新的改动。接下来准备学习一楼提到的“象棋小巫师”。


Chess.zip

966.63 KB, 下载次数: 9, 下载积分: 无忧币 -2

中国象棋 in html5 修改版 2022-09-02

Chess-new.zip

766.37 KB, 下载次数: 6, 下载积分: 无忧币 -2

2022-10-06 最新改动保存于此,不再更新

xqwlight_js.zip

568.52 KB, 下载次数: 1, 下载积分: 无忧币 -2

2022-10-18 象棋小巫师修改版,棋盘是图片

xqwlight_js.zip

349.96 KB, 下载次数: 3, 下载积分: 无忧币 -2

2022-11-15 象棋小巫师修改,用Date.now计时


作者: 不点    时间: 2022-9-3 05:02
前述代码对开局库中的每一行进行了处理,最后返回其中一行中的 4 个字节作为结果:

return arr[inx].slice (len, len + 4).split ("");

这个结果,就代表一种着法。也就是说,每一行是双方着法的交替,其中每 4 个数字就代表一种着法。

另一方面,从人工智能运作代码的返回值:

return [man.x, man.y, val.x, val.y];

可以了解,这四个数字分别是某个位置的 x 坐标,y 坐标,以及另一个位置的 x坐标,y 坐标。

既然是一种着法,那就容易猜到,这是棋子的起始坐标和终止坐标。

好的,现在把开局库的第一行拿出来研究一下:


774770627967807089797276666512422625000119270171


按照每 4 字节一组,把它分解成着法序列。


如果左上角的坐标是 (0, 0),那么 (7, 7) 就是右侧红炮的位置,而 (4, 7) 就是当头炮的位置。因此,7747 就是 炮二平五。



7747 炮二平五

7062 马 8进 7 ——(7, 0)是黑方 8路的马,(6, 2) 是跳正马的位置

7967 马二进三 ——(7, 0)是红方二路的马,(6, 7) 是跳正马的位置
8070 车 9平 8 ——(8, 0)是黑方 9路的车,(7, 0) 是出直车的位置
8979 车一平二 ——(8, 9)是红方一路的车,(7, 9) 是出直车的位置
7276 炮 8进 4 ——(7, 2)是黑方 8路的炮,(7, 6) 是进炮封红车的位置
6665 兵三进一 ——(6, 6)是红方三路的兵,(6, 5) 是进兵走到河口相位的位置
1242 炮 2平 5 ——(1, 2)是黑方 2路的炮,(4, 2) 是黑方当头炮(中相)的位置
2625 兵七进一 ——(2, 6)是红方七路的兵,(2, 5) 是进兵走到河口相位的位置
0001 车 1进 1 ——(0, 0)是黑方 1路的车,(0, 1) 是出横车的位置
1927 马二进三 ——(1, 9)是红方八路的马,(2, 7) 是跳正马的位置
0171 车 1平 8 ——(0, 1)是黑方1路横车,(7, 1) 是横车平到 8 路的位置,形成霸王车


这样,开局库的结构也就完全清楚了。



作者: 不点    时间: 2022-9-3 12:09
本帖最后由 不点 于 2022-9-3 12:16 编辑

既然弄清楚了一个开局库的结构,增强了信心,就想进一步探索有关开局库的知识,于是搜到了这个网页:

https://www.xqbase.com/computer/eleeye_book.htm

文章不长,但介绍得很详细。这些知识正是我欠缺的,干脆就全部复制过来:



() 开局库

  在阅读本章前,建议读者先阅读象棋百科全书网中《对弈程序基本技术》专题的以下几篇译文:
  (1) 其他策略——开局库(Martin Fierz)

 7.1 象棋程序对开局库的处理   

  开局库是象棋程序中知识含量最多的部分,各种程序的棋力会因为开局库的不同而有所差距。互联网上介绍国际象棋开局库的文章很多,而中国象棋开局库的原理和国际象棋是完全一样的,因此笔者就不作太多的介绍了。

  很多国际象棋的程序中,开局库并不被引擎处理,而是由界面来完成的,棋局进入中局脱离棋谱后,才让引擎作搜索。ChessBase 的系列软件 Junior Fritz,以及支持 UCI 协议的 Shredder,都使用这种工作方式。由于它们使用同一套 ChessBase 的界面,因此开局库格式是统一的。由于 WinBoard 本身并不能处理开局库,因此支持 WinBoard 的引擎都具有处理开局库的能力,而且每个引擎都有各自的开局库格式。

  而中国象棋目前没有统一的界面,因此也没有统一的开局库格式,开局库一般由引擎来处理,各种引擎有各自定义的开局库格式。

  ElephantEye 早期开局库具有非常明显的特点——它是文本格式的,每一行记录一个着法,依次是着法 (红色部分)、权重 (绿色部分) 和局面 (紫色部分)

b2e2 5895  rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR  w - - 0 1 

  由于记录局面时使用 FEN 串,这就增加了开局库的可读性,但同时使开局库变得特别庞大,ElephantEye 的开局库 (BOOK.DAT) 仅有 20,000 多个着法却达到了1.6M

  从 ElephantEye 2.0 以后,每个局面仅占用 8 字节 (4 字节的 Zobrist 键值、2 字节的着法和 2 字节的权重),而且对称局面作了合并,因此由 10,000 多个着法组成的开局库大小不超过 100K

 7.2 开局库的制作 

  ElephantEye 现有的开局库是从象棋百科全书网上收录的 8000 多局对局中整理出来的,这些对局涵盖了 1990 年到 2004 年的全国顶级象棋比赛 (团体赛、个人赛、甲级联赛和五羊杯),因此具有代表性。现在简要介绍一下开局库的制作流程:

  (1) 把所有的对局中的每个着法解析出来,记录到临时文件中,临时文件的每个着法记录占 16 字节 (8 字节的 Zobrist 键值、4 字节的着法和 2 字节的权重)


  (2) 对临时文件的着法记录按 Zobrist 键值 (第一关键字) 和着法 (第二关键字) 排序;


  (3) Zobrist 键值和着法作关键字,对权重进行叠加;


  (4) 按照笔者观点,权重 = 胜局数×3 + 和棋局数 - 负局数,因此在第 (1) 步中,导致胜局的着法的权重置 3,和局置 1,负局置 -1


  (5) 过滤掉权重小于 4 的着法 (即收入开局库的着法至少是一胜一和的),把 Zobrist 键值的后 4 字节、2 字节表示的着法和权重除以 4 16 位整数值记录到开局库文件 (BOOK.DAT) 中。

 7.3 开局着法的选择

  象棋程序在处理一个局面时,首先要从开局库中寻找是否有相同的局面,当开局库中找不到局面时,才会调用搜索程序进行思考。ElephantEye 从开局库中找到着法的过程如下:

  (1) 由于开局库是按 Zobrist 键值排序的,因此用二分查找法可以很快找到所有符合的局面及其着法;

  (2) 如果当前局面在开局库中没有,那么查找对称局面,找到的着法也要作对称处理;

  (3) 由于开局库中的 Zobrist 键值只有 4 字节,因此有可能产生冲突,必须从着法列表中排除不合理着法;

  (4) 用随机数根据着法的权重选择着法;

  (5) 如果该着法没有构成重复局面,那么将该着法作为最佳着法直接返回。




作者: 不点    时间: 2022-9-4 13:02
本帖最后由 不点 于 2022-9-4 18:41 编辑

今天思考一下局面应该如何表示的问题

如果用 hash 值来代表局面,它的好处是占用空间小,搜索速度快。但它具有碰撞的可能性。

考虑用普通字节串来表示局面。这个局面的表示,也仅仅用在库中,不影响 AI 子程序的人工智能计算过程。

我们来表示局面的时候,也要尽量压缩空间的占用。以下就给出一些思考。

第一部分:帅和相,其位置组合,共有 254 种,用一个字节(8位)即可表示。详述如下:

情况(1):帅占据中相的位置,相就不能占据这个位置了。那么,相能够占据的,也只有其他 6 个位置了。细分为以下 3 种情况:

1、没有相(共1种情况)
2、只有一个相(共有 6 种可能的情况)
3、双相都在,共有 6×5/2 = 15 种情况。

综合这 3 种,共有 1 + 6 + 15 = 22 种可能性。

情况(2):帅不在中相的位置,那么帅在九宫格中共有其它 8 种可能的位置。每一种可能性,都对应着相在 7 个位置的不同分布情况。相的各种可能的分布情况如下:

1、没有相(共1种情况)
2、只有一个相(共有7种可能性)
3、双相都在,共有 7×6/2 = 21 种情况。

综合这 3 种,共有 1 + 7 + 21 = 29 种可能性。而帅有 8 种变化,因此,总的可能性是 8×29=232 种。

综合两种情况,帅和相的不同组合,共有 232 + 22 = 254 种。这样,我们就能够用 8 位的字节来表示它了。


第二部分:士的位置组合,共有 16 种,刚好能用 4 位(也就是半个字节)来表示。详述如下:


1、没有士(共1种情况)
2、只有一个士(共有 5 种可能的位置变化)
3、双士都在,共有 5×4/2 = 10 种可能的组合。


综合这三种,共有 1 + 5 + 10 = 16 种可能的位置变化。


第三部分:马的位置变化,共有 4096 种,刚好能用 12 位(也就是 1.5 个字节)来表示。详述如下:


1、没有马(共1种情况)
2、只有一个马(共有 90 种可能的位置变化)
3、双马都在,共有 90×89/2 = 4005 种可能的组合。


综合这三种,共有 1 + 90 + 4005 = 4096 种可能的位置变化。

第四部分:车的位置变化,与马的情况相同,共有 4096 种,不再赘述。


第五部分:炮的位置变化,与车、马的情况相同,共有 4096 种,不再赘述。


第六部分:兵的位置变化。共有 5 个兵,每个兵可能占据的点位不超过 63 个(其实只有 55 个,未过河的兵的位置只有 10 个),用 6 位可以表示。那么,5 个兵总共需要占据 30 位的空间(差不多要占据 4 个字节了)。


综合以上分析,要表示红方的棋子位置,需要的二进制位数是



8(帅、相)+ 4(士)+ 12(马)+ 12(车)+ 12(炮)+ 30(兵)= 78 位


也就是近乎 10 个字节。


同理,黑方也需要近乎 10 个字节。


因此,一个局面的表示,需要近乎 20 个字节的空间。



较新的浏览器支持 JavaScript 新增的 bigint 数据类型。用 JavaScript 的大整数来处理棋局的局面,应该也很方便。对于库中保存的局面(和着法),我们主要使用的是排序(比较大小)之类的操作,因此,初步感觉 bigint 是可以使用的。当然,也可以考虑使用 ArrayBuffer。






作者: a349683510    时间: 2022-9-4 13:26
还能这么晚。。。
作者: 不点    时间: 2022-9-4 22:17
本帖最后由 不点 于 2022-9-4 22:24 编辑

前面说了,局面可用 78(红方) + 78(黑方) = 156 位 的整数来表示。

下面说说具体可以采用的什么样的方案。

帅、相占用最低 8 位,接着是黑方将、象占用的 8 位。(双方共占用 2 字节)

然后是红方士占用的 4 位,接着是黑方士占用的 4 位。(双方共占用 1 字节)

再后是红方马 12 位,黑方马 12 位。(双方共占用 3 字节)
以及红方车 12 位,黑方车 12 位。(双方共占用 3 字节)
以及红方炮 12 位,黑方炮 12 位。(双方共占用 3 字节)

最后一轮是红方兵 30 位,黑方卒 30 位。(双方共占用 7.5 字节)

这就到达最高的位了。

棋盘左右翻转以后,计算得到的整数数值可能不一样。只保留数值较小的那个局面即可。






帅、相的位置与相应的一字节(8 位)整数数值对应表的设计


数值 0~21 对应于帅占据中相位的可能性:


0——帅占中相位,双相都不存在
1——帅占中相位,只有单相,位于红方一路边相位
2——帅占中相位,只有单相,位于红方三路底相位
3——帅占中相位,只有单相,位于红方三路河口相位
4——帅占中相位,只有单相,位于红方七路底相位
5——帅占中相位,只有单相,位于红方七路河口相位
6——帅占中相位,只有单相,位于红方九路边相位
7——帅占中相位,有双相,有一相位于一路,另一相位于三路底部
8——帅占中相位,有双相,有一相位于一路,另一相位于三路河口

9——帅占中相位,有双相,有一相位于一路,另一相位于七路底部
10——帅占中相位,有双相,有一相位于一路,另一相位于七路河口

11——帅占中相位,有双相,有一相位于一路,另一相位于九路边线
12——帅占中相位,有双相,有一相位于三路底部,另一相位于三路河口
13——帅占中相位,有双相,有一相位于三路底部,另一相位于七路底部
14——帅占中相位,有双相,有一相位于三路底部,另一相位于七路河口

15——帅占中相位,有双相,有一相位于三路底部,另一相位于九路边线

16——帅占中相位,有双相,有一相位于三路河口,另一相位于七路底部

17——帅占中相位,有双相,有一相位于三路河口,另一相位于七路河口

18——帅占中相位,有双相,有一相位于三路河口,另一相位于九路边线

19——帅占中相位,有双相,有一相位于七路底部,另一相位于七路河口
20——帅占中相位,有双相,有一相位于七路底部,另一相位于九路边线
21——帅占中相位,有双相,有一相位于七路河口,另一相位于九路边线

数值 22~50 对应于帅在四路底线的 29 种可能性:

22——帅在四路底线,双相都不存在
23——帅在四路底线,只有单相,位于红方一路边相位

24——帅在四路底线,只有单相,位于红方三路底相位
25——帅在四路底线,只有单相,位于红方三路河口相位
26——帅在四路底线,只有单相,位于红方中相位
27——帅在四路底线,只有单相,位于红方七路底相位
28——帅在四路底线,只有单相,位于红方七路河口相位
29——帅在四路底线,只有单相,位于红方九路边相位
30——帅在四路底线,有双相,有一相位于一路,另一相位于三路底部
31——帅在四路底线,有双相,有一相位于一路,另一相位于三路河口

32——帅在四路底线,有双相,有一相位于一路,另一相位于中相位
33——帅在四路底线,有双相,有一相位于一路,另一相位于七路底部
34——帅在四路底线,有双相,有一相位于一路,另一相位于七路河口
35——帅在四路底线,有双相,有一相位于一路,另一相位于九路边线
36——帅在四路底线,有双相,有一相位于三路底部,另一相位于三路河口
37——帅在四路底线,有双相,有一相位于三路底部,另一相位于中相位
38——帅在四路底线,有双相,有一相位于三路底部,另一相位于七路底部
39——帅在四路底线,有双相,有一相位于三路底部,另一相位于七路河口
40——帅在四路底线,有双相,有一相位于三路底部,另一相位于九路边线

41——帅在四路底线,有双相,有一相位于三路河口,另一相位于中相位
42——帅在四路底线,有双相,有一相位于三路河口,另一相位于七路底部
43——帅在四路底线,有双相,有一相位于三路河口,另一相位于七路河口
44——帅在四路底线,有双相,有一相位于三路河口,另一相位于九路边线

45——帅在四路底线,有双相,有一相位于中相位,另一相位于七路底部
46——帅在四路底线,有双相,有一相位于中相位,另一相位于七路河口
47——帅在四路底线,有双相,有一相位于中相位,另一相位于九路边线
48——帅在四路底线,有双相,有一相位于七路底部,另一相位于七路河口
49——帅在四路底线,有双相,有一相位于七路底部,另一相位于九路边线
50——帅在四路底线,有双相,有一相位于七路河口,另一相位于九路边线

依次类推:
数值 51~79 对应于帅在四路象眼的 29 种可能性。
数值 80~108 对应于帅在四路士角的 29 种可能性。
数值 109~137 对应于帅在中路底部的 29 种可能性。
数值 138~166 对应于帅在九宫花心的 29 种可能性。
数值 167~195 对应于帅在六路底部的 29 种可能性。

数值 196~224 对应于帅在六路象眼的 29 种可能性。

数值 225~253 对应于帅在六路士角的 29 种可能性。


就是说,通过查表,就可以建立帅、相位置与字节数值的对应关系。同样,士的 16 种位置分布也可以用查表的方式来与序号建立对应关系。兵的处理是每个兵对应一个独立的 6 位数值,能用数学公式来简单地表示。最后需要动用技巧来处理的,是马的位置与序号的对应关系(车、炮的处理与马完全相同)。



作者: 201027149    时间: 2022-9-4 22:53
感谢楼主分享、更新!!
作者: 不点    时间: 2022-9-5 07:08
本帖最后由 不点 于 2022-9-5 07:11 编辑

刚才的思路,从基本点上,是可行的。但从技术细节上,仍需要优化、调整。

先说说从基本点上,为什么帅、相要搅和在一起?这不是增添麻烦吗?

是的,麻烦是有点麻烦,但能获得节约位数占用的好处。节约的位数只有 1 位,貌似用处不大,而带来的麻烦却不少。然而,假如我们通过技术优化,能够把麻烦降低到可以接受的程度,那节约空间的优点就感觉很便宜、很值了。

如果把帅、相独立处理,帅在九宫中有 9 种位置变化,需要 4 位二进制数,因为 3 位二进制数只能表示 8 种状态,刚好不能表示帅的 9 种状态。另一方面,一个相有 7 种位置变化,需要 3 位二进制数。但有两个相,因此,另一个相也需要有 3 位二进制数。这么说来,如果帅、双相全都独立处理,需要的二进制位数是 4 + 3 + 3 = 10 位。如果双相搅和在一起(不独立),(通过前文的分析计算)双相的状态变化共 29 种,可以用 5 位二进制数来表示。这样的话,帅、相共需 4 + 5 = 9 位。如果 9 位、10 位都愿意接受的话,那就没必要优化成 8 位了。但如果优化成 8 位以后,带来的麻烦不大,那还是可以考虑优化的。

下面就接着探讨对前述方案的调整。

调整的思路是把帅不在中相位的 232 种,放在低位,便于计算。另一个思路是对 7 个相位进行排序优化,中相的占位由于与帅容易发生冲突,就把中相的编号排在末尾。其他相的位置,先后顺序不重要。比如,就这么安排:

0号相位——一路边相
1号相位——三路底相
2号相位——三路河口相
3号相位——七路底相
4号相位——七路河口相
5号相位——九路边相
6号相位——中相(排在末尾)

同理,对帅的 9 个位置也进行排序,中相位的帅,排在末尾:
0号帅位——四路底线(九宫右下角)
1号帅位——四路象眼(九宫右边线中点)
2号帅位——四路士角(九宫右上角)
3号帅位——中路底线(帅的原位)
4号帅位——九宫花心
5号帅位——六路底线(九宫左下角)
6号帅位——六路象眼(九宫左边线中点)
7号帅位——六路士角(九宫左上角)
8号帅位——中路宫顶(中相的位置)(排在末尾)

首先处理 232 个位置(编号 0~231),此时,帅不占据中相位,共有 8 种变化。因此,帅可以单独占据 3 位,比如就取最低 3 位即可。高 5 位可以表示 32 种不同状态,但我们只需要相的 29 种位置变化(见前文)。这 29 种变化也可以安排成有规律的。如果安排成无规律的,那也可以通过建立一个数组来与编号发生对应,工作量不大。由于只有 29 种不同相位的变化,所以,帅、相总共只需要 8 × 29 = 232 个状态,最小编号是 0,最大编号是 231。剩下的,超过 231(但不超过 255) 的编号,还有 24 个,足够我们用于表示剩余的 22 种 “帅占据中相位” 的状态变化。计算时,先判断编号是否大于 231。如果大于 231,那么,帅就在中相位,双相的状态也能够通过编号来确定。如果编号小于或等于 231,帅的位置可以通过编号的最低 3 位来确定,双相的状态通过编号的高 5 位来确定。

作者: 不点    时间: 2022-9-5 11:57
本帖最后由 不点 于 2022-9-5 12:07 编辑

在 PHP 中文网,有一篇文章很不错,下面是标题和链接:

h5+js实现本地文件读取和写入
https://www.php.cn/div-tutorial-389900.html

为什么需要研究读写本地文件的方法呢?

因为 中国象棋 - in html5 就有读取本地文件的代码,但却被所有新版的主流浏览器封杀了(CORS 错误,不允许 js 代码读写本地文件)。我是把访问本地开局库数据文件的代码修改成把开局库数据直接嵌入 js 代码里面,才绕过了这个问题。然而,本地文件确实需要读取和写入,所以,就需要研究有没有其它可行的办法。原文的代码是以图片形式提供的,而跨站访问图片有可能被封杀(即便现在允许,将来某一天也可能不再允许了),因此文章内容就不贴了。【更正一下】原文中的代码不是图片,但原文的代码是彩色高亮显示,很美观。我以为是截图呢!本站还没有支持彩色代码,转贴过来就不如原文好看了,所以,还是算了,不转贴了。



作者: 不点    时间: 2022-9-5 20:26
本帖最后由 不点 于 2022-9-6 08:52 编辑

在研究马的位置表示法之前,再来看看兵的位置。前面让 5 个兵独立占据空间,每个兵的位置需要 6 位二进制数来表示,5 个兵就需要 30 位了。

如果把 5 个兵搅合在一起,应该会节约一些空间。


兵有 55 种可能的位置。过河兵的 45 个位置,再加上未过河的 10 个位置,就是 55 个位置。那么,最多有


1 + 55 + 55*54/2 + 55*54*53/(3*2) + 55*54*53*52/(4*3*2) + 55*54*53*52*51/(5*4*3*2)
= 1 + 55 + 1485 + 26235 + 341055 + 3478761
= 3847592


种可能性。


我们再看 2^22 = 4194304 比上述结果 3847592 大,因此,用 22 位二进制数,就可以表示 5 个兵的不同位置变化。也就是说,与前面采用的 30 位相比,我们可以节约出 8 位的空间了。这已经是最大限度的节约了,不可能再用更少的位数了。


真的不能再节约了吗?如果不死心,还可以再挖空心思找一找,看看有没有可能。


前述 55 个位置当中,有些组合是不可能出现的。这些情况可以排除掉。


有哪些情况是不可能发生的呢?两个未过河的兵,发生前后叠兵,是不可能的。


这种叠兵,有多少种情况呢?


大的方面,有 “一对叠兵” 和 “两对叠兵”,这两种情况。不可能有三对叠兵,因为总共只有 5 个兵。


首先,容易计算出 “两对叠兵” 出现的可能性。未过河的兵,总共只有 5 列,这 5 列当中,有两列都是叠兵。这种可能的组合,有


5*4/2 = 10 种。由于两对叠兵已经消耗掉了 4 个兵,剩下只有一个兵在棋盘上摆放了,当然,第 5 个兵不存在的情况,也是一种情况,也要计算在内。总共 55 个位置,而两对叠兵已经占用了 4 个位置。剩下的,只有 51 个位置,供第 5 个兵占用。因此,只有 51 种情况。再加上第 5 个兵不存在的情况,那就是 52 种情况。


10*52=520,这就是全部可能的 “发生两对叠兵” 的情况了。太少了!从庞大的 3847592 中扣除 520,就跟掉了一根头发一样,影响不了啥。


一对叠兵出现的可能性会不会多一些呢?肯定会多一些,但也不会太多。


一对叠兵出现在未过河的 5 列当中,出现的可能性有 5 种。剩下的三个兵(其中任意一个兵都可以是不存在的),在 53 种位置上摆放,精确地,有这么多的可能:


1 + 53 + 53*52/2 + 53*52*51/(3*2)
= 1 + 53 + 1378 + 23426
=24858


5*24858=124290


这个数,已经包括两对叠兵的情况了,甚至还可能重复计算了一些两对叠兵的情况,也就是说,只会算多了,不可能算少了。


也就是说,撑死了,也就 124290 种情况,从 3847592 种扣除 124290 得到 3723302,仍然是需要 22 位二进制的数才能表示这么多的不同位置组合。


原先我们粗估需要 20 个字节(160位二进制数)来表示黑白双方棋子的不同位置变化。现在,在对兵进行优化、压缩之后,又能节约 2 个字节了,也就是说,用 18 个字节就可以表示一个棋局的局面了。精确地说,用 140 位的二进制数(即 17.5 个字节),就能表示一个棋局的局面了。




作者: 不点    时间: 2022-9-5 23:01
前帖并未精确计算出叠兵的可能性有多少种。虽然其准确数值对于我们没有用,但是,作为一个数学问题,还是有点用的吧。本帖就试试看,能否计算出准确值。

假定红方的一路(即最右边的边线)出现了叠兵(未过河)。前帖已经计算出,这种情况共有 24858 种,其中也包括了在三路、五路、七路、九路出现叠兵的那些可能性。是的,我们现在就是要把那些情况排除掉,目的是只让一路有(未过河的)叠兵,其他几路没有(未过河的)叠兵。

好的,一路已经有叠兵了,假定三路也是叠兵,则只剩下一个兵,在 51 个位置活动。当然,还有一种情况,只有 4 个兵,第 5 个兵不存在。因此,有 52 种可能的变化。同理,如果一路和五路都有叠兵,这也有 52 种情况。同理,如果一路和七路有叠兵,这也有 52 种情况。同理,如果一路和九路都有叠兵,这也有 52 种情况。

因此,从 24858 当中,去掉这 4 个 52,就是只有一路有叠兵的情况了:

24858 − 4×52 = 24650

这个数,就是精确的 “只有一路有叠兵” 的情况总数。

同理,“只有三路有叠兵”的情况总数,也是精确地等于 24650。
同理,“只有五路有叠兵”的情况总数,也是精确地等于 24650。
同理,“只有七路有叠兵”的情况总数,也是精确地等于 24650。
同理,“只有九路有叠兵”的情况总数,也是精确地等于 24650。

所以,“只出现一个叠兵”的情况总数,精确地等于

5×24650 = 123250

再加上同时出现两对叠兵的情况 520:

123250 + 520 = 123770

这就是未过河兵“至少出现一对叠兵”(意思是也包括“同时出现两对叠兵”的可能性)的全部情况了(精确值)。这没有超过前一帖估算出的 124290 这个数,因此,这也反过来印证了,上述的计算,是比较可靠的,没有出现矛盾的结果。

作者: 不点    时间: 2022-9-5 23:46
前面我们研究出,在最节约的情况下,可以用 140 位二进制数,来表示棋局的局面。那么,也许有些情况不需要节约呢?所以,本帖就研究一下,红黑双方,如果全部 32 个棋子都独立占据二进制位,那需要多少位呢?

先说红方吧。

帅有 9 个可变的位置,需要 4 位二进制数才能表示。士有 5 个可能的位置,需要 3 位二进制数。另一个士也需要 3 位。相有 7 个可能的位置,需要 3 位二进制数。另一个相也需要 3 位。马有 90 个可能的位置变化,需要 7 位二进制数。另一个马也需要 7 位。同理,双车需要 14 位二进制数,双炮也需要 14 位二进制数。兵有 55 个位置可以放置,需要 6 位二进制数。5 个兵就需要 30 位二进制数。这样,红方总共需要

4(帅) + 6(士)+ 6(相)+ 14(马)+ 14(车)+ 14(炮)+ 30(兵)
= 88 位(二进制数)= 11 (个字节)

红黑双方总共就需要 22 个字节了。

这仍然算是比较节约的方案了。如果要更浪费一点,横纵坐标独立处理。每个棋子,横坐标有 9 个变化,占据 4 位二进制数。纵坐标有 10 个可能的变化,也占据 4 位二进制位。这样每个棋子就需要占据 8 位二进制数了,也就是占用一个字节。红黑双方总共 32 个棋子,也就需要 32 个字节了。最浪费的情况,也不过如此而已。回想一下,我们最节约的情况,是采用 17.5 个字节。

所以,如果觉得 32 个字节是可以接受的,那一切都很轻松,不需要绞尽脑汁去做那些费劲的事情了。

作者: 不点    时间: 2022-9-6 07:22
局面的表示,最优可以压缩到 17.5 字节(140位二进制数),最差也不过只需 32 字节(256位二进制数)。如果能压缩到 16 字节(128位二进制数),那就太棒了,但这是做不到的。既然做不到,那么,从电脑CPU硬件的角度来考虑,17.5 字节与 32 字节,差别不大,只不过 17.5 字节能够节约一些内存空间的占用罢了。究竟要选择哪一种表示方法,还要看具体情况。我们此时就可以与 hash 表示法做个对比。hash 表示法,如果采用 4 字节(32位)整数,其空间占用与我们的 17.5 字节也只有 4 倍多的优势。如果 hash 值采用 8 字节(64位)整数,那与 17.5 字节的差别已经不大了,只有 2 倍多的差别了。所以我感觉,采用 hash 方法不太好。hash 方法有碰撞的可能,所以,它有着潜在的问题。64位的 hash,貌似很难产生碰撞。但是,如果库中的局面不断增多,则碰撞的概率是增加的。当然,在碰撞发生的时候,可以再挑选一个不同的 hash 算法,来保证现有库不发生碰撞。那么,hash 算法就成了必须研究的一门学问了。随着库容量的增大,可能出现这样的情况:更换多个 hash 算法,也无法避免碰撞。而发生碰撞的坏处是明显的:AI 会按照库中的着法,下出一步错棋!这 “有库” 还不如 “没库” 好呢!你的 AI 会下出错棋,对手的 AI 不出错,那对手就更有把握赢棋。
作者: 不点    时间: 2022-9-6 11:25
本帖最后由 不点 于 2022-9-8 06:56 编辑

接着探讨对相的不同位置组合进行排序的方案。

先看帅在中相位的情况。此时,相只有 6 种位置可以放置(因为中相位已经被帅占据):

0号相位——一路边相
1号相位——三路底相
2号相位——三路河口相
3号相位——七路底相
4号相位——七路河口相
5号相位——九路边相

双相位置序号0:双相都不存在。
双相位置序号1:只存在一个相,位于0号相位。
双相位置序号2:只存在一个相,位于1号相位。
双相位置序号3:只存在一个相,位于2号相位。
双相位置序号4:只存在一个相,位于3号相位。
双相位置序号5:只存在一个相,位于4号相位。
双相位置序号6:只存在一个相,位于5号相位。
双相位置序号07:双相都在,一相位于0号相位,另一相位于1号相位。
双相位置序号08:双相都在,一相位于0号相位,另一相位于2号相位。
双相位置序号09:双相都在,一相位于0号相位,另一相位于3号相位。
双相位置序号10:双相都在,一相位于0号相位,另一相位于4号相位。
双相位置序号11:双相都在,一相位于0号相位,另一相位于5号相位。
双相位置序号12:双相都在,一相位于1号相位,另一相位于2号相位。
双相位置序号13:双相都在,一相位于1号相位,另一相位于3号相位。
双相位置序号14:双相都在,一相位于1号相位,另一相位于4号相位。
双相位置序号15:双相都在,一相位于1号相位,另一相位于5号相位。
双相位置序号16:双相都在,一相位于2号相位,另一相位于3号相位。
双相位置序号17:双相都在,一相位于2号相位,另一相位于4号相位。
双相位置序号18:双相都在,一相位于2号相位,另一相位于5号相位。
双相位置序号19:双相都在,一相位于3号相位,另一相位于4号相位。
双相位置序号20:双相都在,一相位于3号相位,另一相位于5号相位。
双相位置序号21:双相都在,一相位于4号相位,另一相位于5号相位。

有没有什么公式来把序号与相位进行对应?难点在于双相的情况,下面只讨论双相的情况。

上述的对应列表,有关双相的部分,可以排列成倒金字塔形状:

07(0,1) 08(0,2) 09(0,3) 10(0,4) 11(0,5)

   12(1,2) 13(1,3) 14(1,4) 15(1,5)

      16(2,3) 17(2,4) 18(2,5)

         19(3,4) 20(3,5)

            21(4,5)

这个堆积木(倒三角)的形状,有如下一些规律:
1、每一行,小相的序号是相同的,大相的序号是递增的。
2、每一行,第一个项目的小相与大相的序号相差1,只要能确定第一项的小相序号,此行的其他情况也都确定了。
3、各行小相的序号是递增的。

现在我们的目的是确定,括号外的总序号与括号内的双相位置序号有什么能够互相推导的关系。

先来看看如下堆积木问题:

    1
   2 3
  4 5 6
 7 8 9 10

它是连续正整数排成积木形状,问正整数 m 排在第几行?就是求相应的行号 n。

注意到,如果 m 碰巧是三角形右下角的数(它是积木的总数),那么,


(1 + n) * n / 2 = m

已知 m 可以求出 n。

两边乘以 2,得:n² + n = 2 * m

配方,得 n² + n + 1/4 = 2 * m + 1/4

(n + 0.5)² = 2 * m + 0.25


n = (2 * m + 0.25)𑁦 − 0.5


【此处 0.5 次方代表 “开平方”;也可以用 sqrt 函数来表示】

如果 m 比右下角的数小(但比上一行的数都大),那么,计算出来的 n 值是带有小数部分的,不是一个整数。如果把结果向上补足到整数(就像 ceil 函数所做的那样),那就得到正确的行号了。

所以,无论 m 是否位于右下角,它的行号 n 是可以用公式计算出来的:

n = Math.ceil(Math.sqrt(2 * m + 0.25) − 0.5);

行号知道了,但一行中有很多数,究竟 m 在哪个位置呢?

这容易知道。因为此行中最右侧的数是 (行号)*(行号 + 1)/ 2,它比 m 大(至少等于 m),

所以,m 距离此行最右侧的距离就是 (行号)*(行号 + 1)/ 2 − m。

这就是说,已知 m,可以完全确定它在哪一行的哪个具体位置了。

反过来,如果知道某个数 m 位于第 n 行的最右侧,n 是已知的,m 是未知的,如何求出 m 呢?

很容易。因为前面说了,m = n * (n + 1) / 2,特别简单。

如果 m 不在一行的最右侧呢?那得告诉它距离最右侧有多远。假定距离是 d,那么

m = n * (n + 1) / 2 − d;

也很简单。

这就是说,如果知道了某个数(未知的,m)在某行(已知的行号)的某个位置(已知的位置),也就能够算出这个数的值了。

好的,这个数学问题解决了之后,我们前面的序号与相的位置的对应关系问题,就容易解决了。


作者: 879792799    时间: 2022-9-6 11:36
我以为我下不过程序  原来我连网页都打不过
作者: 不点    时间: 2022-9-7 07:23
前面我们在排序的时候,排成了倒三角形。现在试试重新排列成正三角形。



        07(0,1)

      08(0,2) 09(1,2)

    10(0,3) 11(1,3) 12(2,3)


  13(0,4) 14(1,4) 15(2,4) 16(3,4)

17(0,5) 18(1,5) 19(2,5) 20(3,5) 21(4,5)



把括号外的数字也改小一点,便于我们观察:


        01(0,1)

      02(0,2) 03(1,2)

    04(0,3) 05(1,3) 06(2,3)


  07(0,4) 08(1,4) 09(2,4) 10(3,4)

11(0,5) 12(1,5) 13(2,5) 14(3,5) 15(4,5)



看看!规律是不是出来啦!


单看括号外的数字,那就是我们前面堆积木的格式。
括号内的,右侧那个数,就是行号。
而括号内左侧那个数呢,在一行中是递增的,它代表的是在本行的序号。


前面我们已经研究过了,只要知道括号外的数,就能算出它所处的行号以及它在行内的序号。反过来也行:只要知道行号和行内的位置(就是括号内的两个数),也能算出它的序号(就是括号外的那个数)。


这样,我们的问题就彻底解决了。



解决了双相的位置表示问题,也就顺便解决了双马、双车、双炮的位置表示问题。其实,双相的位置变化很少,每个相最多只有 7 种不同的位置。即便我们找不出公式,也能用穷举(列表)的方式,把位置与序号的对应关系建立起来。然而,车、马、炮的情况就不同了。每个车、马、炮都有 90 个可能的位置,双车、双马、双炮的组合,各自都超过 4000,合起来要超过 12000,这么多的数据,用列表来穷举,工作量太大,难以承受。


前面说了,双马如果不揉合在一块儿,而是各自独立处理,每个马有 90 个位置,需要 7 位二进制数来表示。这样双马就需要 14 位。而搅合在一块儿,只需 12 位即可表示双马的全部 4096 种位置组合。也就是说,我们节约了两个二进制位。


有时候,从另外的视角来看的话,也许有人觉得不值得如此抠门。费这么大劲,只是节约了 2 个二进制位。即便是浪费了一点,如果能够带来别的好处,那不也是很值吗?当然是这样的。具体该怎么做,那是一个权衡。觉得怎么合适,就应该怎么去做。


但是,我们现在只是研究嘛。我们研究的是技术的极限情况。究竟能否达到这种极限?我们研究的是实在可行性,能够具体实施,而不是只有一个空洞的、笼统的理论框架。万一有人需要这种技术呢?如果我们的结论是能做到,但比较辛苦。那人家不怕辛苦,就愿意这么干呢?那么,这个研究,对他来说,就是有用的。我不知道有没有人能够跟随我的帖子,一口气看到这里。如果有的话,那很有可能是同意我的观点和做法的,至少是部分同意的。那么,我也就是在和您一起研究和探讨了,我也就不是孤立的了。



前面说的东西,其实都不难,实施起来,也没有实质障碍。接下来要说的,恐怕还真是有些难度了。因为,5 个兵的情况,发生了跳跃性的变化,如果要揉合在一起来处理的话,比 2 个相、2 个马、2 个车、2 个炮的情况复杂了很多。光是数学,都够吓人的。我们前面解方程的时候,解的是一元二次方程,有成熟的求根方法和求根公式。想象一下,假如我们在处理兵的时候,碰上 3 次、4 次、5 次方程,那还能解吗?当然了,如果我们畏惧的话,总是可以退缩的:我们把每个兵进行独立处理,每个兵占用 6 位二进制数,5 个兵就占用 30 位。前面我们已经知道,如果 5 个兵揉合在一起,用排列组合的方法,知道它的变化可以用 22 位二进制数来表示。这样,一下子就节约了 8 个位。前面在处理相、车、马、炮的时候,一两个位,我们都很抠门,现在 8 个位,如果我们放过了、无视了、认栽了,应该也会 “心有不甘” 吧?




作者: 不点    时间: 2022-9-8 09:15
本帖最后由 不点 于 2022-9-8 09:22 编辑

前面我们知道,在对兵的处理 “不节约” 的情况下,整个局面的表示,需要 156 位二进制数,或者说,需要 20 个字节(精确地说是 19.5 个字节)。而在对兵的表示法进行极限优化的情况下,要表示一个局面,只需 17.5 个字节(即 140 位二进制数)。

现在,无论是花费 20 个字节,还是 18 个字节,我们大致知道了一个局面需要的空间的大小。库中不止有局面,还得有着法。对于给定的一个局面,要给出各种最好的着法,这就是库的用处。最好的着法,可能有很多,而不是只有 1 个。所以,库应该给出那些最好的着法。

一个局面,最多能有多少种着法呢?当然了,一个局面,都是轮到 “我” 走棋了,才需要知道着法是啥。所以,库都是假定,自己就是该走棋的一方。库要给出一个局面的各种最优的走法,供 “我” 选择。这个 “我”,肯定是电脑,因为人是无法利用库的。人只能用自己的大脑。此处所说的库,不是像 中国象棋 in html5 中采用的那样,仅仅用于 “开局”,而是指一般性的 “库”,没有 “开局”、“终局”、“残局” 之分。这个库,用20 个字节或 18 个字节来表示局面,然后,紧跟着就是配套的着法了。

“我” 方要走棋了,那肯定只能移动我方的棋子。对方的棋子可以被吃,但我方无权移动对方的棋子。

所以,走一着棋,有多少种可能性呢?那就只需要考虑 “我” 方的棋子了。用穷举法列出来看看吧。

先说说如何区分两个相、两个马、两个车、两个炮、5 个兵。按照它们在当前棋盘上的位置,总能分出先后。一路的棋子在最先,其次是二路、三路,……,九路的棋子在最后。同一路的棋子,靠近己方底线的在先。

0、保留,暂不使用


1、兵——向右移动一格
2、兵——向上移动一格
3、兵——向左移动一格
4、第二兵——向右移动一格
5、第二兵——向上移动一格
6、第二兵——向左移动一格

7、第三兵——向右移动一格
8、第三兵——向上移动一格
9、第三兵——向左移动一格
10、第四兵——向右移动一格
11、第四兵——向上移动一格
12、第四兵——向左移动一格
13、第五兵——向右移动一格
14、第五兵——向上移动一格
15、第五兵——向左移动一格
16、马——先向右走两格,再向下走一格
17、马——先向右走两格,再向上走一格
18、马——先向下走两格,再向右走一格
19、马——先向上走两格,再向右走一格
20、马——先向下走两格,再向左走一格
21、马——先向上走两格,再向左走一格
22、马——先向左走两格,再向下走一格
23、马——先向左走两格,再向上走一格
24、第二马——先向右走两格,再向下走一格
25、第二马——先向右走两格,再向上走一格
26、第二马——先向下走两格,再向右走一格
27、第二马——先向上走两格,再向右走一格
28、第二马——先向下走两格,再向左走一格
29、第二马——先向上走两格,再向左走一格
30、第二马——先向左走两格,再向下走一格
31、第二马——先向左走两格,再向上走一格
32、帅——向右移动一格
33、帅——向下移动一格
34、帅——向上移动一格
35、帅——向左移动一格

36、士——向右下滑动,即使有两个士,也只有其中一个士能够这样走,不可能同时有两个士都这么走。所以,不需要明确指出究竟是哪个士要走这步棋,因为双士会互相堵塞,其结果是,无论如何摆放,最多都只能有一个士来走这步棋。

37、士——向右上滑动,参考上述说明,无需指出是哪个士走这步棋,因为只有一个士能走这步棋。

38、士——向左下滑动,参考上述说明,无需指出是哪个士走这步棋,因为只有一个士能走这步棋。
39、士——向左上滑动,参考上述说明,无需指出是哪个士走这步棋,因为只有一个士能走这步棋。

40、相——向右下走
41、相——向右上走
42、相——向左下走
43、相——向左上走
44、第二相——向右下走
45、第二相——向右上走
46、第二相——向左下走
47、第二相——向左上走

48、车——向右走 8 格,或向左走 1 格
49、车——向右走 7 格,或向左走 2 格
50、车——向右走 6 格,或向左走 3 格
51、车——向右走 5 格,或向左走 4 格
52、车——向右走 4 格,或向左走 5 格
53、车——向右走 3 格,或向左走 6 格
54、车——向右走 2 格,或向左走 7 格
55、车——向右走 1 格,或向左走 8 格
56、车——向上走 1 格,或向下走 9 格
57、车——向上走 2 格,或向下走 8 格
58、车——向上走 3 格,或向下走 7 格
59、车——向上走 4 格,或向下走 6 格
60、车——向上走 5 格,或向下走 5 格
61、车——向上走 6 格,或向下走 4 格
62、车——向上走 7 格,或向下走 3 格
63、车——向上走 8 格,或向下走 2 格
64、车——向上走 9 格,或向下走 1 格
65、第二车——向右走 8 格,或向左走 1 格
66、第二车——向右走 7 格,或向左走 2 格
67、第二车——向右走 6 格,或向左走 3 格
68、第二车——向右走 5 格,或向左走 4 格
69、第二车——向右走 4 格,或向左走 5 格
70、第二车——向右走 3 格,或向左走 6 格
71、第二车——向右走 2 格,或向左走 7 格
72、第二车——向右走 1 格,或向左走 8 格
73、第二车——向上走 1 格,或向下走 9 格
74、第二车——向上走 2 格,或向下走 8 格
75、第二车——向上走 3 格,或向下走 7 格
76、第二车——向上走 4 格,或向下走 6 格
77、第二车——向上走 5 格,或向下走 5 格
78、第二车——向上走 6 格,或向下走 4 格
79、第二车——向上走 7 格,或向下走 3 格
80、第二车——向上走 8 格,或向下走 2 格
81、第二车——向上走 9 格,或向下走 1 格
82、炮——向右走 8 格,或向左走 1 格
83、炮——向右走 7 格,或向左走 2 格
84、炮——向右走 6 格,或向左走 3 格
85、炮——向右走 5 格,或向左走 4 格
86、炮——向右走 4 格,或向左走 5 格
87、炮——向右走 3 格,或向左走 6 格
88、炮——向右走 2 格,或向左走 7 格
89、炮——向右走 1 格,或向左走 8 格
90、炮——向上走 1 格,或向下走 9 格
91、炮——向上走 2 格,或向下走 8 格
92、炮——向上走 3 格,或向下走 7 格
93、炮——向上走 4 格,或向下走 6 格
94、炮——向上走 5 格,或向下走 5 格
95、炮——向上走 6 格,或向下走 4 格
96、炮——向上走 7 格,或向下走 3 格
97、炮——向上走 8 格,或向下走 2 格
98、炮——向上走 9 格,或向下走 1 格
99、第二炮——向右走 8 格,或向左走 1 格
100、第二炮——向右走 7 格,或向左走 2 格
101、第二炮——向右走 6 格,或向左走 3 格
102、第二炮——向右走 5 格,或向左走 4 格
103、第二炮——向右走 4 格,或向左走 5 格
104、第二炮——向右走 3 格,或向左走 6 格
105、第二炮——向右走 2 格,或向左走 7 格
106、第二炮——向右走 1 格,或向左走 8 格
107、第二炮——向上走 1 格,或向下走 9 格
108、第二炮——向上走 2 格,或向下走 8 格
109、第二炮——向上走 3 格,或向下走 7 格
110、第二炮——向上走 4 格,或向下走 6 格
111、第二炮——向上走 5 格,或向下走 5 格
112、第二炮——向上走 6 格,或向下走 4 格
113、第二炮——向上走 7 格,或向下走 3 格
114、第二炮——向上走 8 格,或向下走 2 格
115、第二炮——向上走 9 格,或向下走 1 格

用 1 个字节(实际上只需要 7 个二进制位),足以表示这 115 个着法。




作者: 不点    时间: 2022-9-8 19:26
本帖最后由 不点 于 2022-9-8 20:55 编辑

前面我们有了堆积木的公式:

(n + 1) * n / 2 = m

这是一元二次方程,可以求出 n:

n = ceil(sqrt(2 * m + 0.25) − 0.5);


这个结果很重要,如果没有它,我们就无法根据序号 m 来反求双相的位置。

现在,我们不再用解方程的方法了,而是使用估算的方法,来得出 n 的值。

难道说,上面得出的这个公式还不够好吗?

不是 “不好”,而是 “太好了”,以至于它难以推广到高次方程当中。

一元二次方程,求解的结果太准确了。我们要让求解的过程粗糙一些,但结果当然必须仍是准确的。

我们现在就开始估算了。我们假装 n 是难以求出来的,只能采用估算的方法。

方程两边同乘以 2,得

(n + 1) * n = 2 * m

左边是两个连续自然数相乘,我们心中暗想:唉!假如左边直接是两个相同的数相乘,那不就简单了吗?

哪个数的平方与 (n + 1) * n 最接近呢?很容易想到,(n + 0.5)²(n + 1) * n 最接近。

下一个问题,究竟 (n + 1)* n 与 (n + 0.5)² 谁大呢?

肯定 (n + 0.5)² 大呀!

在两数的总和是固定的情况下,两数相乘要想取得最大值,只有当这两个数完全相等时才行。

这条规律在多个数相乘时,也是成立的。

也就是说,这方法能够用于  (n + 2) * (n + 1) * n 的情况。

我们将来在求解三个兵的组合问题时,就要遇到这个情况。

好的,回到正题。我们已经知道

(n + 0.5)²  > (n + 1) * n

也就是

(n + 0.5)² > 2 * m

因此,我们得到了第一个估算的式子:

n > sqrt(2 * m) 0.5

这是 n 大于某个数的形式。

此时我们心中暗想,假如还能有个 n 小于某个数的形式,那就齐全了。而最理想的呢,就是这个:

n < sqrt(2 * m) + 0.5

试试看,有没有好运气?

2 * m = (n + 1) * n > n²

所以

n < sqrt(2 * m) 【<------------- 这个运气超好!】

综合两个不等式,现在我们有了

sqrt(2 * m) 0.5 < n < sqrt(2 * m)

简直太好了!我们来验证一下,看看对不对:


【记住 m 一定是一行中最大的那个数,否则 (n + 1) * n / 2 = m 不成立】

当 m = 1 时,得到 1.414 0.5 < n < 1.414,此时,n 只能等于 1,嗯,没错。
当 m = 3 时,得到 2.449 0.5 < n < 2.449,此时,n 只能等于 2,嗯,没错。
当 m = 6 时,得到 3.464 0.5 < n < 3.464,此时,n 只能等于 3,嗯,没错。
当 m =10 时,得到 4.472 0.5 < n < 4.472,此时,n 只能等于 4,嗯,没错。
当 m =15 时,得到 5.477 0.5 < n < 5.477,此时,n 只能等于 5,嗯,没错。
当 m =21 时,得到 6.481 0.5 < n < 6.481,此时,n 只能等于 6,嗯,没错。
当 m =28 时,得到 7.483 0.5 < n < 7.483,此时,n 只能等于 7,嗯,没错。
……………………………………
弄个大一点的数试试,因为如果小数部分与 0.5 逐渐逼近,电脑的误差就可能影响程序的执行结果了!

当 m =200010000 时,得到
20000.49999375 0.5 < n < 20000.49999375,此时,n 只能等于 20000,嗯,没错。

果然!小数部分无限接近 0.5,电脑有可能把它弄成 0.5,从而造成错误!我们今后还得防止此类问题的发生。

回到正题。既然有了

sqrt(2 * m) 0.5 < n < sqrt(2 * m)


那我们就可以肯定

n = ceil(sqrt(2 * m) 0.5)

了。

跟我们先前得到的结果对比一下。我们原先得到的是

n = ceil(sqrt(2 * m + 0.25) − 0.5)

其实两个结果都是正确的,只不过计算公式有一点点差别。

必须说明,我们求得的表达式,也适用于当 m 不是一行中最大值的情况。这是因为,这一行的最小一个数,也比上一行最大的数 m’ 大。我们只需要证明,只要 m = m’ + 1,即,m 只比 m’ 增加了 1,相应的 n 就会比 n’ 增加 1。

因为 m’ 是一行中最大的那个数,所以,

m’ = (n’ + 1) * n’ / 2

sqrt(2 * m) 0.5 = sqrt(2 * m’ + 2) 0.5 = sqrt((n’ + 1) * n’ + 2) 0.5

我们现在来证明它是大于 n’ 的。也就是要证明:

sqrt((n’ + 1) * n’ + 2) 0.5 > n’

把 0.5 移到右边,就是要证明

sqrt((n’ + 1) * n’ + 2) > n’ + 0.5

两边再平方,就等价于要证明

(n’ + 1) * n’ + 2 > (n’ + 0.5)²

展开以后,就是

n’ ² + n’ + 2 > n’ ² + n’ + 0.25

这显然是成立的。因此,倒推回去,就证明了

sqrt((n’ + 1) * n’ + 2) 0.5 > n’

也即

sqrt(2 * m) 0.5 > n’

因此,

ceil(sqrt(2 * m) 0.5) > n’

另一方面,此时 m 是新行中最小的数,而新行的行号 n 肯定不小于 ceil(sqrt(2 * m) 0.5),即

n >= ceil(sqrt(2 * m) 0.5)

这是因为 n = ceil(sqrt(2 * M) 0.5),M 是此行中的最大数,而 m 却是此行中的最小数。

另外,注意到 n = n’ + 1,所以

n’ < ceil(sqrt(2 * m) 0.5) <= n’ + 1

因此肯定有

ceil(sqrt(2 * m) 0.5) = n’ + 1 = n

到此为止,我们证明了,即使 m 是一行中最小的那个数,m 所在的行号 n 仍然可以表示为

n = ceil(sqrt(2 * m) 0.5)

大功告成,证明完毕。


上述求解(估算)的过程,比前面直接求解一元二次方程繁琐多了!然而,这个办法推广到高次方程的情况,也基本就是同样的套路,不会更加繁琐了。可是,前面一元二次方程的求解过程,却无法套到高次方程上。所以,尽管这是一个繁琐的方法,但却比简单的方法更有优势。





作者: 不点    时间: 2022-9-8 22:28
本帖最后由 不点 于 2022-9-9 06:44 编辑

我们不着急去处理兵的高次方程问题。我们先把二次方程的简单问题彻底搞透了,再去处理那些复杂问题也不迟。相的数据量比较小,还是拿相来说事。而且,我们让帅占据中相位,这样,每个相只有 0,1,2,3,4,5 这六个位置可以活动(前面已经定义了这六个位置,不再重复),这样我们勾画出映射的框架,易于观察和理解。

下面就是这个对应关系了。

括号外是整体可能性的序号。括号内是两个相的位置编号。

双相都不存在的情况,是特殊情况。这种可能性也不能被漏掉,所以我们要为它编号,把它记录下来。这种情况,整体就编为 0 号吧:

        0()

只存在一个相的情况,整体编号 1~6:

1(0, ) 2(1, ) 3(2, ) 4(3, ) 5(4, ) 6(5, )

双相都存在的情况,其中较小的相位编号总是排在左侧。整体编号 7~21:

        07(0,1)

      08(0,2)  09(1,2)

    10(0,3) 11(1,3) 12(2,3)


  13(0,4) 14(1,4) 15(2,4) 16(3,4)

17(0,5) 18(1,5) 19(2,5) 20(3,5) 21(4,5)




第一个问题:已知括号外的整体编号 SN,如何确定双相的位置(A,B)呢?

function get_相位置_from_整体编号 (SN) {
    if (SN == 0) {
        return [null, null]; // 双相都不存在
    }
    if (SN < 7) {
        return [SN - 1, null]; // 只存在一个相
    }
    //双相都在
    let m = SN - 6; // m 是从 1 开头的自然数
    let B = Math.ceil(Math.sqrt(2 * m) - 0.5); // m 所在的行号
    //此行的前一行中最大的数是多少?
    let M = B * (B - 1) / 2;
    let A = m - M - 1;
    return [A, B]; // 双相的位置
}


此处调用 sqrt 和 ceil 函数,就可能产生精度错误了。由于 m 是正整数,因此


sqrt(2*m) - 0.5


就不可能是整数了。我们前面已经知道,它总是小于我们想要得到的那个行号,所以,我们再添上一个 ceil 函数,就得到了行号。比如说,它如果是 7.999999,那我们用 ceil 函数:ceil(7.999999) 就得到了 8。这没问题。如果再狠一点,假如它是 700000000000.9999999999999999999999999999999999999999999999
,我们还是要用 ceil 函数。假如在调用 ceil 之前,系统已经有误差了,已经把它弄成  700000000001 了,那么,ceil(700000000001) 仍然得到 700000000001,这仍然很幸运,没出现问题。不过,假如系统已经把它弄成 700000000001.00001 了,此时若执行 ceil 函数,会得到一个错误的结果:700000000002,出错了!这对于我们的程序来说可是致命的!如果只有一次 sqrt 函数调用,出现的误差可能是不大的。假如反复调用一些会产生误差的函数,误差积累时,就可能出现超出预期的偏差了。我们这里没有误差积累(调用 sqrt 之后马上就调用 ceil 函数,没有别的步骤来使误差产生积累),而且参数的值也不会达到 700000000000 这样的数量级。所以,基本上可以说,不会出问题。因此也就不需要使用额外的手段来加强防护了。


第二个问题:已知双相位置 pos,如何获得整体编号呢?

function get_整体编号_from_相位置 (pos) {
    if (typeof pos[0] !== 'number') {
        return 0; // 双相都不存在
    }
    if (typeof pos[1] !== 'number') {
        return pos[0] + 1; // 只存在一个相位于 pos[0]
    }
    //双相都在
    let B = pos[1]; // 行号
    let M = B * (B - 1) / 2; // 前一行的最大数
    return M + pos[0] + 1 + 6;
}



作者: 不点    时间: 2022-9-9 07:55
本帖最后由 不点 于 2022-9-9 09:36 编辑

现在看看 3 个兵的情况。兵本来有 55 个可能的位置,但是,我们先探讨规律,所以,先只观察 8 个位置的情况。假定兵的位置编号是 0, 1, 2, 3, 4, 5, 6, 7。以下先穷举出那些同时出现 3 个兵的所有可能的组合:

共有 8*7*6/(3*2) = 56 种:

(0 1 2) (0 1 3) (0 1 4) (0 1 5) (0 1 6) (0 1 7)
(0 2 3) (0 2 4) (0 2 5) (0 2 6) (0 2 7)
(0 3 4) (0 3 5) (0 3 6) (0 3 7)
(0 4 5) (0 4 6) (0 4 7)
(0 5 6) (0 5 7)
(0 6 7)


(1 2 3) (1 2 4) (1 2 5) (1 2 6) (1 2 7)
(1 3 4) (1 3 5) (1 3 6) (1 3 7)
(1 4 5) (1 4 6) (1 4 7)
(1 5 6) (1 5 7)
(1 6 7)


(2 3 4) (2 3 5) (2 3 6) (2 3 7)
(2 4 5) (2 4 6) (2 4 7)
(2 5 6) (2 5 7)
(2 6 7)


(3 4 5) (3 4 6) (3 4 7)
(3 5 6) (3 5 7)
(3 6 7)


(4 5 6) (4 5 7)
(4 6 7)


(5 6 7)

我们在双相的时候,同时出现两个相的情况,是排列成(平面)三角形了。


现在,三个兵的情况,已经上升到 “立体” 了,就跟立方体用刀切掉一个角是一样的。


不过,我们要的,正是被切掉的角部四面体,而不是留下的多面体。


这是一个直角的四面体,或者说成三角锥体也行。


如果我们把砖块堆到墙角,堆成一个斜坡形状,那就是锥形了。


刚才在上面列出的数据,分成了几个三角形。


最上面的那个三角形最大,我们可以把它放在墙角的底部。


第二个三角形比它稍小,就摞在它上面。


第三个更小一点,就再摞起来。


然后摞上第四个小三角形。


到第五个的时候,这个三角形就只有三个元素了,也把它摞上。


第六个,是最后一个三角形。它已经不是三角形了,它退化成一个元素了,把它也摞上,它就位于锥形的顶尖了。


我们看到,在二维平面的情况,我们还可以画出框架。现在是三维立体了,我们已经没法画出来了,只能用文字来描述。


设想一下,将来如果碰上四维、五维的超体,那岂不更糟糕?


所以,我们该知足了。目前的情况还是令人满意的。



作者: 不点    时间: 2022-9-9 11:53
本帖最后由 不点 于 2022-9-9 17:17 编辑

把数据重新排列一下,貌似这样,才可以有简单的运算规律。

(0 1 2)

(0 1 3)
(0 2 3) (1 2 3)

(0 1 4)
(0 2 4) (1 2 4)
(0 3 4) (1 3 4) (2 3 4)

(0 1 5)
(0 2 5) (1 2 5)
(0 3 5) (1 3 5) (2 3 5)
(0 4 5) (1 4 5) (2 4 5) (3 4 5)

(0 1 6)
(0 2 6) (1 2 6)
(0 3 6) (1 3 6) (2 3 6)
(0 4 6) (1 4 6) (2 4 6) (3 4 6)
(0 5 6) (1 5 6) (2 5 6) (3 5 6) (4 5 6)

(0 1 7)
(0 2 7) (1 2 7)
(0 3 7) (1 3 7) (2 3 7)
(0 4 7) (1 4 7) (2 4 7) (3 4 7)
(0 5 7) (1 5 7) (2 5 7) (3 5 7) (4 5 7)
(0 6 7) (1 6 7) (2 6 7) (3 6 7) (4 6 7) (5 6 7)

在上述排列中,这个立体是分层的。括号外面的自然数省略了,没有标注。从上到下,自然数是递增的。

自然数 m 和层号 n 的关系是(此处,m 是某一层中最大的那个自然数,否则不成立):

m = (n + 2) * (n + 1) * n / 6

比如,第一层,n = 1,计算的结果,m = 1。

第二层,n = 2,计算结果,m = 4,它就是第二层中最大的那个自然数(序号)。

就是说,已知层数 n,求层内最大的数 m,很容易。

但是,反过来,已知 m 来求 n,则需要解三次方程,就不容易了。

我们不去解方程,而是采用估算的方法。

把方程变成这样:

(n + 2) * (n + 1) * n = 6 * m

这三个连续自然数相乘,要与某个数的立方进行对比,很容易想到,应该与 (n + 1)³ 进行对比。

(n + 1)³ > (n + 2) * (n + 1) * n = 6 * m

所以,

n > (6 * m) ** (1 / 3) - 1

再用

n³ < (n + 2) * (n + 1) * n = 6 * m

得到

n < (6 * m) ** (1 / 3)

综合以上两个不等式,可以肯定

n = ceil((6 * m) ** (1 / 3) - 1)

这个式子,是当 m 是一层中最大的数时,才可以保证成立的。需要进一步证明,当 m 不是一层中的最大数时,式子也能成立。只需要证明,当 m 是一层中最小的数时,式子照样成立便可。


假定 m 是一行中最小的数,而 m’ 是上一行最大的数。显然,m = m’ + 1。由于 m’ 是一行里面最大的数,所以

n’ = ceil((6 * m’) ** (1 / 3) - 1)

m’ = (n’ + 2) * (n’ + 1) * n’ / 6

都是成立的。我们考察 (6 * m) ** (1 / 3) - 1,希望能证明它大于 n’。因为

(6 * m) ** (1 / 3) - 1 = (6 * m’ + 6) ** (1 / 3) - 1

所以,也就是说,我们想要证明

(6 * m’ + 6) ** (1 / 3) - 1 > n’

变换一下,等价于要证明

(6 * m’ + 6) ** (1 / 3) > n’ + 1


也即



(6 * m’ + 6) > (n’ + 1)³

也即


(n’ + 2) * (n’ + 1) * n’ + 6  > (n’ + 1)³



两边展开后是


n ³ + 3 * n’ ² + 2 * n’ + 6 > n ³ + 3 * n’ ² + 3 * n’ + 1



糟糕!这式子无法成立!当 n’ 增大时,右边将变大!


我们要失败了!


这说明,我们估计出来的表达式


n = ceil((6 * m) ** (1 / 3) - 1)



不够精确,需要更精细的估计才行。



作者: 不点    时间: 2022-9-9 19:49
前面用估算的方法求解方程

(n + 2) * (n + 1) * n = 6 * m

得到的表达式

n = ceil((6 * m) ** (1 / 3) - 1)

虽然是正确的,但却不能适用于当 m 不是一层中的最大值的情况。所以,这仍然算是一个失败的表达式。

本帖要尝试一种更精细的估计。

我们看

(n + 2) * (n + 1) * n = 6 * m

变换左边,得

(n + 1) ³ - (n + 1)  = 6 * m

再变换

(n + 1) ³ = 6 * m + (n + 1)

令 x = n + 1,有

x³ = 6 * m + x

x = (6 * m + x) ** (1 / 3)

由于 x = n + 1 > 0,因此

x = (6 * m + x) ** (1 / 3) > (6 * m + 0) ** (1 / 3)

因此

x > (6 * m) ** (1 / 3)

这其实就是我们以前给出的估计值。

现在我们粗略认为这个估计仍然可以初步使用,即

x ≈ (6 * m) ** (1 / 3)

好了,我们把它代入前面曾经得到的式子

x = (6 * m + x) ** (1 / 3)

的右端,得

x ≈ (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

这个估计值,显然比前面的估计值更大一些了。

我们知道,

x = (6 * m + x) ** (1 / 3)     ……………………(1)

是真正成立的。我们又知道

x > (6 * m) ** (1 / 3)

因此,当我们把(1)式右边的 x 换成 (6 * m) ** (1 / 3) 以后,

(1)的右边就变小了,而(1)的左边还是 x,所以,左边大,右边小:

x >  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

注意到 x = n + 1,所以

n > -1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

这就是对 n 的最新一次估计。当然,为了确定 n,我们还需要证明

n <  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

这是容易的,因为我们在前一帖中已经知道

n < (6 * m) ** (1 / 3)



(6 * m) ** (1 / 3) < (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

所以

n <  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

也是没问题的。综合以上两个不等式,就可以肯定

n = ceil( -1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3))

好了,别忘了,这是假定 m 为一层中的最大值才获得的结果。接下来需要证明,这个结果对于别的 m 也成立,其实只需证明,对于一层中最小的那个 m 成立即可。

假定 m 是一层中最小的值。那么,m’ = m - 1 就是上一层中最大的值了。所以,

n’ = ceil( -1 +  (6 * m’ + (6 * m’) ** (1 / 3)) ** (1 / 3))
m’ = (n’ + 2) * (n’ + 1) * n’ / 6

都成立。我们考察

-1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

希望能证明它大于 n’。因为

-1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)
= -1 +  (6 * m’ + 6 + (6 * m’ + 6) ** (1 / 3)) ** (1 / 3)

所以,也就是说,我们想要证明

-1 +  (6 * m’ + 6 + (6 * m’ + 6) ** (1 / 3)) ** (1 / 3) > n’

把它进行等价变换,就是想要证明

(6 * m’ + 6 + (6 * m’ + 6) ** (1 / 3)) ** (1 / 3) > n’ + 1

也即

(6 * m’ + 6 + (6 * m’ + 6) ** (1 / 3)) > (n’ + 1) ³


也即


((n’ + 2) * (n’ + 1) * n’ + 6 + ((n’ + 2) * (n’ + 1) * n’ + 6)  ** (1 / 3)) > (n’ + 1) ³


两边展开一下,得


n’  ³ + 3 * n’ ² + 2 * n’ + 6 + (n’  ³ + 3 * n’ ² + 2 * n’ + 6)  ** (1 / 3) > n’  ³ + 3 * n’ ² + 3 * n’ + 1


化简,得



(n’  ³ + 3 * n’ ² + 2 * n’ + 6)  ** (1 / 3) > n’ - 5


这显然是成立的,因为左边大于 n’, 而右边小于 n’。



反推回去,就证明了


-1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3) > n’


终于成功啦!这样,就得到


ceil( -1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3) ) > n’

假定 m 所在层中的最大数是 M,我们有


n = ceil( -1 +  (6 * M + (6 * M) ** (1 / 3)) ** (1 / 3) )


  >= ceil( -1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3) )


注意到 n = n’ + 1,综合上述两个不等式,肯定有


n = ceil( -1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3) )


到此为止,我们证明了,当 m 并非一层中的最大值时,上述这个求层号的表达式仍然成立。只有像这样证明了其普适性以后,我们才能使用这个表达式,否则,我们在程序中就不敢使用这个表达式。


作者: 不点    时间: 2022-9-9 23:42
本帖最后由 不点 于 2022-9-10 06:01 编辑

现在给出已知整体编号 SN 求三兵位置的函数


function get_三兵位置_from_整体编号 (SN) {
    if (SN == 0) {
        return [null, null, null]; // 三兵都不存在
    }
    if (SN < 9) {
        return [SN - 1, null, null]; // 只存在一个兵
    }
    if (SN < 37) {    //存在两个兵
        let m = SN - 8; // m 是从 1 开头的自然数
        let B = Math.ceil(Math.sqrt(2 * m) - 0.5); // m 所在的行号
        //此行的前一行中最大的数是多少?
        let M = B * (B - 1) / 2;
        let A = m - M - 1;
        return [A, B, null]; // 双兵的位置
    }
        //三兵都在
        let m = SN - 36; // m 是从 1 开头的自然数
        let C = Math.ceil((6 * m+(6*m)**(1/3))**(1/3) - 1); // m 所在的层号
        //此层的上一层中最大的数是多少?
        let M = (C + 1) * C * (C - 1) / 6;
        // 让此层的第一个数拥有临时序号 1
        let t = SN - M;
        // 从 t 可以求出 A, B
        let B = Math.ceil(Math.sqrt(2 * t) - 0.5); // t 所在的行号
        //此行的前一行中最大的数是多少?
        let W = B * (B - 1) / 2;
        let A = t - W - 1;

        return [A, B, C+1]; // 三兵的位置

}

下面的函数是, 已知三兵位置,算出整体编号。


function get_整体编号_from_三兵位置 (pos) {
    if (typeof pos[0] !== 'number') {
        return 0; // 三兵都不存在
    }
    if (typeof pos[1] !== 'number') {
        return pos[0] + 1; // 只存在一个兵位于 pos[0]
    }
    if (typeof pos[2] !== 'number') {
        // 只存在两个兵位于 pos[0], pos[1]
        let B = pos[1]; // 行号
        let M = B * (B - 1) / 2; // 前一行的最大数
        return M + pos[0] + 1 + 8;
    }

        //三兵都在

        let C = pos[2] - 1; // 层号

        let W = (C + 1) * C * (C - 1) / 6; // 前一层的最大数
        let B = pos[1]; // 在本层中的行号
        let M = B * (B - 1) / 2; // 前一行的最大数
    return M + pos[0] + 1 + W + 37;
}



作者: 不点    时间: 2022-9-10 06:51
三个兵的情况,堆成了一个三角锥体,放在墙角。四个兵的情况,就没法再这样描述了。

在三个兵的锥体中,我们用层号来区分不同的层。

在四个兵的情况,它不是按层来分的,而是按 “锥体” 来分的。第一个锥体,就退化为一个元素。第二个锥体,是一个由 4 个元素组成的小锥体。第三个锥体,是一个由 10 个元素组成的一个边长为 3 个元素的小锥体。依次类推。这些锥体合起来,构成四维的超锥体。

从前面三维锥体的排列中,我们已经大致知道了其排列的规律。四维的情况,我们就不再罗列出每个情况了。因为我们已经明白,最关键的地方,三维的情况是要把层号求出来,四维的情况,就是把锥体号求出来。

现在进入正题。

跟三维的情况类似,四维超体中元素的最大编号 m 与锥体号 n 有如下关系:

m = (n+3)(n+2)(n+1)n/24

现在又变成一个数学问题,即,如何求解 n。

作者: 不点    时间: 2022-9-10 07:56
上述四次方程碰巧能够求得精确解:

n = -1.5 + sqrt(1.25 + sqrt(1 + 24*m))

既然能得到精确解,那就不用再去估算了。而且,对于 m 不是某个 “锥体” 号中最大数的情形,只需对右边做 ceil 即可:

n = ceil ( -1.5 + sqrt(1.25 + sqrt(1 + 24*m)) )

然而这个地方就需要注意了。当我们手工计算能够得到准确的平方根的时候,我们很高兴,然而此时电脑反而有可能出现近似值。比如说,我们手工开平方得到了 3,而电脑却得到的是 3.00001,这可咋办?此时该不该用 ceil 函数得到 4 的最终结果?电脑就一根筋啊,它不知道 4 是错的呀。为了防止这种情况发生,我们可以稍稍改动一下上述表达式,让它稍微小一点:

n = ceil ( -1.5 + sqrt(1.25 + sqrt(24*m)) )

取 ceil 之后,并不影响最终结果。这样就比较安全了。

作者: 不点    时间: 2022-9-10 09:20
前面的三次方程,其实也有精确解:

n = -1 + cbrt(3m + sqrt(9m² - 1/27)) + cbrt(3m - sqrt(9m² - 1/27))

此处 cbrt 是立方根的意思。

虽然有精确解,但是,它的计算量反而比较大,因为至少需要 3 次调用求根函数。我们前面采用的估计方法,反倒是更优的算法。

到目前为止,求解三次方程,是我们碰到的最难处理的一个环节了。不知道接下来的 5 次方程,情况会是怎样的。

作者: 不点    时间: 2022-9-11 21:11
为了处理五次方程,查了很多资料。果然是难办的。


化简以后,我们其实需要处理的五次方程就是这个样子的:


x⁵ - 5x³ + 4x - 120m = 0


然而不幸的是,我未能找到这个方程的精确解。如果系数是这样的:


x⁵ - 5x³ + 5x - 120m = 0

那就有根式解了。可偏偏一次项系数不是 5,而是 4,那就没辙了。



我其实挣扎了一番,试图寻找求解方法,但最终还是失败了。


精确解没找到,能用估计的方法,求得一个近似解也行啊?


可是,估算的方法也不太顺利。


这个 5 次方程比 3 次方程复杂了不少,用估算的办法,也得消耗大量的时间去尝试。


今天我利用了一个在线画函数曲线的网站,来尝试画各种曲线图,以便帮助我判断,确定究竟啥样的曲线是合适的。终于有了一点希望。以下这个函数很关键,我迫不及待地把它记录下来:


-5*x^3+4*x+120+5*(x^5-5*x^3+4*x+120+5*(x^5-5*x^3+4*x+120)^(3/5))^(3/5) - 4*(x^5-5*x^3+4*x+120+5*(x^5-5*x^3+4*x+120)^(3/5))^(1/5)


这个函数有可能帮助我走向成功。还有下面这个函数,也是很奇妙的:

-5*x^3+4*x+120+5*(x^5-5*x^3+4*x+120+5*(x^5-5*x^3+4*x+120)^(3/5))^(3/5) - 4*(x^5-5*x^3+4*x+120)^(1/5)


我把它们放在这里,是不想让它们丢失,防止以后找不到它们了。





作者: 不点    时间: 2022-9-11 21:48
通过初步的试验,这样一个表达式:

n = ceil(-2+(120*m+5*(120*m+5*(120*m)**0.6)**0.6-4*(120*m)**0.2)**0.2));

貌似是成功的。现在还没有从数学上严格证明。慢慢来吧,只要确实是成功的,应该也能从数学上证明。如果证明不了,就算能用,也不完美,而且,也放心不下。
作者: ZMLoveLH    时间: 2022-9-12 13:42
我喜欢象棋,上网下一局
作者: 不点    时间: 2022-9-12 14:21
本帖最后由 不点 于 2022-9-12 14:23 编辑

上述 5 维锥体中元素号 m 与其所处 4 维锥体号 n 的关系为

n = ceil(-2+(120*m+5*(120*m+5*(120*m)**0.6)**0.6-4*(120*m)**0.2)**0.2));

本来想给出一个严格的证明,但是,当我尝试去证明的时候,发现这证明竟然异常困难。

尽管证明起来不容易,但我还是相信这个式子是对的。

下面考虑另一个问题:电脑计算时的误差。我验证了,当 n 值到达 500 左右那么大的时候,电脑计算产生的误差就可能导致 n 值被多算了 1 个。

不过,我们在解决 5 个兵的问题时,最大也只能碰上 n = 55 的情况(实际上只能碰上 n = 51 的情况)。因此,在我们的应用范围内,这个公式是安全的。公式不仅(针对电脑的计算误差而言)是安全的,而且(在我们的应用范围内)是正确的,这是因为对 400 以内的 n 值,我都是人工逐一验证过的,没问题的。也就是说,不管这个公式在理论上是否 100% 成立,它在我们具体的(较小的)应用范围内,是 100% 可靠的,其可靠性是由人工(穷举)验证来保证的。


下面是我验证时编写的程序,这是只有一行的程序,程序代码之中没有回车(或换行)符。这行代码可以复制粘贴到浏览器的控制台中直接(调试)执行。计算得到的四维锥号是未经 ceil 运算的值,便于观察它的变化规律。


for (let n = 1; n < 1000; n++) { let m = (n+4)*(n+3)*(n+2)*(n+1)*n; let p=m+120; console.log('真实四维锥号 n = ' + n + ', 计算得到的本锥最大元素的锥号: ' + (-2+(m+5*(m+5*m**0.6)**0.6-4*m**0.2)**0.2) + ', 计算得到的下一锥最小元素的锥号: ' + (-2+(p+5*(p+5*p**0.6)**0.6-4*p**0.2)**0.2)) ;}



计算的结果有 1000 行之多,就不适合贴在这里了。

作者: 不点    时间: 2022-9-12 22:00
本帖最后由 不点 于 2022-9-13 07:53 编辑

现在给出五兵位置与整体编号互映射的两个函数,都已经过调试验证。

在两个函数之后的结尾处,附有简短的测试代码。



  1. <!DOCTYPE html>
  2. <html>
  3. <head> <meta charset="UTF-8">
  4. <script>
  5. //现在给出已知整体编号 SN 求五兵位置的函数

  6. //每个兵的位置有 55 种可能性,这 55 个位置的编号为 0~54。


  7. function get_五兵位置_from_整体编号 (SN) {
  8.     var A, B, C, D, E, t, m;

  9.     if (SN == 0) {
  10.         return [null, null, null, null, null]; // 五个兵都不存在
  11.     }
  12.     if (SN < 56) { // 56 = 1 + 55
  13.         return [SN - 1, null, null, null, null]; // 只存在一个兵
  14.     }
  15.     if (SN < 1541) {    // 1+55+55*54/2
  16.         //只存在两个兵
  17.         A = SN - 55; // 当前元素在当前层内部的编号, 从 1 开始
  18.         // 计算当前元素在当前层中的行号 B

  19.         t = 2 * A;

  20.         B = Math.ceil(Math.sqrt(t) - 0.5);

  21.         // 用 B 计算前一行最大元素在当前层内部的编号 m

  22.         m = B * (B - 1) / 2;
  23.         A -= m; // 当前元素在当前行内部的编号,从 1 开始

  24.         return [A-1, B, null, null, null]; // 双兵的位置
  25.     }
  26.     if (SN < 27776) {    // 1+55+55*54/2+55*54*53/6
  27.         //只存在三个兵
  28.         A = SN - 1540; // 当前元素在当前(三维)锥内部的编号, 从 1 开始
  29.         // 计算当前元素在当前(三维)锥中的层号 C

  30.         t = 6 * A;

  31.         C = Math.ceil((t + t**(1/3))**(1/3) - 1);
  32.         // 用 C 计算上一层最大元素在当前(三维)锥内部的编号 m
  33.         m = (C + 1) * C * (C - 1) / 6;
  34.         A -= m; // 当前元素在当前层内部的编号,从 1 开始
  35.         // 计算当前元素在当前层中的行号 B
  36.         t = 2 * A;

  37.         B = Math.ceil(Math.sqrt(t) - 0.5);

  38.         // 用 B 计算前一行最大元素在当前层内部的编号 m
  39.         m = B * (B - 1) / 2;
  40.         A -= m; // 当前元素在当前行内部的编号,从 1 开始

  41.         return [A-1, B, C+1, null, null]; // 三个兵的位置
  42.     }


  43.     if (SN < 368831) {  // 1+55+55*54/2+55*54*53/6+55*54*53*52/24
  44.         //只存在四个兵
  45.         A = SN - 27775; // 当前元素在四维超锥内部的编号, 从 1 开始

  46.         // 计算当前元素所在(三维)锥号 D

  47.         t = 24 * A;

  48.         D = Math.ceil ( -1.5 + Math.sqrt(1.25 + Math.sqrt(t)) );

  49.         // 用 D 计算上一锥最大元素在四维超锥内部的编号 m
  50.         m = (D + 2) *(D + 1) * D * (D - 1) / 24;
  51.         A -= m; // 当前元素在当前(三维)锥内部的编号,从 1 开始
  52.         // 计算当前元素在当前(三维)锥中的层号 C
  53.         t = 6 * A;

  54.         C = Math.ceil((t + t**(1/3))**(1/3) - 1);
  55.         // 用 C 计算上一层最大元素在当前(三维)锥内部的编号 m
  56.         m = (C + 1) * C * (C - 1) / 6;
  57.         A -= m; // 当前元素在当前层内部的编号,从 1 开始
  58.         // 计算当前元素在当前层中的行号 B

  59.         t = 2 * A;

  60.         B = Math.ceil(Math.sqrt(t) - 0.5);
  61.         // 用 B 计算前一行最大元素在当前层内部的编号 m
  62.         m = B * (B - 1) / 2;
  63.         A -= m; // 当前元素在当前行内部的编号,从 1 开始


  64.         return [A-1, B, C+1, D+2, null]; // 四个兵的位置

  65.     }



  66.         //五个兵都在
  67.         A = SN - 368830; // 当前元素在五维超锥内部的编号, 从 1 开始
  68.         // 用 A 计算当前元素所在(四维)超锥号 E
  69.         t = 120 * A;

  70.         E = Math.ceil ( -2 + (t + 5*(t+5*t**0.6)**0.6 - 4 * t**0.2)**0.2 );
  71.         // 用 E 计算上一(四维)超锥最大元素在五维超锥内部的编号 m
  72.         m = (E + 3)* (E + 2) *(E + 1) * E * (E - 1) / 120;


  73.         A -= m; // 当前元素在当前(四维)超锥内部的编号, 从 1 开始
  74.         // 用 A 计算当前元素所在(三维)锥号 D
  75.         t = 24 * A;

  76.         D = Math.ceil ( -1.5 + Math.sqrt(1.25 + Math.sqrt(t)) );

  77.         // 用 D 计算上一锥最大元素在四维超锥内部的编号 m
  78.         m = (D + 2) *(D + 1) * D * (D - 1) / 24;
  79.         A -= m; // 当前元素在当前(三维)锥内部的编号,从 1 开始
  80.         // 计算当前元素在当前(三维)锥中的层号 C

  81.         t = 6 * A;

  82.         C = Math.ceil((t + t**(1/3))**(1/3) - 1);
  83.         // 用 C 计算上一层最大元素在当前(三维)锥内部的编号 m
  84.         m = (C + 1) * C * (C - 1) / 6;
  85.         A -= m; // 当前元素在当前层内部的编号,从 1 开始
  86.         // 计算当前元素在当前层中的行号 B

  87.         t = 2 * A;

  88.         B = Math.ceil(Math.sqrt(t) - 0.5);
  89.         // 用 B 计算前一行最大元素在当前层内部的编号 m
  90.         m = B * (B - 1) / 2;
  91.         A -= m; // 当前元素在当前行内部的编号,从 1 开始


  92.         return [A-1, B, C+1, D+2, E+3]; // 五个兵的位置
  93. }



  94. //下面的函数是, 已知五兵位置,算出整体编号。

  95. function get_整体编号_from_五兵位置 (pos) {
  96.     var B, C, D, E, m;

  97.     if (typeof pos[0] !== 'number') {
  98.         return 0; // 五个兵都不存在
  99.     }
  100.     if (typeof pos[1] !== 'number') {
  101.         return pos[0] + 1; // 只存在一个兵位于 pos[0]
  102.     }
  103.     if (typeof pos[2] !== 'number') {
  104.         // 只存在两个兵位于 pos[0], pos[1]
  105.         m = 1 + 55;

  106.         B = pos[1]; // 在本层中的行号
  107.         //前一行的最大元素在当前层中的序号
  108.         m += B * (B - 1) / 2;

  109.         return m + pos[0];
  110.     }
  111.     if (typeof pos[3] !== 'number') {
  112.         // 只存在三个兵位于 pos[0], pos[1], pos[2]
  113.         m = 1 + 55 + 55 * 54 / 2;
  114.         C = pos[2] - 1; // 层号

  115.         //前一层的最大元素在当前(三维)锥中的序号

  116.         m += (C + 1) * C * (C - 1) / 6;
  117.         B = pos[1]; // 在本层中的行号

  118.         //前一行的最大元素在当前层中的序号
  119.         m += B * (B - 1) / 2;

  120.         return m + pos[0];
  121.     }
  122.     if (typeof pos[4] !== 'number') {
  123.         // 只存在四个兵位于 pos[0], pos[1], pos[2], pos[3]
  124.         m = 1 + 55 + 55 * 54 / 2 + 55 * 54 * 53 / 6;

  125.         D = pos[3] - 2; // 锥号
  126.         //前一(三维)锥中的最大元素在当前(四维)超锥中的序号
  127.         m += (D + 2) * (D + 1) * D * (D - 1) / 24;
  128.         C = pos[2] - 1; // 层号

  129.         //前一层的最大元素在当前(三维)锥中的序号
  130.         m += (C + 1) * C * (C - 1) / 6;
  131.         B = pos[1]; // 在本层中的行号

  132.         //前一行的最大元素在当前层中的序号

  133.         m += B * (B - 1) / 2;

  134.         return m + pos[0];
  135.     }
  136.         // 五个兵都在
  137.         m = 1 + 55 + 55 * 54 / 2 + 55 * 54 * 53 / 6 + 55 * 54 * 53 * 52 / 24;
  138.         E = pos[4] - 3; // 超锥号
  139.         //前一(四维)超锥中的最大元素在当前五维超锥中的序号
  140.         m += (E + 3) * (E + 2) * (E + 1) * E * (E - 1) / 120;
  141.         D = pos[3] - 2; // 锥号
  142.         //前一(三维)锥中的最大元素在当前(四维)超锥中的序号
  143.         m += (D + 2) * (D + 1) * D * (D - 1) / 24;
  144.         C = pos[2] - 1; // 层号
  145.         //前一层的最大元素在当前(三维)锥中的序号
  146.         m += (C + 1) * C * (C - 1) / 6;
  147.         B = pos[1]; // 在本层中的行号
  148.         //前一行的最大元素在当前层中的序号
  149.         m += B * (B - 1) / 2;


  150.         return m + pos[0];
  151. }


  152. // 以下是测试代码

  153. for (var i = 0; i < 3847592; i++) {

  154.         var pos = get_五兵位置_from_整体编号 (i);
  155.         var n = get_整体编号_from_五兵位置 (pos);
  156.         if (n != i) {
  157.                 console.log('出错! ' + i + ' ==== ' + pos + ' ==== ' + n);
  158.         } else
  159.         if ((i & 4095) == 0) {
  160.                 console.log('------ ' + i + ' ==== ' + pos + ' ==== ' + n);
  161.         }
  162.         //if (i > 30000) break;
  163. }
  164. console.log('--OK-- ' + '--last--' + ' ==== ' + pos + ' ==== ' + n);

  165. </script>

  166. </head>
  167. <body>

  168. </body>
  169. </html>

复制代码

作者: 不点    时间: 2022-9-17 10:14
局面用 140 位二进制数表示,在 js 中可以用 bigint 来表示。那么,库的结构,也就容易设计了。为了方便使用,库用数组来表示。数组中,每个元素也是一个数组,这个数组的第一个元素就是局面的 bigint 表示,第二个元素,就是着法的字符串表示。库的数组元素按照局面 bigint 的大小排序。
作者: 2010techon    时间: 2022-9-17 11:10
看完知道LZ为什么要浏览器js读写本地文件的权限了。
原来是在写js程序
作者: 不点    时间: 2022-9-17 11:43
本帖最后由 不点 于 2022-9-17 11:59 编辑
2010techon 发表于 2022-9-17 11:10
看完知道LZ为什么要浏览器js读写本地文件的权限了。
原来是在写js程序

感谢,多谢理解。


碰上一个能明白这个问题缘由的人,不容易。我似乎得多说几句,再解释一下。此处的 js,貌似是读取、写入本地文件,其实不然。其实是要读取和写入与 js 文件处于同一电脑上的文件。也就是说,电脑本身是处于 “服务器” 的地位,index.html 相当于是服务器上的网页。此时,js 和 html 都相当于是在服务器上,而不是所谓的“本地”。此时,js 本应有权读取和写入本机上的文件。如果 js 和 html 是在别的网站上(而不是在本机上),那自然是应该予以禁止访问的。但现在不是那样的情况,所以不应该禁止访问。现在 js 和 html 都是由本机提供的,不存在 http 访问互联网的问题,所以根本不存在所谓 “跨域” 访问的问题。以“跨域”访问作为理由,来禁止 js 应用程序的正常运作,其性质是“封杀”,这种目的,我认为属于流氓行为。






作者: 2010techon    时间: 2022-9-17 12:10
不点 发表于 2022-9-17 11:43
感谢,多谢理解。

大多数浏览器是都是属于BS应用,你的需求是要一个本地浏览器。
我记得chrome和ff是可以通过参数或者扩产来支持跨域js访问的
作者: 不点    时间: 2022-9-17 12:29
2010techon 发表于 2022-9-17 12:10
大多数浏览器是都是属于BS应用,你的需求是要一个本地浏览器。
我记得chrome和ff是可以通过参数或者扩产 ...

你的记忆貌似停留在所谓 “旧时代”。新版已经完全封杀了,不再给你任何机会了。而且,封杀行为是统一的,而不是某个浏览器单独行动。

如果某个浏览器单独行动,那还达不到封杀的目的。因为只要有一个浏览器不封杀,就相当于开了 “漏子”(后门),就不能形成有效封杀之势。 只有统一封杀,才是真正的封杀。现在,我粗略感觉,貌似是已经进行了大面积的封杀了,至于说有没有漏网的,我也没有时间和精力去逐一验证。

作者: 2010techon    时间: 2022-9-17 13:38
不点 发表于 2022-9-17 12:29
你的记忆貌似停留在所谓 “旧时代”。新版已经完全封杀了,不再给你任何机会了。而且,封杀行为是统一的 ...

还在用XP从不追新的我就没这么多烦恼
作者: 不点    时间: 2022-9-17 14:43
2010techon 发表于 2022-9-17 13:38
还在用XP从不追新的我就没这么多烦恼

随着旧电脑坏掉、老去,XP 也就灭亡了。同理,win7、8、10 都会灭亡的,只剩下 11 可以活。据说 12 也出来了,我还没赶上,现在在用的是 11。

11 有很多毛病,正准备退回 7。

XP 的优势是安装后体积占用比较小。还有一个优势,就是你说的情况,死不更新,不再有软件的恶意更新带来的烦恼了。


我已经不再使用 XP 了,已经适应了 7 带来的各种不自在。寄望将来有个好用的 Linux、BSD 之类的系统能够在办公环境普及开来。



作者: 不点    时间: 2022-9-17 20:14
中国象棋 in html5 的 基础算法逻辑仍然含有漏洞。


这个图片,红方跳马将军,黑方认为将死了,跳出 “你赢了” 的提示。其实黑方还能垫车挣扎。

可以垫车.png (450.76 KB, 下载次数: 21)

AI不知道垫车可以解杀

AI不知道垫车可以解杀

作者: 不点    时间: 2022-9-27 07:42
一个程序写得 “好” 还是 “坏”,那要看这个程序是给谁看了。评判 “好” 与 “坏” 的标准,就掌握在这个 “来看和研究这个代码” 的人手上。

好吧,各位猜到了,是我想发表评判结果了。首先,中国象棋 in html5,写得好。为什么好?因为我能看懂。如果看不懂的话,那怎么可能说 “好” 呢?

你是你们公司的一个领导,基层有一个技术项目的研究者向你汇报。他为了让项目通过验收,就设法用所谓 “新技术”、“新概念”,把项目吹成 “高、大、上”。一句话,他忽悠你,把你带到阴沟里,让你变成傻子,彻底不懂了,那么他就达到目的了。

回到正题。

中国象棋 in html5 的代码,我能看懂,至少是初步可以看懂,而有些暂时没看懂的,还可以继续琢磨。所以,我说,代码很棒!

看懂以后,发现程序中有一些毛病。有些是明显的毛病,比如,相的价值,对方边象的价值为零,己方边相价值为 18,这显然不对。我就把它更改为:都是 18。

己方马在己方车原位的时候,竟然价值为 88,超过在马原位的 85,这不像是手误,而可能是有某种道理的。但我目前想不出是啥道理,所以,我就改小这个数,变成 82。

另外,AI 走棋的时候,总喜欢弃掉自己未过河的兵,这在大多数情况下都是错的。究其原因,我觉得是兵的价值给的太少了,只有 10 左右。如果加大兵的份量,AI 就不会轻易弃掉自己的兵了。

作者: 不点    时间: 2022-9-28 14:18
这个图片,暴露出代码的一个 bug。


跟踪执行后发现,刚过河的卒不能横走,只能向前。这是个 bug。究其原因,卒的行棋规则里面,y >= 6 应为 y>= 5 才是正确的。


可以垫卒.png (388.41 KB, 下载次数: 10)

可以垫卒挣扎,却宣布“你赢了”

可以垫卒挣扎,却宣布“你赢了”

作者: 不点    时间: 2022-10-1 08:22
这个图,我跳左马去将军,对方竟然用黑马吃掉我的马!AI 不知道黑马的任务是蹩另一个红马的脚,不可以离开。


这个 bug 也跟踪到了,发现了代码的错误:在马的走棋规则中,play.map 应为 map 才对!


蹩脚马还敢走开.png (452.38 KB, 下载次数: 7)

红方跳左马将军,黑方用黑马吃掉红马,不知道此黑马需要蹩另一个红马的脚

红方跳左马将军,黑方用黑马吃掉红马,不知道此黑马需要蹩另一个红马的脚

作者: hbhsdmd    时间: 2022-10-2 11:28
漏洞肯定有的吧
作者: 不点    时间: 2022-10-3 18:42
前面给出了如下测试代码:

for (let n = 1; n < 1000; n++) { let m = (n+4)*(n+3)*(n+2)*(n+1)*n; let p=m+120; console.log('真实四维锥号 n = ' + n + ', 计算得到的本锥最大元素的锥号: ' + (-2+(m+5*(m+5*m**0.6)**0.6-4*m**0.2)**0.2) + ', 计算得到的下一锥最小元素的锥号: ' + (-2+(p+5*(p+5*p**0.6)**0.6-4*p**0.2)**0.2)) ;}


从结果可以发现,下一锥的最小元素的锥号,更容易抵抗电脑计算误差。因此,我们可以改进,让现在的本锥最大编号,放在下一锥最小编号的位置。让编号不是从 1 开始,而是从 0 开始,就可以达到这个目的。


作者: 不点    时间: 2022-10-4 15:27
前面我们有了求四维锥号的公式:

        t = 120 * A;

        E = Math.ceil ( -2 + (t + 5*(t+5*t**0.6)**0.6 - 4 * t**0.2)**0.2 );

这公式只是近似值,并非适用于所有的序号( A 值)。但在较小的序号之下,都是正确的。

今天,通过试验,又找到了一个类似的公式:

        t = 120 * A;

        E = Math.ceil ( -2 + t**0.2 + 1/(t**0.2) + 1/(6*t**0.6) );


这个公式的精度也很高,够我们用了。在我们的应用范围内,100% 可靠,不会有问题。这个公式的优点是,只需调用一次求根函数,剩下的只是加减乘除。上述公式在 E < 700 时都是完全可靠的。而在我们处理兵的问题中,E 值总是 < 60,所以,没问题。而且,甚至我们可以继续简化,用如下的估计式:



        E = Math.ceil ( -2 + t**0.2 + 1/(t**0.2) );

它在 E < 100 时都是完全可靠的,所以,也够我们用了。



五次方程求解的难度确实很高,这些天学了不少知识,但仍然没有获得一个通用的、100% 可靠的公式。我们前面得到的有关五次方程求解的表达式,也都是近似解,只能适用于锥号较小的情况。锥号如果增大,需要添加必要的修正项才能适应。


有鉴于求解五次方程的难度,刚才得到的简单公式,是非常理想的了,因为这个结果只需要一次求根运算,这甚至比求解三次方程的那个情况还要好。





作者: 不点    时间: 2022-10-4 22:41
思考一下着法的选择。

对全部着法进行评估,按照分值的高低,对所有的着法排序。排在最前的,一般也是最好的。但由于评分并不十分准确,所以,在选择着法的时候也要留有活动的余地。首先,要决定哪些着法必须舍去。下面是一个方案。

排在最前的(得分最高的),肯定要留下。它的分值,作为基础分值,来决定其它着法的取舍。然后,看第二名的分值,如果它与第一名的差额超过 20,就舍去,或者第二名的分值小于 -9000(快要被对方将死了的情况)也要舍去。只要有一个开始舍去,后面的着法当然也全都舍去了。如果第二名留下了,再看第三名。第三名与第二名的差值,不能大于 20,而且与基础分值的差额不能超过 40。也就是说,相邻两个分值的差额不超过 20,任何一个分值都不可以与基础分值的差额超过 40。不符合条件的,全都舍去。在留下的那些着法中,随机选取一个着法,来作为正式采用的着法。第一名被选中的概率应该是最高的,最后一名被选中的概率应该是最低的。

下面是一种概率设计方案。

倒数第一名,有 1 次机会被选中。
倒数第二名,有 8 次机会被选中。
倒数第三名,有 27 次机会被选中。
倒数第四名,有 64 次机会被选中。
倒数第五名,有 125 次机会被选中。

就这样,按照立方递增规律设计。相邻两个着法被选中的概率,有一定的差别,但差别不太大。





作者: 不点    时间: 2022-10-5 09:44
本帖最后由 不点 于 2022-10-6 21:28 编辑

思考一下兵的子力价值。

看到 AI 很容易弃掉自己的兵,因此,子力价值的问题,就是个不得不重新考虑的问题了。

子力价值(分值),应该分为 “基础潜力分” 和 “浮动分(奖惩分)”。通常是以奖励为主,惩罚的情况,不要太多。由于问题的复杂性,要精确设计这套逻辑,还是很困难的。

兵的潜力是能过河。过河以后,最大能力,能够控制 3 个点位。而马能控制 8 个点位。兵不能后退,而马可以满盘跑。所以,兵的基础价值,大约在马的价值的 2/8 到 3/8 之间,粗略就取 2.5/8 吧,也就是 5/16,或者也近似等于 1/3。未过河的兵,它的基础分也有这么多。如果马的基础分定为 100 分,兵的基础分至少也有 30 分那么多。当然,兵的步子属于小步,运动速度慢。马走一步到达某个位置,兵就需要 3 步才能到达那个位置。这是兵的弱点,是否应该扣分呢?这么说来,兵的基础价值再减少一些,就定为马的 1/4 吧。

中国象棋 in html5 的兵的基础价值,是马的 1/12,太少了。我调整到1/6, AI 仍然频繁弃兵。看来还得再提高兵的价值。这次就调整到马的 1/4,看看情况能否改善。



【补充】AI 弃兵的问题,也不一定是兵价值太低的原因。后来发现 AI 还会胡乱弃掉别的子力,例如,弃象、弃炮。



作者: 不点    时间: 2022-10-5 12:31
士相的价值,如果以控制点位个数来看,每个士相最多控制 4 个点位,比兵多一个。但士相不能过河,防守的点位又是固定不变的。似乎士相的价值比兵小。但在防守的时候,士相的价值比兵大很多。全士相可以抵抗单车的进攻。所以,士相的基础价值,大约为车的 1/4,为马的 1/2,为(未过河)兵的两倍。
作者: jinslaaa    时间: 2022-10-5 22:47
JavaScript很强
作者: 不点    时间: 2022-10-6 13:37
为了系数优化,昨天进行了如下测试,不断变动系数 c,使得达到最优。

for (let n = 1; n < 2400; n++) { let m = (n+4)*(n+3)*(n+2)*(n+1)*n ; let p=m+120; let c=5.4647371218604141596131285041337832808494567871093749999999999999 ; let x=(-2-n+(m**0.2)+1.0/(m**0.2))+1/(c*m**0.6); let y=(-2-n+(p**0.2)+1.0/(p**0.2))+1/(c*p**0.6); if (x >= 0 || y <= 0) console.log('出错的四维锥号 n = ' + n + ', ' + x + ', ' + y ) ;}


c 末尾 4999... 中 9 的个数无关紧要,但不可以舍入成 5000。也就是说,我已经触摸到了 js 的极限精度。打印到控制台的是出错的 n 值。第一个出错的 n = 2396。就是说,当 n < 2396 时,都是正确的。我们不需要用这个结果,但这是一个数学问题。


作者: 不点    时间: 2022-10-6 18:36
这个图,黑方弃炮打相,这不是一炮换双相,而是只换了一个相,肯定不划算。这说明局面评估函数有错误。


但跟踪代码,未发现问题出在哪里。那就只好放弃这个代码了。


接下来准备切换到一楼提到的 “象棋小巫师” 来学习。


弃炮打底相,局面评估混乱.png (428.8 KB, 下载次数: 8)

黑方弃炮打底相,局面评估函数有错误,但这个错误难以跟踪。

黑方弃炮打底相,局面评估函数有错误,但这个错误难以跟踪。

作者: 2010Athura    时间: 2022-10-6 19:10
谢谢楼主分享中国象棋
作者: 不点    时间: 2022-10-7 08:20
象棋小巫师对局面评估以后,对最佳评估的分值,还撒个随机数。这不科学。评估本身可能不准确,但撒随机数,这就是不合理的了。首先应该相信评估。撒随机数,就是不相信评估。其次,评估本身有局限性,不能做到 100% 准确。但这可以通过取前若干名“候选者”着法来确定。随机数应该撒在对那些 “较好的着法” 所作的选择上,而评估的分数,是一个重要的参考数据,能够决定该着法被选中的概率。但如果对评估的分数本身撒随机数,那就是添乱了,毫无道理可言。

下棋的时候,我发现局面对自己不利,然后悔棋。然而黑方往往会变招,而变招以后,对黑方不利了,黑方很快就输掉了。我怀疑,这就是胡乱撒随机数惹的祸。

作者: hexj68    时间: 2022-10-7 12:54

谢谢不点大师提供分享
作者: 不点    时间: 2022-10-7 21:48
DRAW_VALUE 是个常数,貌似是表示 “谁也不能赢” 的 “和棋” 情况。然而,这个常数设置为 20,处于正常的局面评分范围内,会干扰局面评估的处理过程。我认为这不划算,而且这增加了复杂性,让代码难以被别人看懂。

我尝试着把这个常数修改成 Infinity,貌似没有出现异常情况。如果我理解正确的话,这个常数仅仅是个信号,并无大小的区分,也不参与加减乘除运算。所以,把它设置成任何一个不会用到的数即可。比如,设置为 0.123456789 也行,或者 3.1415926 也行。我把它设置成一个极端的 “数” Infinity,就是想确认,这个 DRAW_VALUE 常数确实就是一个信号。同时,我把那些影响局面评分的相应代码也都删除了。如果这样做被证明是可行的、没问题的话,那么,新的代码是更加优化的。

作者: 不点    时间: 2022-10-7 22:11
象棋小巫师也会走弃兵的棋。就是白送一个卒给我吃,让我的兵白白过河。这说明局面评估的过程有错误。“象棋小巫师” 与 “中国象棋 in html5” 的子力价值表,竟然(几乎)完全一样。兵和士相的价值,给的都太少了。当然了,我不敢说给少了就不正确。说不定给少了是正确的。兵的蜗牛速度,比马差远了。从这个意义上来说,兵的价值确实不高。但无论如何,不该闹到随便弃兵的地步。兵的价值,不该少到让 AI 随便弃兵。
作者: 不点    时间: 2022-10-10 15:10
好的,抓住了象棋小巫师的一个错误。【抓错误的时候,都是用最强的专业水平来测试的。】

这个图,黑方跳马企图避免子力损失。但红方下一步是炮打底象绝杀。这说明 AI 看不到被杀的情况,因此,“局面评估” 或者 “着法搜索代码” 肯定有错。

AI不知道下一步要被杀.png (811.96 KB, 下载次数: 9)

黑方逃马,不知道下一步红方炮打底象闷宫

黑方逃马,不知道下一步红方炮打底象闷宫

作者: 不点    时间: 2022-10-11 20:58
今天找到一个好东西,开源的象棋打谱软件:

微思象棋播放器

https://www.xiaxiangqi.com/vschess/demo.html

希望我能学会它,然后就能制作出漂亮的棋谱了。

作者: 不点    时间: 2022-10-11 21:13
对 “象棋小巫师”,我做了两个修改:

1、去除撒随机数的代码,让程序总是选取最优的着法。
2、去掉了棋谱数据库 book_dat。

目的是尽量暴露出算法本身的毛病,以便能够改进算法。

做了这两项改动之后,发现我不那么容易打败它了!如果反复悔棋,总能找到它的弱点并打败它。但如果不悔棋的话,就不那么容易了。

所以,我猜测,撒随机数的代码,是一个漏洞,或者说就是一个错误。另一个猜测是,book_dat 数据库的处理过程中,可能有错,导致 AI 下出明显不正确的着法。

作者: 不点    时间: 2022-10-14 01:34
这个图,黑车守6路,导致速败。下一步,红车八平六,黑士6进5,红车三进二,成必杀之势,红胜。


黑车如改守4路,红方的攻势被化解,而黑方有过河卒,反而要赢。


这说明,AI 的局面评估有毛病,需要改进。

平车6路导致速败,应占据4路才对.png (475.08 KB, 下载次数: 5)

黑车应保4路士,而不是守6路。

黑车应保4路士,而不是守6路。

作者: 不点    时间: 2022-10-16 19:01
象棋小巫师,当我把电脑思考时间调高至 2000 毫秒时,它再也不会犯上一帖所说的错误了。

但它还会送卒给我吃,这样,它吃了卒的亏以后,还是容易输掉。

所以,兵卒价值给得太低,可能是一个实实在在的错误。猜测,把子力价值调整以后,它的棋力会有提高。

作者: 不点    时间: 2022-10-17 00:12
把未过河兵的价值从 7 提高到 21 以后,AI 在开局阶段,就用炮打我的兵。

只把兵的价值提高到 14,它也打我的兵,但打得不那么频繁了。

好像难以两全:兵的价值如果太低,AI 会胡乱弃兵。兵的价值太高,AI 又会在开局阶段就用炮打兵。

但兵的价值定为马的四分之一,我觉得还是合理的。兵过河的话,其价值应该就是马的二分之一了。

至于说开局阶段,炮随便打兵,这个问题可以再想别的办法来解决。


比如说,把棋局分为“开局”、“中局”、“残局”三个阶段,开局阶段,兵的价值设定为一个较低的值,中局以后,就把兵的价值恢复到正常值。同理,这三个阶段,马和炮的价值,也在发生变化,因此也应该适当调整马和炮的价值。



这些“价值”,都是一个粗略的概念,有一定的模糊性。而增加 “开局”、“中局”、“残局”的区别对待,则属于“动态微调”的改进尝试。






作者: 不点    时间: 2022-10-19 17:53
我昨天把 “象棋小巫师” 修改版上传到 18 楼了。是一个临时的测试版。我修改了啥?

1、(未过河)兵的价值从 7 提高到 21,过河兵的价值也有一些提升。

2、士相的价值也加倍。

我的修改,并不一定合理。

现在,AI 不再随便弃兵、弃士相了。但矫枉过正,现在它却乐意去吃对手的兵和士相。

我知道随便去贪吃对手的棋子不好,但是我竟然下不过它了!开局它就想办法吃我的兵。我让它吃,心想,它会很快输掉的。但我水平太差,很难赢它,即使赢,也是反复悔棋,等待它出错后才赢的。赢得十分艰难。

“象棋小巫师”,在基础逻辑方面,至今没有发现错误。这一点,比 “中国象棋 in html5” 强很多。

作者: 不点    时间: 2022-10-20 17:44
记录一下我对于 “绘制棋盘” 的思考。在 “象棋小巫师” 中,棋盘是一个背景图片。这个图片有几十 KB 那么大。因此,我考虑用 html、css 和 js 手段来实现棋盘。网上也能找到画棋盘的 css 代码,不过,并不特别理想。比如,用 linear-gradient 来画背景图,这 linear-gradient 就只支持较新的浏览器,所以,这会限制浏览器,是个缺点。再比如,用 canvas 来画图,这也是 html5 的新功能,不支持旧浏览器。

现在我正考虑一种完全兼容各种浏览器的方案:用 html 元素的可重叠性,来实现一个棋盘。

作者: 不点    时间: 2022-10-20 20:17
本帖最后由 不点 于 2022-10-21 13:29 编辑

记录一下备忘。这个知识很重要:

html 中给表格添加斜线
https://www.lmlphp.com/user/60160/article/item/614543/


有了这个知识,就可以画九宫中的米字格了。棋盘的其它部分,用 table 来实现。这样的话,兼容性杠杠的。更正:这个方案使用了 transform,这是浏览器新特性,不具有兼容性。


作者: 不点    时间: 2022-10-20 20:35
本帖最后由 不点 于 2022-10-21 13:26 编辑

这三个 unicode 字符,配合表格,也可以用来画斜线。

╱      U+2571
╲      U+2572
╳      U+2573

这种方法,说不定还更简单一些呢。当然了,字体的大小,不容易与表格完美匹配。而上一帖中的方法,应该算是一个终极的解决方案了,适应性较强。


更正:楼上的 transform 方案也是浏览器新特性,不具有兼容性。而本帖中的斜线字符,如果字体变大或变小,它的粗细也会相应变化。所以说这个方案,尽管不存在兼容性问题,也只能算是凑合。





作者: 不点    时间: 2022-10-22 12:58
为了画棋盘,折腾了好几天。

首先,上网搜索 “html 画斜线”,有一篇文章说,传统(旧)的 html 规范不支持画斜线。

于是,搜 “html 画三角形”。这次运气不错,有办法。利用 html 对于矩形角部边界的处理,画出三角形。就是说,不使用任何 html 新功能,只要使用 border 的这一特性,就可以画出三角形。

既然能画出三角形,也就能画出斜线。用一个小三角形把大三角形盖住,只留下斜线,这就成功了。但这需要用到 html 的 z-index。貌似这是被各种浏览器支持的。不过,上个世纪的浏览器是否支持 z-index,也不好说。目前就看这个 z-index 的兼容性如何了。


18 楼上传了最新的改动。有兴趣者,可以试试,看有哪些浏览器不支持这种 css 画棋盘的方法。


作者: 不点    时间: 2022-10-22 18:44
确实有兼容性问题。幸运的是,兼容性问题不是出在画斜线(三角形)的问题上,而是出在别的方面,比如,表格的特性。为了最大限度保持兼容性,下一步准备删除 table,而只用 div 来实现。

另外,这个象棋,运行在老机器上,由于机器运算能力弱,导致 AI 也很弱,没有多大的意思。所以,不打算支持 XP 以前的电脑。而在 XP 上,也不打算支持旧的 IE 6/7,而只打算支持最新的 IE 8。当然,火狐以及 chrome 都是要尽量给以支持的。

作者: 不点    时间: 2022-10-24 15:00
这两天尝试删除 table 而用 div 来实现。结果大失所望!div 和 span 竟然也出妖蛾子!

在 IE 8 中,div 的 inline-block 就跟 block 完全一样。而如果设置为 inline,则与 span 又完全一样了,其 width 的表现,与 firefox 中的表现是完全不一样的,因此竟然没法兼顾 firefox 和 IE 了!

尽管 table 也有妖娥子,但还可以挽救,通过努力、努力、再努力,找到了一个兼容 IE 8 和火狐的方案。所以,最终还是回到了 table 上。

新代码上载在 18 楼了。

作者: 不点    时间: 2022-10-28 10:39
本帖最后由 不点 于 2022-10-29 13:22 编辑

画棋盘的事还没完,其复杂程度,超乎想象。定义 border 的粗细,不同浏览器,结果不一样。而且,单元格同样都是那么宽,可是不同浏览器显示的实际宽度却有微调。这就麻烦了,单元格的宽度不一致,画出的网格线,有的粗,有的细。好不容易让网格线均匀了,但配合画三角形(斜线),又产生了新的问题。最后的结果是,用 table 也失败了!如果不计较粗细不均的现象,则很多方法都行,都算成功。但那该是多么 “低标准” 的成功啊!无异于失败。

最后,又回到了用 div 的路子上了,期望单一的 div 不会带来不兼容。

然而,想不到的是,单一 div 在不同浏览器下的表现也不同!ie 8 把 border 算在 width 和 height 以内,而火狐却把 border 放在 width 和 height 之外!应付这个差异,又得想办法!


2022-10-29:办法有了。只用 div 的上边框和左边框,不用下边框和右边框,这样就可以在各种浏览器下做到兼容(一致)了。目前这个方案,支持 ie8 和火狐,我猜很可能会支持所有 WinXP 以后开发出来的浏览器。WinXP 上的默认浏览器是 IE 6,应该也能支持。IE 5 有可能不支持,也有可能支持(估计现在大家很难有条件去验证了)。最新改动已经上载在 18 楼了。


作者: linux爱好者    时间: 2022-10-29 10:34
大师要开发象棋软件?
作者: 不点    时间: 2022-10-29 13:10
linux爱好者 发表于 2022-10-29 10:34
大师要开发象棋软件?

年岁高了,记忆力差,精力不旺盛,再加上本来只是一个电脑业余爱好者,也就搞不动操作系统之类的大玩艺了。更重要的是,心态变了,感觉整个世界面临危险,技术发展得越快,世界就毁灭得越快。因此,对那些所谓操作系统、CPU 等 “核心技术”,已经失去热情了。就让那些能够控制核武器的人去搞这些好了,我就吃瓜了,我就打酱油了。

不是开发象棋软件,是学习,是修补和完善,是做脑保健操,不让脑子彻底萎缩。

作者: 不点    时间: 2022-11-1 21:17
关于画棋盘,前面说,用 div 的上边框和左边框,避免使用 width 和 height,这样来保持兼容性。这个方案居然也失败了!问题出在哪儿呢?问题出在:画九宫里面的斜线时,直角边的粗细,在不同浏览器下不一样,导致九宫里面的横竖线与九宫外面的横竖线粗细不一样,很难看。于是,只好放弃使用边框,而改用画三角形这一个办法,来构成棋盘。这已经是逼上梁山、没有退路、只此一招了。下面这个图片,展示了这种画法。到目前为止,这是兼容性最好的方法了,在我测试的几个浏览器下,都成功了。


统一使用三角形,其好处是,算法一致,这样,“边框”的粗细能够保持恒定。“边框”就是两个相邻三角形之间留出的缝隙。不同浏览器下 “边框”的粗细是不同的。但在任意一个 “当前” 浏览器下,边框的粗细是一样的,这就基本算是完美解决了。

用三角形画棋盘.png (12.63 KB, 下载次数: 2)

统一使用三角形, 算法一致,兼容性强

统一使用三角形, 算法一致,兼容性强





欢迎光临 无忧启动论坛 (http://bbs.wuyou.net/) Powered by Discuz! X3.3