[Learning Ruby 2] Minesweeper

กลับมาที่ Learning Ruby บ้างครับ โปรแกรมที่สองที่ผมหัดเขียนใน Ruby คือ Minesweeper ครับ ผมบังเอิญอ่านเจอใน reddit สักที่ถึงกฎของ minesweeper เลยมีความรู้สึกอยากเขียนเหมือนกัน จนวันต่อมาคุยกับพ่อแล้วพ่อก็แนะนำเกมนี้พอดีก็เลยเหมาะเหมง ได้เวลาเขียน

**Disclaimer:** Entry นี้เขียนขณะผมกำลังเรียนรู้ Ruby ข้อมูลในนี้คือสิ่งที่ผมเข้าใจในขณะที่กำลังเรียน ไม่จำเป็นว่าเป็นวิธีที่ถูกต้องที่สุดหรือเหมาะสม

อธิบายถึงกฎของ Minesweeper ก่อนครับ น่าจะทราบดีแล้วว่าเกมจะมีระเบิดซ่อนในกระดานอยู่ และมีตัวเลขบอกว่ารอบๆ ช่องนั้นมีระเบิดกี่ช่อง แต่ที่อาจจะไม่ทราบ

– Minesweeper ที่เขียนถูกต้อง กดครั้งแรกต้องไม่โดนระเบิดเสมอ (ในเครื่องเล่น MP4 อันแรกของผมกดแล้วโดนระเบิดได้นะ)
– เลขในช่องสูงสุดคือ 8 ฉะนั้นแสดงว่าต้องนับระเบิดในแนวเฉียงด้วย
– เมื่อคลิกแล้วพบว่าเลขในช่องเป็น 0 ให้คลิกทุกช่องรอบๆ ช่องนั้นด้วย นั่นคือทำไมกดแล้วกระดานเปิดออกมากว้างมากในบางครั้ง

เกมนี้ใช้ Curses เหมือนเดิมครับ คราวนี้เริ่มชินกับ Curses แล้ว

จากโค้ดจะเห็นว่าผมแบ่งเป็น 2 ส่วนนะครับ คือบรรทัด 14-33 เป็นหน้าตั้งค่า ใช้ puts, gets ตามปกติ โดยจะใช้ while loop เช็คว่าผู้ใช้ป้อนถูกต้องหรือไม่ ถ้าไม่ถูกต้องจะต้องป้อนใหม่จนกว่าจะถูก

และในส่วนที่สองคือบรรทัดที่ 35 เป็นต้นไปจะเป็นส่วนของเกมครับ มีฟังค์ชั่นคือ

– `gen_bomb` จะทำการสร้างระเบิด โดยจะเก็บเป็น (x,y) ของระเบิด และไม่วางระเบิดในช่องปัจจุบัน ฟังก์ชั่นนี้ใช้การสุ่มวางแล้วเช็คช่องเอา ไม่ใช้การไล่ช่องแล้วสุ่ม true/false เอา (วิธีหลังนี้จะเร็วกว่าหลายเท่า) ก็เพราะว่าวิธีหลังระเบิดจะกระจุกตัวอยู่ตามด้านบนครับ
– `open_tile` จะเปิดช่องที่ส่งเข้ามาครับ โดยถ้าเจอช่องที่เป็น 0 คะแนนปุ๊บจะเรียกตัวเอง (recursive function) เปิดช่องรอบๆ อัตโนมัติ ข้อสังเกตคือต้องมีการเก็บช่องที่เปิดแล้วนะครับ ไม่อย่างนั้น recursive function จะทำงานไม่รู้จบได้
– `winterface` เป็นหน้าแสดงเวลาแพ้หรือชนะ (Win + interface)

ระบบเกมนี้ไม่ใช่ Tick แล้วครับ เพราะไม่มีกระบวนการเบื้องหลัง แต่จำเป็นต้องใช้ non-blocking input อยู่ดีเพราะว่าต้องอ่านค่าจนหมด line buffer ถึงจะรู้ว่าเป็นตัวอะไร

สิ่งที่ได้จากโปรแกรมนี้:

– การเขียน block ใช้เอง (`do .. done`) ใน `get_until_cond` คือฟังก์ชั่นสามารถรับ block ได้ โดย block จะเป็นชนิด `Proc` เมื่อต้องการจะเรียกให้ฟังค์ชั่นที่ส่งมาเป็น block ทำงาน ให้ใช้คำสั่ง `yield` (สามารถมี argument ได้ ซึ่งจะส่งต่อไปให้ใน block)
– `Proc` ไม่ใช่ฟังก์ชั่น ไม่สามารถ `return` ได้ไม่อย่างนั้นจะ error เพราะไม่รู้ return ให้ใคร (อ่านใน docs ประมาณว่า `Proc` มัน bind scope ไว้ ทำให้ return ไม่ได้)
– Function ทุกตัว รวมถึง Proc จะ return ผลลัพท์ของคำสั่งสุดท้ายใน Function นั้นเสมอ (และนี่คือวิธีเดียวในการ return จาก Proc)
– การเรียก Function ที่ใส่ \* (เรียกว่า splat) หน้า argument ตัวแรกและตัวเดียว คือการเอาค่าใน argument แต่ละตัวใส่เป็นแต่ละ argument ของฟังก์ชั่น (argument นั้นต้องเป็น array) ตัวอย่างเช่น ถ้า `x=[1,2]` การเรียก `setpos *x` มีค่าเท่ากับการเรียก `setpos 1,2` — พฤติกรรมนี้คล้ายๆ กับ \*args ของ Python
– Function บางตัว โดยเฉพาะใน `String` จะมีทั้งรูปปกติ และมี ! ต่อท้าย แบบหลังคือ in-place หรืออธิบายได้ว่า `x.rstrip!` ได้ผลเหมือนกับ `x=x.rstrip`
– ใน for จะข้ามไปลูปต่อไป ใช้ว่า `next` ไม่ใช่ `continue` เหมือนภาษาอื่น
– สุ่มเลขใช้ว่า `rand` ตอนหลังผมมาทราบว่า `rand` รับ argument 1 ตัวคือค่าสูงสุด (exclusive) เช่น `rand 5` จะได้คำตอบในช่วง {x|x∈I⁺∧x∈[0,5)} (0-4 นั่นแหละ)

ที่เกมยังทำไม่ได้คือกระดานขนาดใหญ่มากๆ ระเบิดน้อยมากๆ อาจจะติด maximum recursion ได้ และน่าจะทำคือพอ `winterface` แล้วให้แสดงระเบิดด้วย ซึ่งผมขี้เกียจเขียนเพราะตอนนั้นเขียนแบบ ssh ไปหาเครื่องอื่น ซึ่งมันขยายจอ nano ไม่ได้ ก๊อปวางก็ไม่สะดวก

ตอนต่อไปใน Learning Ruby … ผมก็ยังไม่รู้เหมือนกันครับ เขียนไว้สองโปรแกรมเอง

[Learning Ruby] เกมฆ่าตัวเลข

เปิดซีรีส์ที่สองไปพร้อมกันครับกับ Learning Ruby บันทึกประสบการณ์การเรียน Ruby ของผม

**หมายเหตุ:** Entry นี้เขียนตอนผมกำลังเรียน Ruby อยู่ มันคือสิ่งที่ผมคิด ผมทำในขณะที่เรียน Ruby มันอาจจะไม่ใช่วิธีที่ถูก ข้อมูลที่ถูกในการเขียน Ruby ที่ดีเสมอไปครับ

ที่เรียนรูบี้เนี่ยเพราะพ่อแนะนำมาครับ พ่อนั่งดูสมัครงานหลายที่แล้วก็พบว่ามันมีแต่ Ruby. Python มีน้อยมาก ผมก็อะเรียน Ruby ก็ได้

ย้อนกลับไปสมัยขึ้นม. 2 ตอนนั้นที่เลือก Python คือผมเล็งเรียนภาษาใหม่อยู่ครับ ตอนนั้นมี Python (Django) กับ Ruby (on Rails) ที่ผมกำลังตัดสินใจเลือก ผมเลือก Python ด้วยเหตุผลนึงคือพ่อเคยซื้อหนังสือมา (แต่พ่อผมเขียนเป็นแต่ VB นะครับ) แล้วก็อีกเหตุผลนึงคือการบังคับ indentation ของ Python ผมรู้สึกว่ามันเป็นอะไรที่เป็นนวัตกรรมมาก และจะได้แก้โรคเขียนโค๊ดยุบรวมบรรทัดเดียวของผมไปด้วย (เคยขุดโค้ดสมัยม. 1 ใน PHP มาอ่าน ผมยังอ่านแล้วไม่เข้าใจเลย)

ผมจำจากประสบการณ์ Python ว่าตอนนั้นผมจับ web.py ก่อน เป็น framework เล็กๆ ความสามารถหลายตัว แต่ผมใช้มันเป็นแค่ URL Router เฉยๆ เขียนไปหลายงาน รวมถึงงานลูกค้าด้วย จนกระทั่งม. 4 ผมถึงลอง Django จากงานที่ครูอยากให้ทำคือทำ[โปรแกรมข้อสอบ](https://github.com/whs/Quiz) ตอนหัด Python ผมเคยลองไต่ Django แล้วสไลด์ตกกลับมาแบบงงๆ มันดูเวทย์มนต์เกินไป ฉะนั้นใน Ruby ผมจะไม่พยายามไล่ Rails แต่ให้เข้าใจภาษามันก่อน

ทีนี้ผมเริ่มเรียนจากอ่านหนังสือครับ ปกติใครถามผมจะไม่ค่อยแนะนำหนังสือโปรแกรมมิ่งให้อ่านเพราะพอเป็นแล้วไปอ่านมันจะรู้สึกว่าหนังสือเขียนไม่ดี แต่พอมาเรียนเองแล้วก็พบว่าหนังสือนี่จำเป็น แต่อ่านแค่ให้เข้าใจโครงสร้างของมันพอ แล้วเปิด docs ประกอบเวลาอยากจะทำอะไร ถ้าให้เทียบเหมือนเรียนภาษาคงเหมือนเรียน grammar มันก่อนว่า x no y คือ x ของ y แล้วผมอยากบอกว่ากระเป๋าของคุณสมชาย ผมก็เปิดดิกท์เจอว่ากระเป๋าคือ kaban ก็ kaban no somchai-san จบ

เล่มที่ผมดูผ่านๆ ก็มีหลายเล่มครับ มี [Ruby Koans](http://rubykoans.com) อันนึงที่ทำไปเล่นๆ พอตอบคำถามได้ แต่ก็ยังไม่รู้สึกว่าคืบหน้า พ่อแนะนำให้หาหนังสือ ผมก็เลยค้นๆ ดูหลายเล่มจนกระทั่งไปเจอ [Beginning Ruby](http://www.apress.com/9781590597668)

ครั้นจะไปซื้อมาเรียนมันธรรมดาไปครับ เราต้องยืนอ่านหน้าร้านก่อนสิว่าหนังสือมันดีแค่ไหน มันก็มี Google Books ให้กดดูได้ครับจากในลิงก์เมื่อกี้นี้

ด้วยความใจดีของ Google ครับ บทที่ 3 ทั้งบทอ่านได้ฟรี บทที่ 4 ก็ด้วย ผมอ่านสองบทนี้จบปุ๊บผมก็บรรลุ syntax Ruby แล้ว (ไม่เห็นต้องซื้อ ;p)

คำถามต่อมาผมก็ถามว่า เรียนแล้วไง จะเขียนอะไร พ่อก็แนะนำให้ลองเอางานลูกค้ามา reimplement แต่ผมบอกแล้วว่าผมจะไม่ไต่ rails จนกว่าจะเข้าใจ ruby พ่อเลยแนะนำว่าเขียนเกมฆ่าตัวเลขดู

เกมฆ่าตัวเลข ตอนแรกผมเข้าใจว่าจะมีตัวเลขวิ่งมาให้กด counter ให้เป็นเลขนั้นแล้วกด ok เพื่อลบออกไปก่อนที่เลขนั้นจะชนขอบจอ หลังๆ เลขจะมีหลายตัวพร้อมกัน ไอ้นี่ผมก็ไม่แน่ใจว่าเขียนยังไง แต่พอถามไปถามมาพ่อบอกว่ามันคือเกมที่จะมีตัวเลขออกมา 1 ตัว ให้เราปรับ counter เป็นเลขตัวนั้น แล้วเลขตัวนั้นจะหายไป แต่ทุกๆ เวลานึงเลขจะเพิ่มขึ้นทีละตัว เกมก็จะยากขึ้น (โดยที่ถ้ามีเลข 7 อยู่ 3 ตัวบนกระดาน กด counter เป็นเลข 7 แล้วกด enter ปุ๊บเลข 7 ทั้ง 3 ตัวจะหายไปจากเกมทันที)

ทีนี้ความยากเริ่มต้นอยู่ที่การวาด UI ครับ ผมเคยเขียนเกม​ Skifree clone บน Java ทีนึงตอนหัด Java ก็เลยได้ผ่านตากับ curses​ ซึ่งเป็นไลบรารีในการวาดจอ ผมก็เลยลองมาดูบ้างว่า curses ใน ruby มีมั้ย ปรากฎว่ามันมีครับอยู่ในตัวเลย ชื่อมอดูล [Curses](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/curses/rdoc/Curses.html) ผมเห็นวิธีการใช้ใน docs คือ

~~~~
require ‘curses’
include Curses
~~~~

โอเค ไม่ยาก ผมศึกษาจาก example ใน docs สักพักก็พอเข้าใจว่า เวลา include อะไรเข้าไปแล้วมันจะไปรวมกับ global namespace สามารถเรียกใช้ได้เลย ไม่ต้องสั่ง `Curses.บลาๆ` ทุกบรรทัด สักพักก็ได้มาประมาณนี้ครับ

ผมใช้ Tick ประมวลผล ซึ่งอันนี้ผมเอาไอเดียมาจากสมัยเขียน Skifree คือผมสามารถปรับความเร็วเกมได้โดยการปรับ tick speed โดยตรง โดย tick คือความเร็วการประมวลผลของเกม (เหมือนสัญญาณนาฬิกา อะไรทำนองนั้นครับ)

สิ่งที่ผมเรียนจากโปรแกรมนี้คือ

– การใช้ curses
– ใส่ $ นำหน้าตัวแปร คือ global variable, ชื่อตัวแปรตัวแรกตัวใหญ่คือค่าคงที่ ชื่อคลาสทั้งหมดต้องเป็นค่าคงที่ ฉะนั้นคลาสจะต้องตัวแรกตัวใหญ่เสมอ
– keycode ของปุ่มขึ้น/ลง ไม่ใช่ตัวเลขตัวเดียว ใช้ `Curses.KEY_UP` ไม่ได้ เอาจริงๆ แล้วมันจะเป็น string `\e[A` (สามตัว) ฉะนั้นต้องดักโค้ดให้หมด input ก่อนแล้วค่อยประมวลผลว่ามันคืออะไร
– join ruby สวยมาก คือ `str.join “joiner”` ได้เลย ใน python มันใช้ `”joiner”.join(str)` โคตรน่าเกลียด
– `Array#select` คือคำสั่งแนวๆ filter (ในชุด map reduce filter นั่นแหละครับ) โดยใส่ฟังก์ชั่นด้วยบล็อค do หรือ { .. } ก็ได้ (จริงๆ เรื่องบล็อคในเล่มตะกี้มีเขียนอธิบายละเอียดแล้วครับ)
– ฟังก์ชั่นบางตัวที่มี = ต่อท้าย จะเป็น setter การใช้ต้องใช้รูปเต็มเท่านั้นคือ `Curses.timeout= 5` วรรคแบบอื่นจากนี้ไม่ได้ (ผมคิดว่าข้อมูลนี้ผิดนะ แต่จากการทดลองผมได้ประมาณนี้)
– วัดความยาว array ก็ `Array#length` เลย ไม่ต้อง `len()` ให้งง
– จะลบจาก array ก็ `Array#delete` จะลบสิ่งที่กำหนดออกจาก array ทั้งหมดไม่ว่าจะปรากฎกี่ครั้ง
– เวลาวาดจอใน Curses แล้วให้สั่ง `refresh` จะ flush buffer

ก็ เกมฆ่าตัวเลขก็ประมาณนี้ครับ โพสต์ต่อไปมากับเกมที่ยากขึ้นกว่านี้หน่อย รอดูต่อไปนะครับ!