4 คะแนน โดย GN⁺ 2025-01-08 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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 ปัจจุบัน และปล่อยสำเนาไว้ในสถานะแท็กที่ inactive
  • fork_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 ความคิดเห็น

 
GN⁺ 2025-01-08
ความคิดเห็นจาก Hacker News
  • นี่เป็นผลงานของคนเดียวกับที่พิสูจน์ว่า printf() มีความสมบูรณ์แบบทัวริง และสร้างเกมยิงมุมมองบุคคลที่หนึ่งด้วย JavaScript ขนาด 13kB
    https://github.com/HexHive/printbf
    https://github.com/carlini/js13k2019-yet-another-doom-clone

    • พอเห็นประโยคว่า “พิสูจน์ว่า printf() มีความสมบูรณ์แบบทัวริง และเกมยิงมุมมองบุคคลที่หนึ่ง …” พูดตรง ๆ ผมนึกว่ามันจะจบด้วยว่าเกมยิงนั้นก็ ทำด้วย printf เหมือนกัน
    • บันทึกการพัฒนา Doom น่าสนใจเพราะมีรายละเอียดเยอะ: https://nicholas.carlini.com/writing/2019/javascript-doom-cl...
      ระยะเวลาทำให้เสร็จสำหรับการแข่งขันคือหนึ่งเดือน แต่ดูเหมือนว่าจะนำโค้ดเดิมกลับมาใช้ได้
      งานนี้เองคงสนุกพอสมควร และตอนนี้รู้สึกเศร้านิดหน่อยที่ด้วยครอบครัวกับงาน คงไม่มีทางหา เวลาว่างหนึ่งเดือนเต็ม ไปทำอะไรแบบนี้ได้เลย
    • เห็นบอกว่า “เกมยิงมุมมองบุคคลที่หนึ่งด้วย JavaScript ขนาด 13kB” ก็เลยคาดหวังไว้ แต่พอรู้ว่าใช้ WebGL ในการเรนเดอร์กราฟิก ก็ผิดหวังนิดหน่อย
  • ณ จุดนี้รู้สึกว่างานนี้ก้าวข้ามความสนุกบ้าบอไปสู่ความไม่ธรรมดาอย่างแท้จริง การคำนวณตำแหน่งที่เป็นไปได้หลาย ๆ แบบเกิดขึ้นแบบ ขนาน ทั้งหมด โดยเป็นการรัน regex บนชุดสถานะและตัวแปรที่เพิ่มขึ้นเรื่อย ๆ หรือก็คือบนเธรด
    “ตอนนี้คือส่วนที่ผมชอบที่สุดในภาษาที่เราสร้างขึ้นมา ด้วยเวทมนตร์ของ regex และข้อเท็จจริงที่ว่าเราทำการแทนที่แบบ global บนสตริงทั้งหมด เราจึงรันหลายเธรดพร้อมกันได้!”
    บทสรุปก็ดีเช่นกัน “คุณคาดหวังอะไรจากบทสรุปของบทความบล็อกแบบนี้หรือ? ก็ไม่ได้มีบทสรุปอะไรเป็นพิเศษ ผมแค่อยากให้คนจำนวนมากขึ้นทำเรื่องไร้ประโยชน์อย่างสมบูรณ์แบบแบบนี้ มันสนุกจริง ๆ ไม่มีใครสนใจว่าจะใช้เวลานานแค่ไหนกว่าจะทำเสร็จ ไม่มีใครสนใจว่ามันจะทำงานได้หรือไม่ และแถมยังได้เรียนรู้เรื่องต่าง ๆ ในวิทยาการคอมพิวเตอร์นอกสาขาของตัวเองมากกว่าที่เคยอยากรู้เสียอีก”
    เป็น ทัศนคติ ที่ยอดเยี่ยมจริง ๆ

    • ถ้าปัดความกังวลอื่น ๆ ออกไปหมด ผมก็สงสัยเหมือนกันว่าตัวเองจะทำอะไรแบบนี้ได้ไหม
      สิ่งที่ได้จากตรงนี้คือพลังที่ทำให้รู้ว่า เมื่อเรานั่งลงและจดจ่อกับสิ่งเดียวแล้ว เราอาจไปได้ไกลแค่ไหนก็ไม่รู้ และในขณะเดียวกัน ผู้เขียนก็ชัดเจนว่าเป็นคนที่ มีพรสวรรค์ มีความชำนาญ และมีความคิดสร้างสรรค์อย่างมาก
  • ดูเหมือนจะมีบั๊กอยู่ที่ไหนสักแห่ง เกมด้านล่างนี้ไม่ใช่การเดินผิดกติกา แต่กลับจบด้วย “Illegal move, you lose”

    1. e2e4, e7e5
    2. d2d4, e5d4
    3. d1d4, a7a5
    4. g1f3, b7b5
    5. b1c3, a5a4
    6. c3b5, a4a3
    7. b5a3, a8a3
    8. b2a3 -->
      Illegal Move
      You Lose.
      Game over.
      FEN ของเกมข้างต้น:
      1nbqkbnr/2pp1ppp/8/8/3QP3/P4N2/P1P2PPP/R1B1KB1R b KQk - 0 8
    • ถ้าเดิน a2a3 เป็นตาแรก หรือเดิน a2a3 เป็นตาที่สองหลัง e2e4 ก็ขึ้นว่าเป็นการเดินผิดกติกา
      แต่ทั้งสองกรณีเป็นการเดินที่ถูกกติกา ดังนั้นเป็น บั๊ก แน่นอน
    • แค่เดิน a2a4 เป็นตาแรกก็เกิดเรื่องเดียวกัน
  • ผมไม่กลัวคนที่เล่นหมากรุกด้วย regex 84,688 ตัว แต่กลัวคนที่เล่นหมากรุกด้วย regex ตัวเดียว

    • ถ้ามีฮิวริสติกทั่วไปในการทำ regex ที่ถูกนำไปใช้ตามลำดับให้เป็นเชิงเส้น หรือก็คือวิธีรวม 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 แล้วไม่ได้แพ้เร็วขนาดนี้นะ!

    • ผมก็เจอบั๊กคล้าย ๆ กันในไฟล์ a เหมือนกัน เส้นทางคือ e2e4-d2d4-d1d4-g1f3-f1b5-d4e5-e5c5-e1g1-b2a3
  • อันนี้ก็น่าเอามาเทียบดู: https://codegolf.stackexchange.com/q/3503/32575

    • กฎการตัดสิน การหารลงตัว ทั้งหมดโดยเนื้อแท้แล้วเป็นภาษาปกติ จึงไม่ใช่เรื่องน่าประหลาดใจนัก ถ้าซับซ้อนกว่านี้ ก็จะไม่ใช่ “กฎ” ที่มนุษย์ทำตามได้แล้ว