2 คะแนน โดย GN⁺ 2024-02-22 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • โมดูล planner ของ Picat ก้าวไปอีกขั้นจาก logic programming ที่ค้นหาการกำหนดค่าให้ตัวแปร โดยให้เรานิยามปัญหาเป็น ลำดับการเปลี่ยนสถานะ เพื่อไปยังสถานะเป้าหมาย
  • เมื่อกำหนดสถานะเริ่มต้น Start, กฎการเปลี่ยนผ่าน action(From, To, Action, Cost) และเงื่อนไขสิ้นสุด final(S) แล้ว best_plan(Start, Plan) จะค้นหา แผนที่มีต้นทุนต่ำสุด ไปจนถึงสถานะสุดท้ายให้
  • ในตัวอย่างการหาเส้นทางบนกริด สามารถจัดการการเคลื่อนที่ ขอบเขตกริด การหลบสิ่งกีดขวาง และการเยี่ยมหลายเป้าหมายได้เพียงเปลี่ยน action กับ final และยังเลือกได้ว่าจะตรึงลำดับการเยี่ยมเป้าหมายหรือปล่อยให้ยืดหยุ่น
  • ฟีเจอร์วางแผนผสานกับ constraint solving ทำให้สามารถแสดงปัญหาอย่าง partition problem เช่น การหา sublist ที่ใหญ่ที่สุดซึ่งได้จากการลบองค์ประกอบออกบางตัวและยังแบ่งเป็นสองส่วนที่มีผลรวมเท่ากันได้
  • แม้ Picat จะเป็นภาษาเพื่อการวิจัยที่เอกสารและข้อความผิดพลาดยังมีไม่มาก แต่ในฐานะ ภาษาเชิงเครื่องมือ สำหรับแก้ปัญหาการคำนวณบางประเภทอย่างรวดเร็ว ก็ให้วิธีแก้ที่กระชับกว่าภาษาทั่วไปได้

แนวคิดพื้นฐานของ Picat และ Planner Programming

  • Picat เป็นภาษาเพื่อการวิจัยที่พยายามผสาน logic programming, imperative programming และ constraint solving เข้าด้วยกัน
  • การเขียนโปรแกรมแบบ imperative และ functional ทั่วไปคือการเขียนอัลกอริทึมที่สร้างเอาต์พุตจากอินพุต แต่ logic programming และ constraint solving จะค้นหา การกำหนดค่าให้ตัวแปร ที่ทำให้ความสัมพันธ์เป็นจริง
  • ใน Picat ตัวระบุที่ไม่ใช่ฟังก์ชันและขึ้นต้นด้วยตัวพิมพ์เล็ก เช่น a, b, c คือ atom ส่วนตัวระบุที่ขึ้นต้นด้วยตัวพิมพ์ใหญ่คือ variable
  • แม้ในนิพจน์อย่าง member(Y, Arr) ที่มีตัวแปร Y ซึ่งยังไม่ได้ถูกกำหนด Picat ก็สามารถค้นหาและกำหนดค่าที่ทำให้นิพจน์เป็นจริงได้
    • ถ้า Arr = [a, b, c, a] แล้ว Y อาจเป็นหนึ่งใน a, b, c
    • จากนั้นหากใส่เงื่อนไขอย่าง X != Y ค่าที่เป็นไปได้ก็จะแคบลง
    • แม้ในกรณีอย่าง member(a, Z) ที่ยังไม่รู้ตัว list เอง ก็สามารถ instantiate Z ให้เป็น list ได้

การวางแผนค้นหาการเปลี่ยนสถานะ ไม่ใช่การกำหนดค่าให้ตัวแปร

  • Planning คือการค้นหา ลำดับการเปลี่ยนตัวแปร ที่ไปถึงสถานะสิ้นสุดหนึ่ง ๆ แทนที่จะค้นหาค่าตัวแปรที่ทำให้สมการเป็นจริง
  • ปัญหา planning ใน Picat ต้องมีองค์ประกอบสามอย่าง
    • สถานะเริ่มต้น Start
    • ฟังก์ชัน action ที่แทนการเปลี่ยนสถานะ
    • final(S) ที่ตัดสินว่าสถานะนั้นเป็นสถานะสิ้นสุดหรือไม่
  • ฟังก์ชัน action ของ Picat ทุกตัวต้องมีชื่อว่า action และรับพารามิเตอร์สี่ตัว
    • สถานะปัจจุบัน
    • สถานะถัดไป
    • ชื่อ action
    • ต้นทุน
  • best_plan(Start, Plan) จะกำหนด แผนที่ใช้จำนวนขั้นน้อยที่สุดหรือมีต้นทุนต่ำสุด ที่จำเป็นต่อการไปถึงสถานะสิ้นสุดให้กับ Plan
    • ถ้าตั้งต้นทุนทั้งหมดเป็น 1 ต้นทุนของแผนจะเท่ากับจำนวนก้าวการเคลื่อนที่รวม
    • หากต้องการแค่แผนใดก็ได้โดยไม่สนใจความยาว สามารถใช้ plan(Start, Plan) ได้

ตัวอย่างการหาเส้นทางบนกริด

  • ปัญหาตัวอย่างคือ marker บนกริดเริ่มจากจุดกำเนิด (0, 0) แล้วไปถึงพิกัดเป้าหมาย
    • ในแต่ละขั้นสามารถขยับขึ้น ลง ซ้าย ขวา ได้ทีละช่อง
    • ห้ามออกนอกขอบเขตกริด
    • เมื่อถึงพิกัดเป้าหมายถือว่าสำเร็จ
  • สถานะเริ่มต้นเก็บตำแหน่งปัจจุบันและเป้าหมายไว้ด้วยกัน เช่น {Origin, Goal}
    • {a, b} ใน Picat เป็นไวยากรณ์ของ array และโดยพื้นฐานใช้เหมือน tuple
  • สามารถเขียนเงื่อนไขสิ้นสุดด้วย pattern matching ได้ เช่น final({Pos, Goal}) => Pos = Goal.
    • หากจะเขียนสิ่งเดียวกันโดยไม่ใช้ pattern matching ต้องแยกสถานะออกเป็น {Pos, Goal} ก่อน
    • ถ้ามีเงื่อนไข final หลายข้อ แผนจะสำเร็จเมื่อมีข้อใดข้อหนึ่งเป็นจริง
  • action การเคลื่อนที่จะเลือกหนึ่งในสี่ทิศทาง {-1,0}, {1,0}, {0,-1}, {0,1} และตรวจว่าพิกัดใหม่อยู่ในช่วง 0..10 หรือไม่
    • member({Dx, Dy}, Dir) ใช้เพื่อหาค่าทิศทางที่เป็นไปได้
    • member(Tx, 0..10) และ member(Ty, 0..10) ใช้เพื่อตรวจว่าพิกัดอยู่ภายในขอบเขตหรือไม่
    • predicate สำหรับตรวจอย่างเดียวโดยไม่กำหนดค่าให้ตัวแปรก็มี membchk ด้วย
  • แผนผลลัพธ์จะถูกแสดงเป็น list ของ action การเคลื่อนที่และพิกัดใหม่ เช่น {move,{1,0}}, {move,{2,0}}
    • สามารถใช้สคริปต์ Raku เพื่อแสดงเส้นทางเป็นภาพได้
    • หากเพิ่มเงื่อนไขอย่าง {Tx, Ty} != {2, 1} ก็สามารถทำ การหลบสิ่งกีดขวาง เพื่อเลี่ยงพิกัดเฉพาะได้

หลายเป้าหมายและการลดต้นทุนให้น้อยที่สุด

  • หากต้องเยี่ยมหลายเป้าหมาย ให้เปลี่ยน Goal จากพิกัดเดียวเป็น คิวเป้าหมาย เช่น [{2, 2}, {3, 4}]
  • เพิ่ม action ใหม่ที่จะลบรายการนั้นออกจาก list เป้าหมายเมื่อไปถึงเป้าหมาย
    • [Head|Tail] แบ่ง list เป็นองค์ประกอบแรกกับส่วนที่เหลือ
    • Goal = [Pos|Rest] จะเป็นจริงเฉพาะเมื่อสถานะปัจจุบัน Pos เท่ากับรายการแรกใน list เป้าหมาย
    • หากตั้งสถานะใหม่เป็น {Pos, Rest} เป้าหมายที่ไปถึงแล้วจะถูกลบออก
  • ตรวจว่าเยี่ยมครบทุกเป้าหมายหรือไม่ด้วย final({Pos, Goal}) => Goal = [].
    • เงื่อนไขสิ้นสุดจะไม่ใช่ว่าตำแหน่งปัจจุบันเท่ากับเป้าหมายใดเป้าหมายหนึ่งหรือไม่ แต่เป็นว่า list เป้าหมายว่างแล้วหรือไม่
  • วิธีเยี่ยมเป้าหมายตามลำดับที่กำหนดไม่ได้สร้างเส้นทางสั้นที่สุดโดยรวมเสมอไป
  • หากต้องการไม่สนใจลำดับเป้าหมายและลดเส้นทางรวมให้น้อยที่สุด ให้เปลี่ยน action mark
    • แทนที่จะใช้ Goal = [Pos|Rest] ให้ใช้ member(Pos, Goal) เพื่อตรวจว่าตำแหน่งปัจจุบันอยู่ที่ใดก็ตามใน list เป้าหมาย
    • ใช้ To = {Pos, delete(Goal, Pos)} เพื่อลบเป้าหมายที่เยี่ยมแล้วออกจาก list
    • ด้วยวิธีนี้ Picat สามารถเลือกเป้าหมายถัดไปที่จะไปและลด ความยาวเส้นทางรวม ให้น้อยที่สุดได้

การผสาน planning กับ constraint solving

  • ฟีเจอร์ planning ของ Picat ผสานกับฟีเจอร์อื่น ๆ ของ Picat และสามารถใช้ planning ร่วมกับ constraint solving ได้
  • partition problem ที่ยกเป็นตัวอย่างคือปัญหา NP-complete ในการแบ่ง list ของตัวเลขออกเป็นสองกลุ่มที่มีผลรวมเท่ากัน
  • โปรแกรมนี้ลบองค์ประกอบออกจาก list ตัวเลขเพื่อหา list ย่อยที่ใหญ่ที่สุดซึ่งสามารถแบ่งเป็นสองกลุ่มที่มีผลรวมเท่ากันได้
    • ให้การลบองค์ประกอบออกจาก list ตัวเลขเป็น action ของแผน
    • final(Numbers) ตรวจว่า list ตัวเลขนั้นมีคำตอบ partition ที่ถูกต้องหรือไม่
    • ใช้ constraint จากโมดูล cp แทนว่าแต่ละองค์ประกอบอยู่ในกลุ่มซ้ายหรือขวาด้วยตัวแปร 0..1
    • ใส่ constraint ให้ผลรวมทั้งหมดเท่ากับสองเท่าของผลรวมกลุ่มหนึ่ง
  • ในผลลัพธ์ตัวอย่าง หลังจากลบ [5,17] แล้ว list ที่เหลือสามารถแบ่งเป็นสองกลุ่มที่แต่ละกลุ่มมีผลรวม 1108
    • 32+99+977=1108
    • 122+77+86+59+47+154+141+172+49+62+109+30=1108
  • วิธีนี้ไม่ได้หยุดแค่การแก้ constraint ที่ถูกต้องโดยตรง แต่ยังแสดง การเปลี่ยนแปลงเพื่อไปถึงสถานะ constraint ที่ถูกต้อง ในรูปแบบแผนด้วย

ข้อจำกัดและกรณีใช้งานที่เหมาะกับ Picat

  • Picat เป็นภาษาเพื่อการวิจัย จึงไม่แนะนำให้ใช้ใน production
  • ฟีเจอร์อำนวยความสะดวกมีไม่มาก และยังขาดเอกสารที่ดีหรือข้อความผิดพลาดที่ชัดเจน
    • เมื่อไม่มีแผนที่แก้ได้ ข้อผิดพลาดจะแสดงประมาณ *** error(failed,main/0)
  • การที่รันบน Windows ได้ถือว่าดีกว่าภาษาเพื่อการวิจัยจำนวนมาก
  • Picat ใกล้เคียงกับ ภาษาแบบ toolkit สำหรับแก้ปัญหาการคำนวณบางประเภท มากกว่าจะเป็นภาษาสำหรับเขียนโค้ดที่ต้องบำรุงรักษาหรือแชร์ต่อ
  • ปัญหาบางอย่างที่จัดการด้วยภาษาโปรแกรมทั่วไปและ constraint solver ได้ยาก สามารถแก้ด้วย Picat ได้ค่อนข้างงดงาม

ภาษา planning อื่น ๆ และแนวคิดที่เกี่ยวข้อง

  • Planning เดิมถูกบุกเบิกใน robotics และ AI แต่ปัจจุบันมักใช้ใน AI ของวิดีโอเกมในชื่อ Goal Oriented Action Planning(GOAP)
  • PDDL เป็นภาษาอธิบายแผนที่ planner อิสระรับเป็นอินพุต
    • มีบทบาทคล้ายกับที่ DIMACS ใช้เป็นรูปแบบคำอธิบายใน SAT

1 ความคิดเห็น

 
GN⁺ 2024-02-22
ความคิดเห็นใน Hacker News
  • เคยลองใช้ โหมด planner ของ Picat ในงานจริง
    ได้ทำต้นแบบระบบสำหรับประสานงานการบำรุงรักษาชุดอุปกรณ์หลายชุด โดยเป็นวิธีที่ป้อนว่า “ต้องการอะไร” “มีการกระทำอะไรที่เป็นไปได้บ้าง” และ “มีข้อจำกัดอะไรที่ต้องรักษาไว้” แทนที่จะบอกว่า “ต้องทำอย่างไร”
    กับตัวอย่างเล็ก ๆ มันสร้างแผนที่เหมาะที่สุดได้ดี แต่พอขยายไปถึงขนาดใช้งานจริงก็พังลงตามคาด และปัญหาการวางแผนก็เป็น EXPTIME อยู่ดี เลยมีข้อจำกัดมาก
    Picat มีทางหนีทีไล่ให้กำหนด heuristic ได้ จึงลองให้ random forest และ naive Bayes classifier บน state predicate มาช่วยทำนายเส้นทางที่มีแววดี แต่ถึงจะใส่ทั้งข้อจำกัดแบบ symmetry breaking และ hierarchical planning ก็ยังต้องลงแรงเยอะเกินไป
    ดูเหมือนว่าในโดเมนปัญหา GOFAI แบบดั้งเดิม ยังมี ฤดูหนาว AI หลงเหลืออยู่
    แต่อาจไม่ใช่ ถ้าเปลี่ยนโจทย์การวางแผนใหม่ให้กลายเป็นการคืนค่า “ตัวสร้างแบบเวลาพหุนามที่สามารถสร้างแผนซึ่งอาจยาวแบบชี้กำลังได้”

    • ตัวแก้เชิงพาณิชย์นั้นเหนือกว่าโอเพนซอร์สทั้งในด้าน ประสิทธิภาพ และด้านการทำให้กระเป๋าตังค์เบาลงอย่างรวดเร็ว
      นึกถึง CPLEX, Xpress, GUROBI, Hexaly และ Hexaly เด่นเป็นพิเศษกับงานวางตารางหรือปัญหาเส้นทางรถ
      ปกติแล้วจะเข้าถึงผ่าน API ของภาษาที่นิยมใช้ในอุตสาหกรรม ซึ่งดูสมเหตุสมผลกว่าภาษาเฉพาะของตัวแก้ที่อ่อนกับงานทั่วไปมาก
      การเรียก GUROBI จาก Python นั้นง่ายมาก และยังใช้ความสามารถทั่วไปของ Python ได้ครบ
      Mosek ถูกกว่า GUROBI มาก แต่ API ของทั้งคู่ก็ค่อนข้าง low-level มาก และประสิทธิภาพก็ยังสู้ GUROBI ไม่ได้
    • ในกรณีนี้ อะไรอย่าง CP-SAT(https://developers.google.com/optimization/cp) อาจขยายได้ดี
      ตัวแก้นี้จัดการตัวแปรและข้อจำกัดจำนวนมหาศาลแบบเหลือเชื่อได้สบาย และ heuristic ในตัวก็ยอดเยี่ยม
    • planner รุ่นก่อน ๆ ใส่ meta-rule หรือ heuristic สำหรับตัดสินใจว่าจะใช้กฎไหน เพื่อลดพื้นที่ค้นหา
      บางระบบแยกปัญหาออกเป็นหลาย representation แล้วต่อเข้ากับตัวแก้อัตโนมัติที่เชี่ยวชาญเฉพาะด้าน
      นึกถึง Jahob Analysis System กับ Cyc
      ในแง่การใช้งานจริง สถาปัตยกรรมที่ดูลงตัวที่สุดใน AI แบบดั้งเดิมคือ Procedural Reasoning System และอยากเห็นเวอร์ชันที่สร้างใหม่โดยเอาวิธีสมัยใหม่มาอุดจุดอ่อนของมัน
      https://en.wikipedia.org/wiki/Procedural_reasoning_system
    • สงสัยว่าทำไมถึงไม่มี ตัวแก้โอเพนซอร์ส คุณภาพสูง
      ในงานคำนวณเชิงตัวเลขสาขาอื่น เทคโนโลยีล้ำ ๆ มักเป็นโอเพนซอร์ส แต่ทำไมสาย optimization ถึงต่างออกไปก็เป็นเรื่องที่สงสัยมาตลอด
    • ถ้ามองจากผลลัพธ์หรือประสิทธิภาพ มันอาจไม่ได้ต่างจาก depth-first search ที่เขียนแบบคร่าว ๆ โดยนึกถึง Prolog ลาง ๆ มากนักก็ได้
  • มีผู้ใช้ HN ที่แอ็กทีฟในชุมชน constraint programming ชื่อ hakank หรือ Hakan Kjellerstrand ซึ่งมีเว็บรวมเอกสารและตัวอย่างของ Picat ไว้จำนวนมาก: http://www.hakank.org/picat/

  • ตามปกติแล้วก็อยากแนะนำ Prolog
    มันสวย เข้าใจง่าย และมีความเป็นผู้ใหญ่มากกว่า ถ้าต้องการแก้ constraint finite domain แค่ของที่มีมาให้ก็เพียงพอแล้ว
    นอกจากนี้ MiniZinc ยังเป็นอินเทอร์เฟซชั้นเยี่ยมสำหรับเข้าถึงตัวแก้หลายตัวที่เหมาะกับวัตถุประสงค์ต่างกัน ดังนั้นถ้าไม่ใช่ผู้เชี่ยวชาญ ก็มักมีโอกาสเข้าใกล้ผลลัพธ์ที่ต้องการมากกว่า
    Prolog มีข้อดีมากมาย แต่ถ้าจะให้ได้ประสิทธิภาพดี “สัญชาตญาณเชิงกลไก” ที่ต้องใช้ก็อาจเพิ่มขึ้นมากอย่างรวดเร็ว
    ถ้าเคยเขียนอะไรด้วย Picat ก็น่าลองคิดดูว่าของเดียวกันนี้จะเขียนในภาษาอื่นอย่างไร
    ปัญหาเล่น ๆ แบบนี้ในภาษาอื่นก็ง่ายเหมือนกัน และในภาษาเชิงฟังก์ชันส่วนใหญ่ Dijkstra หรือ A* ใช้ไม่กี่บรรทัดก็พอ สุดท้ายหลายครั้งสิ่งสำคัญทั้งหมดก็คือการนิยาม state space ของอัลกอริทึมค้นหา

  • ดีใจที่เห็น Predrag เป็นผู้รีวิว แต่ก็ไม่แปลกใจเลย
    Firebase technical screen น่าจะง่ายขึ้นมากถ้ามีเครื่องมือแบบนี้ และสุดท้ายมันก็เป็น ปัญหา optimization อีกแบบหนึ่ง
    เลยอยากลองกลับไปแก้มันด้วย Picat ดูอีกครั้ง
    เขายังทำเรื่องน่าสนใจในสายภาษาโปรแกรมด้วย: https://github.com/obi1kenobi/trustfall

  • สิ่งแรกที่นึกได้คือ “เหมือน ระบบชนิดข้อมูล ที่ต้องแก้เองเลย”
    ลองพยายามแทนมันแบบ naïve ใน TypeScript แต่ a, b, c สามารถเป็นค่าเดียวกันทั้งหมดได้ เลยแก้อะไรไม่ได้เลย
    พอพยายามแทนให้ถูกต้อง ก็ไปได้ถึงระดับที่ดูพอใช้งานได้บ้าง แต่ก็ยังต้องใช้ assertion และยังนิยามชนิดของ Y ไม่ได้อย่างถูกต้อง
    กระบวนการนี้กลับยิ่งทำให้เห็นว่าโปรแกรมสไตล์ planner ทรงพลังแค่ไหนในแง่ความเรียบง่ายและความกระชับ
    ดูเหมือน TypeScript จะยังไม่ทรงพอสำหรับการแสดง constraint ประเภทนี้
    ลิงก์ TS Playground ที่ลองไว้: http://tinyurl.com/3p2pzdtn

  • ดีใจที่เห็น GOAP ถูกพูดถึงอีกครั้ง
    มันคือซอสลับที่ทำให้ศัตรูใน F.E.A.R. สนุกขนาดนั้น และบทความของ Jeff Orkin ที่อธิบายวิธีทำงานก็อ่านง่ายและสนุกด้วย

  • ใช้ Prolog กับ CLPFD บางส่วนในงานจริงอยู่ และมันดีมาก
    อยากให้มันมีอยู่ทุกที่ หรือพูดให้ชัดกว่านั้นคืออยากให้มีแกนตรรกะที่เน้นความบริสุทธิ์ แล้วผลักพฤติกรรมเชิงคำสั่งออกไปไว้ที่ขอบ
    น่าเสียดายที่วงการเหมือนติดอยู่กับเครื่องมือแย่ ๆ แบบนี้

  • ดูคล้าย Prolog และน่าสนใจ

    • Picat ดูเหมือนเป็นทายาทสายตรงของ B-Prolog และแชร์สำนวนใช้งานหลายอย่างร่วมกัน เช่น tabling: https://en.wikipedia.org/wiki/B-Prolog
  • รูปแบบการเขียนโปรแกรมแบบนี้ไม่ได้ใหม่เสียทีเดียว
    ตอนมหาวิทยาลัยเคยเรียน Prolog และมันก็ดูคล้ายกันมาก แต่ Prolog ไม่มีฟังก์ชัน planner
    แต่ planner เป็นวิธีแก้ปัญหาที่ทั้งสง่างามและเรียบง่ายมาก
    พอเห็นส่วนท้ายของบทความที่พูดถึงวิดีโอเกมก็ยิ่งสงสัย
    ฟังก์ชัน planner ช่วยให้แก้ปัญหาได้ง่ายมากด้วยโค้ดชัดเจนไม่กี่บรรทัด แต่เมื่อเทียบกับอัลกอริทึมที่เขียนแบบ imperative แล้ว ประสิทธิภาพจะเป็นอย่างไร?
    Picat ดูเหมือนมีประสิทธิภาพพอตัวเมื่อเทียบกับภาษาคล้ายกัน [1] แต่ยังหาเทียบกับภาษา “มาตรฐาน” ไม่เจอ
    [1]: https://arxiv.org/abs/1405.2538

  • ผมเองก็มีความฝันที่จะบอกคอมพิวเตอร์แค่ว่าต้องไปถึงสถานะแบบไหน
    ไม่ค่อยรู้จักชุมชน planning หรือตัวแก้มากนัก แค่เคยลองจับ ortools แบบ naïve แต่ก็เคยลองสร้างโค้ดที่พาจากสถานะหนึ่งไปยังสถานะเป้าหมายด้วย A*
    มันสร้างคำสั่งแอสเซมบลีสำหรับเคลื่อนย้ายระหว่างสถานะ และยังหาการเปลี่ยนสถานะแฝงจากการเรียกฟังก์ชันเพื่อไปให้ถึงเป้าหมายด้วย
    เพื่อให้ค้นหาเร็วขึ้นก็รันแบบขนานด้วย Python multiprocessing และใช้การสร้างเพื่อนบ้านแบบไดนามิกเพราะแต่ละเธรดสร้างเพื่อนบ้านไม่เหมือนกัน
    ในความพยายามเดิม A* ขนานได้ยากเลยต้องทำ sharding
    ความฝันจากการทดลองของผมคือ แค่บอกคอมพิวเตอร์ว่า “ตอนนี้มีอะไรอยู่” กับ “อยากได้อะไร” แล้วมันจะหาเส้นทางการย้ายที่ถูกต้องได้เอง
    ส่วนตัวมองว่าการเขียนโปรแกรมคล้าย โลจิสติกส์ แบบ Factorio หรือโรงงานมากกว่า
    เลยเรียกมันว่า “sliding puzzle” เพราะมันคือปริศนาที่ต้องขยับของไปมาเพื่อให้เห็นภาพที่ถูกต้อง
    GitHub repository และบันทึก: https://github.com/samsquire/sliding-puzzle-codegen-memory
    Replit: https://replit.com/@Chronological/SlidingPuzzle3