เอนจินหมากรุกมินิแมกซ์ 2-ply ที่สร้างด้วย Regular Expression 84,688 รายการ
(nicholas.carlini.com)- Regex Chess เป็นเอนจินมินิแมกซ์ 2-ply ที่เลือกตาเดินบนกระดานหมากรุกที่ถูกกติกาและ “ไม่ได้แย่จนเกินไป” โดยรันการแทนที่ด้วย Regular Expression 84,688 รายการตามลำดับเท่านั้น
- สถานะใส่ทั้ง สแตกและตัวแปร ไว้ด้วยกันในสตริงเดียว และจัดการคำสั่งอย่าง
push,pop, การค้นค่าและกำหนดค่าตัวแปรด้วยการแทนที่ Regular Expression - การแตกแขนงตามเงื่อนไขจำลองด้วยการเปลี่ยนมาร์กเกอร์
%%ที่หมายถึงสถานะที่กำลังทำงาน และด้วยการแทนที่แบบ global ทำให้สามารถ รันขนานแบบ SIMD เพื่อประมวลผลหลายสถานะพร้อมกันได้ - เอนจินสร้างตาที่ถูกกติกาทั้งหมดเป็นสถานะขนานและประเมินไปถึงการตอบโต้ของคู่แข่ง แต่ไม่ได้ทำการค้นหา depth-3 ที่ตรวจสอบความถูกกติกาของการตอบโต้ของคู่แข่งอย่างครบถ้วน
- ตอนแรกการตอบกลับหนึ่งตาใช้เวลาประมาณ 30 นาที แต่ด้วยการลบตัวแปรกลาง การปรับแต่งการแมตช์ คำสั่งเฉพาะทาง และการทำขนาน ทำให้เวอร์ชันสุดท้ายลดลงเหลือระดับ 1~10 วินาที แล้วแต่ตำแหน่ง
โครงสร้างพื้นฐานของ Regex Chess
- Regex Chess ประกอบเอนจินหมากรุกทั้งหมดจากรายการ Regular Expression 84,688 รายการ
- โครงสร้างการทำงานเรียบง่าย
- วนผ่านรายการ Regular Expression ตามลำดับ
- นำแต่ละแพตเทิร์นและสตริงแทนที่ไปใช้กับสตริงสถานะกระดานหมากรุกปัจจุบัน
- แสดงสถานะสุดท้ายบนหน้าจอ
- อินพุตไม่ใช่ PGN แต่เป็นรูปแบบพิกัดที่เขียน ช่องต้นทางและช่องปลายทาง ต่อกัน เช่น
e2e4 - โค้ดโปรเจกต์เปิดเผยไว้บน GitHub
CPU ที่เป็น Regular Expression
- แทนที่จะเขียนกติกาหมากรุกด้วย Regular Expression โดยตรง ผู้เขียนสร้างคอมพิวเตอร์ขนาดเล็กที่ทำงานด้วย Regular Expression ขึ้นมาก่อน
- คอมพิวเตอร์นี้ใช้ชุดคำสั่งที่มีการรันตามเงื่อนไขแบบไม่มี branch, การรันแบบคำสั่งเดียวหลายข้อมูล และการจัดการสแตกกับตัวแปร
- สถานะปัจจุบันแทนด้วยสตริงเดียว
%%แสดงจุดเริ่มของสถานะการทำงานที่ active- ใต้
#stack:จะสะสมรายการในสแตก - ตัวแปรถูกเก็บในรูป
#variable: value
-
การจัดการสแตกและตัวแปร
pushหาเฮดเดอร์%%\n#stack:\nแล้วแทรกค่าลงใต้เฮดเดอร์นั้น เพื่อวางไว้ที่ บนสุดของสแตกpopลบบรรทัดหนึ่งบรรทัดที่อยู่ใต้#stack:ทันที เพื่อนำค่าบนสุดของสแตกออก- การค้นค่าตัวแปรจะหา
#ชื่อตัวแปร:ในสตริงสถานะ คัดลอกค่า แล้วเพิ่มไว้บนสุดของสแตก - การกำหนดค่าตัวแปรรองรับทั้งกรณีที่มีตัวแปรอยู่แล้วและยังไม่มี
- หากมีอยู่แล้ว จะแทนที่ค่าตัวแปรเดิมด้วยค่าจากสแตก
- หากไม่มี จะเพิ่มบรรทัดตัวแปรใหม่
- ใช้มาร์กเกอร์ชั่วคราวอย่างแบ็กทิก เพื่อป้องกันไม่ให้คำสั่งเดิมถูกนำไปใช้อีกครั้งในลำดับที่ผิด
การรันตามเงื่อนไขแบบไม่มี branch และข้อจำกัดของลูป
- การรันตามเงื่อนไขทำผ่าน
cond(tag)และreactivate(tag) - หากค่าบนสุดของสแตกเป็น
Falseมาร์กเกอร์ active%%จะถูกเปลี่ยนเป็นรูป%tagทำให้คำสั่งถัดไปไม่ถูกนำไปใช้กับสถานะนั้น - ภายหลัง
reactivate(tag)จะเปลี่ยน%tagกลับเป็น%%เพื่อทำให้สถานะนั้น active อีกครั้ง - วิธีนี้จำลองการรันแบบมีเงื่อนไขโดยไม่ต้องมี branch ชัดเจน
- เนื่องจากโปรแกรมเป็นรายการ Regular Expression แบบเรียงลำดับ จึง ไม่สามารถรันลูปโดยตรงได้
- ไม่สามารถคำนวณแบบ Turing-complete ได้
- แต่การคำนวณที่มีขอบเขต เช่น การคำนวณตาถัดไปของหมากรุก สามารถคลี่ลูปออกมาจัดการได้
การรันขนานด้วยการแทนที่แบบ global
- เนื่องจากการแทนที่ Regular Expression ถูกนำไปใช้กับทั้งสตริงแบบ global หากใส่สถานะการทำงาน
%%หลายชุดไว้ในสตริงสถานะเดียว คำสั่งเดียวกันจะถูกนำไปใช้กับหลายสถานะพร้อมกัน - เช่น เมื่อมีจำนวนเต็มสองตัวอยู่ในแต่ละสแตกสองชุด การรัน
binary_add()จะทำให้การบวกทั้งสองรายการเกิดขึ้นพร้อมกัน fork_inactive(tag)คัดลอกสถานะ active ปัจจุบัน และปล่อยสำเนาไว้ในสถานะแท็กที่ inactivefork_bool(variable)แยกสถานะหนึ่งออกเป็นสองสถานะที่มีค่าTrueและFalse- โครงสร้างนี้ใช้เพื่อ ประเมินพร้อมกัน โดยไม่ต้องวนประมวลผลสถานะกระดานหลายแบบที่เป็นไปได้ในหมากรุกทีละรายการ
Macro Assembler และ Symbolic Execution
- เอนจินไม่ได้เขียนเฉพาะ Regular Expression ด้วยมือโดยตรง แต่ใช้ macro assembler ที่แปลงโปรแกรมสไตล์ Python เป็นลำดับคำสั่งขนาดเล็ก
- โค้ดสไตล์ Python อย่าง
fib()จะถูกแปลงเป็นรายการคำสั่ง เช่นpush,lookup,binary_add,assign_pop - แทนที่จะใช้ parser และ code generation แบบดั้งเดิม จะใช้ symbolic execution
- อ็อบเจ็กต์ตัวแปรไม่ใช่ dictionary จริง แต่เป็นอ็อบเจ็กต์พิเศษที่บันทึกการดำเนินการที่ถูกทำ
a = b + 1จะถูกบันทึกเป็นlookup('b'),push(1),binary_add(),assign_pop('a')
- คำสั่ง
ifจะบันทึกทั้งเส้นทางที่เงื่อนไขเป็นจริงและเป็นเท็จ แล้วจึงรวมกลับเข้าด้วยกัน- เมื่อพบการแตกแขนงตามเงื่อนไข จะสร้างสองเส้นทางใน call tree
- ระหว่างติดตามหลายรอบ จะเลือก branch ที่ถูกเยี่ยมน้อยกว่าเพื่อเติมเส้นทางทั้งสองฝั่ง
- จุดรวมถูกแปลงเป็นคำสั่ง
reactivate
วิธีสร้างตาเดินหมากรุก
- การเขียนเอนจินหมากรุกเองคล้ายกับเอนจินหมากรุกในภาษาทั่วไป แต่หัวใจสำคัญคือการประมวลผลหลายสถานะแบบขนาน
- การสร้างการเดินของพอนมีลำดับดังนี้
- หาตำแหน่งพอนสีขาวทั้งหมด
- สร้างสถานะขนานให้พอนแต่ละตัว
- หยุดสถานะหลักเดิมและเปิดใช้งานสถานะรายพอน
- ตรวจสอบตัวเลือกเดินหน้า 1 ช่อง เดินหน้า 2 ช่อง และกินแนวทแยงในทุกสถานะพอนพร้อมกัน
- รวมรายการตาผู้สมัครของแต่ละสถานะกลับเข้าด้วยกัน
- ตัวอย่างเพื่ออธิบายจัดการสตริง FEN โดยตรง แต่การใช้งานจริงขยายกระดานหมากรุกเป็น ตัวแปรแยกตาม 64 ช่อง เพื่ออ่านและเขียน
- ตัวเดินแบบเลื่อนไกลอย่างบิชอป เรือ และควีน รวมถึงการ castling และ en passant ก็ถูกติดตั้งแยกไว้ด้วย
- รายละเอียดการติดตั้งอยู่ใน chess_engine.py
การประมวลผลหนึ่งเทิร์นและการตรวจสอบตาเดิน
- หนึ่งเทิร์นดำเนินตามลำดับคือ อ่านตาเดินที่มนุษย์ป้อน ตรวจสอบว่าถูกกติกาหรือไม่ แล้วสร้างตาตอบของคอมพิวเตอร์
from_pretty_utf8_to_fen()แปลงการแสดงกระดานหมากรุกแบบ Unicode เป็นรูป FEN และดึงช่องต้นทางกับปลายทางของอินพุตออกมา- ความถูกกติกาของตาเดินมนุษย์ไม่ได้ตรวจด้วยโค้ดตรวจสอบกติกาแยกต่างหาก แต่ใช้วิธีสร้างกระดานถัดไปที่ถูกกติกาทั้งหมดแล้วเปรียบเทียบ
make_moveสร้างกระดานที่นำตาเดินอินพุตไปใช้compute_legal_boardsสร้างกระดานที่ถูกกติกาทั้งหมดที่เป็นไปได้จากตำแหน่งปัจจุบันfork_on_listแยกแต่ละกระดานเป็นสถานะขนาน- สถานะที่ต่างจากผลการใช้ตาเดินอินพุตจะถูกลบด้วย
destroy_active_thread()
- หากไม่มีตาที่ถูกกติกา เอาต์พุตทั้งหมดจะถูกเปลี่ยนเป็นข้อความฮาร์ดโค้ด
"Illegal Move" - ตาตอบของคอมพิวเตอร์สร้างกระดานตอบของฝ่ายดำที่เป็นไปได้พร้อมคะแนน แล้วเหลือเฉพาะกระดานที่มีคะแนนดีที่สุดด้วย
keep_best_scoring_board(score)
มินิแมกซ์ 2-ply และการลดรูปโดยตั้งใจ
compute_and_score_legal_boardsทำหน้าที่หลักของการค้นหา minimax depth-2- ขั้นแรกสร้างตาผู้สมัครที่คอมพิวเตอร์สามารถเดินได้ จากนั้นสร้างตาที่คู่แข่งตอบโต้ได้ เพื่อตรวจสอบว่าคิงอาจถูกกินได้หรือไม่
- เนื่องจากการตรวจสอบสถานะเช็กต้องสร้างการตอบโต้ของคู่แข่งด้วย โครงสร้างการค้นหา depth-2 จึงเกิดขึ้นแล้ว
- แต่ละตำแหน่งผู้สมัครถูกประเมินด้วยคะแนนเมื่อคู่แข่งตอบโต้ดีที่สุด
- ไม่ใช่มินิแมกซ์ depth-2 ที่สมบูรณ์
- ไม่ได้ตรวจสอบว่าตาตอบของคู่แข่งเป็นตาผิดกติกาที่ปล่อยให้คิงของตนเองอยู่ในเช็กหรือไม่
- หากจะจัดการให้ครบถ้วนต้องใช้การค้นหา depth-3 และมีต้นทุนเพิ่มขึ้นมาก
- ตาตอบที่คอมพิวเตอร์สร้างขึ้นเองไม่ใช่ตาผิดกติกา แต่บางกรณีอาจพิจารณาตาที่คู่แข่งเดินไม่ได้ ทำให้เลือกคำตอบที่อ่อนกว่าความเป็นจริง
การปรับแต่งประสิทธิภาพ
- เวอร์ชันเริ่มแรกใช้เวลาประมาณ 30 นาที เพื่อสร้างการตอบกลับต่อหนึ่งตาของมนุษย์
- เวอร์ชันสุดท้ายบนเครื่องของผู้เขียนใช้เวลาประมาณ 1~10 วินาที แล้วแต่ตำแหน่ง
- ความเร็วที่เพิ่มขึ้นประมาณ 100 เท่าเป็นผลจากการปรับแต่งหลายอย่าง
-
การลบตัวแปรกลาง
- การค้นค่าตัวแปรต้องค้นค่าตัวแปรจากสตริงสถานะทั้งหมด จึงมีต้นทุน O(n)
- เมื่อ fork สถานะการทำงาน ตัวแปรก็ถูกคัดลอกไปด้วย ทำให้ใช้หน่วยความจำมากขึ้น
- ลดเวลาและหน่วยความจำด้วยการลบตัวแปรที่ไม่จำเป็นแล้วอย่างจริงจังและนำชื่อตัวแปรกลับมาใช้ซ้ำ
- สถานะภายในของการประเมินหนึ่งตาลดลงจาก 10GB ก่อนปรับแต่ง เหลือประมาณ 300MB
-
การปรับแต่งการแมตช์ Regular Expression
- เพียงรวมอักขระขึ้นบรรทัดใหม่ด้านหน้าไว้ใน Regular Expression สำหรับเก็บกวาดของคำสั่งเงื่อนไข ก็ทำให้ประสิทธิภาพของคำสั่งนั้นดีขึ้นราว 2 เท่า
- เนื่องจากภายในสถานะมีสตริง
TrueและFalseจำนวนมาก จึงต้องจำกัดแพตเทิร์นให้หาค่าเฉพาะบนสุดของสแตกได้อย่างรวดเร็ว - ความต่างเล็ก ๆ ของแพตเทิร์นที่ลดการแมตช์ผู้สมัครที่ไม่จำเป็น ส่งผลต่อเวลารันโดยรวม
-
คำสั่งเฉพาะทางและการทำขนาน
- ส่วนที่ช้า เช่น ลูปหาตำแหน่งตัวหมาก ถูกรวมเป็นคำสั่ง Regular Expression เฉพาะหนึ่งคำสั่ง แทนการประกอบจากคำสั่งเดิมหลายคำสั่ง
- ในการสร้างการเดินของเรือ ไม่ได้ตรวจแต่ละทิศทางตามลำดับ แต่สร้างสถานะขนานหลายชุดเพื่อประมวลผลพร้อมกัน
- การประเมินตำแหน่งก็สร้างสถานะขนานให้แต่ละกระดานผู้สมัครและทำพร้อมกัน
- การรันขนานมีประสิทธิผลเป็นพิเศษกับงานที่ทำซ้ำการดำเนินการเดียวกันในหลายสถานะ เช่น การคำนวณมูลค่าตัวหมาก
การติดตั้งเพิ่มเติมที่อยู่ในซอร์ส
- ในซอร์สยังมีองค์ประกอบการติดตั้งที่ไม่ได้อธิบายลึกในบทความ
- การสร้างตาเดินแบบขนานของตัวเดินแบบเลื่อนไกลอย่างบิชอป เรือ และควีน
- ขั้นตอน “ช่องนี้ถูกโจมตีหรือไม่” สำหรับ castling
- การแปลงระหว่างกระดานหมากรุก FEN กับการแทนค่าแบบตัวแปรรายช่อง
- การตรวจจับสิทธิ์ castling ด้วยการติดตามตำแหน่งคิงและเรือ
- การตรวจจับและติดตาม en passant
- การทดสอบประมาณ 2000 บรรทัด ที่ตรวจสอบความถูกต้องของเอนจินตลอดหลายพันเกม
- บทความปิดท้ายด้วยข้อความว่าโปรเจกต์ไร้จุดประสงค์แบบนี้ทำให้ได้เรียนรู้หัวข้อวิทยาการคอมพิวเตอร์หลายเรื่องนอกสาขาของตนเอง
- ยังกล่าวถึงโปรเจกต์ที่เกี่ยวข้องอย่าง printf-tac-toe ที่เล่น tic-tac-toe ด้วย
printfของ C และ โคลน Doom ขนาด 13kB ด้วย JavaScript ด้วย
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
นี่เป็นผลงานของคนเดียวกับที่พิสูจน์ว่า printf() มีความสมบูรณ์แบบทัวริง และสร้างเกมยิงมุมมองบุคคลที่หนึ่งด้วย JavaScript ขนาด 13kB
https://github.com/HexHive/printbf
https://github.com/carlini/js13k2019-yet-another-doom-clone
ระยะเวลาทำให้เสร็จสำหรับการแข่งขันคือหนึ่งเดือน แต่ดูเหมือนว่าจะนำโค้ดเดิมกลับมาใช้ได้
งานนี้เองคงสนุกพอสมควร และตอนนี้รู้สึกเศร้านิดหน่อยที่ด้วยครอบครัวกับงาน คงไม่มีทางหา เวลาว่างหนึ่งเดือนเต็ม ไปทำอะไรแบบนี้ได้เลย
ณ จุดนี้รู้สึกว่างานนี้ก้าวข้ามความสนุกบ้าบอไปสู่ความไม่ธรรมดาอย่างแท้จริง การคำนวณตำแหน่งที่เป็นไปได้หลาย ๆ แบบเกิดขึ้นแบบ ขนาน ทั้งหมด โดยเป็นการรัน regex บนชุดสถานะและตัวแปรที่เพิ่มขึ้นเรื่อย ๆ หรือก็คือบนเธรด
“ตอนนี้คือส่วนที่ผมชอบที่สุดในภาษาที่เราสร้างขึ้นมา ด้วยเวทมนตร์ของ regex และข้อเท็จจริงที่ว่าเราทำการแทนที่แบบ global บนสตริงทั้งหมด เราจึงรันหลายเธรดพร้อมกันได้!”
บทสรุปก็ดีเช่นกัน “คุณคาดหวังอะไรจากบทสรุปของบทความบล็อกแบบนี้หรือ? ก็ไม่ได้มีบทสรุปอะไรเป็นพิเศษ ผมแค่อยากให้คนจำนวนมากขึ้นทำเรื่องไร้ประโยชน์อย่างสมบูรณ์แบบแบบนี้ มันสนุกจริง ๆ ไม่มีใครสนใจว่าจะใช้เวลานานแค่ไหนกว่าจะทำเสร็จ ไม่มีใครสนใจว่ามันจะทำงานได้หรือไม่ และแถมยังได้เรียนรู้เรื่องต่าง ๆ ในวิทยาการคอมพิวเตอร์นอกสาขาของตัวเองมากกว่าที่เคยอยากรู้เสียอีก”
เป็น ทัศนคติ ที่ยอดเยี่ยมจริง ๆ
สิ่งที่ได้จากตรงนี้คือพลังที่ทำให้รู้ว่า เมื่อเรานั่งลงและจดจ่อกับสิ่งเดียวแล้ว เราอาจไปได้ไกลแค่ไหนก็ไม่รู้ และในขณะเดียวกัน ผู้เขียนก็ชัดเจนว่าเป็นคนที่ มีพรสวรรค์ มีความชำนาญ และมีความคิดสร้างสรรค์อย่างมาก
ดูเหมือนจะมีบั๊กอยู่ที่ไหนสักแห่ง เกมด้านล่างนี้ไม่ใช่การเดินผิดกติกา แต่กลับจบด้วย “Illegal move, you lose”
Illegal Move
You Lose.
Game over.
FEN ของเกมข้างต้น:
1nbqkbnr/2pp1ppp/8/8/3QP3/P4N2/P1P2PPP/R1B1KB1R b KQk - 0 8
แต่ทั้งสองกรณีเป็นการเดินที่ถูกกติกา ดังนั้นเป็น บั๊ก แน่นอน
ผมไม่กลัวคนที่เล่นหมากรุกด้วย regex 84,688 ตัว แต่กลัวคนที่เล่นหมากรุกด้วย regex ตัวเดียว
ช่วยด้วย ผมกำลังโดนเนิร์ดสไนป์
ปัญหาที่อาจเกิดขึ้นและนึกออกทันทีคือ backreference
regex คงจะยาวมาก แต่ก็เท่ากับเข้ารหัสเอนจินหมากรุกทั้งตัวนั่นแหละ…
เห็นอะไรแบบนี้แล้วอยากถอดหมวก และกล่าวขอบคุณอย่างเคร่งขรึมต่อวีรบุรุษตัวจริงของมนุษยชาติ
บั๊กการเดินในไฟล์ a ได้รับการแก้ไขแล้ว: https://github.com/carlini/regex-chess/issues/1
ตัวอย่างก่อนหน้า: หมากรุกที่เขียนด้วย sed https://news.ycombinator.com/item?id=6261314
แน่นอนว่าเวอร์ชัน sed ใช้ คำสั่งควบคุมโฟลว์ ของ sed และน่าจะค้นหาไปข้างหน้าแค่ 1 ตา ดังนั้นจึงค่อนข้างต่างจากเวอร์ชันนี้ในจุดนั้น
นี่ไม่ใช่แค่เอนจินหมากรุกเท่านั้น แต่ยังเป็นการสร้าง คอมพิวเตอร์และภาษาแอสเซมบลี ด้วย regex เพียงอย่างเดียวด้วย
ปกติเริ่มด้วย a2a4 แล้วไม่ได้แพ้เร็วขนาดนี้นะ!
อันนี้ก็น่าเอามาเทียบดู: https://codegolf.stackexchange.com/q/3503/32575