หัดเขียนโปรแกรมแบบบ้านๆ ตอนที่ 6 ตัวอย่างโปรแกรม

บทความก่อนหน้า

พักเบรก & ซ้อมมือ

หลังจากเนื้อหายาวๆ แล้วก็ลองหัดเขียนโค้ดจากความรู้ที่ผ่านมาดูบ้างครับ

ตัวอย่าง 1 หาลำดับ Fibonacci ของ n

เลขฟีโบนัชชีเป็นตัวเลขมหัศจรรย์ที่พบได้ในสิ่งต่างๆ ตามธรรมชาติตัวอย่างที่มักจะยกมาให้เห็นก็คือวงก้นหอย โดยที่ค่าฟีโบนัชชีของจำนวนลำดับที่ n ( Fn ) จะมีค่าเท่ากับผลรวมของสองลำดับก่อนหน้า ( Fn-1 + Fn-2 ) โดยที่ F1 = 1 และ F0 = 0 การหาค่าฟีโบนัชชีสามารถทำได้หลายวิธีตั้งแต่การใช้สูตรคณิตศาสตร์ การคำนวณสมการด้วยเมตริกซ์เป็นต้น แต่เราจะใช้วิธีง่ายๆ สองแบบโดยใช้ความรู้จากตอนก่อนๆ คือการวนลูป และการเรียกตัวเองของฟังก์ชัน จากในตอนที่ 4 เรามีตัวอย่างการหาค่าแฟกทอเรียลมาแล้วซึ่งก็จะใช้วิธีคล้ายกัน

แบบใช้ลูป:


function fib1(n)
  if n<0 then
    return nil                       -- กรณีที่ค่า n น้อยกว่า 0 คืนค่าเป็น nil
  elseif n<2 then
    return n                         -- ถ้า n เป็น 0 หรือ 1 คืนค่าเป็น n
  else
    local a, b, c = 0, 1             -- ให้ a = 0 และ b = 1
    for i = 2, n do                  -- วนรอบ 2 ถึง n
      c = a + b                      -- c คือค่า fibonacci ที่ลำดับ i; a คือลำดับ i-2 และ b คือลำดับ i-1
      a, b = b, c                    -- a เก็บค่า b และ b เก็บค่า c
    end
    return c
  end
end
หรืออาจใช้ table ร่วมก็ได้

function fib2(n)
  local fib = {[0]=0,1,1}
  if n < 0 then
    return nil                       -- กรณีที่ค่า n น้อยกว่า 0 คืนค่าเป็น nil
  elseif n>2 then
    for i = 3, n do
      fib[i] = fib[i-1] + fib[i-2]
    end
  end
  return fib[n]                      -- แบบนี้สามารถคืนค่าได้ทั้งลำดับจาก 0 ถึง n ได้
  -- return fib                         คืนค่า table ที่เก็บลำดับ fibonacci
  -- return table.concat(fib,",")       คืนค่าเป็น string ที่แสดงลำดับ 0 ถึง n
end

print(fib2(5))                       -- ได้ 5 ( fib = {[1]=1,[2]=1,[3]=2,[4]=3,[5]=5,[0]=0} )

แบบ Recursive:


function fib3(n)
  local fib = {[0]=0,1,1}
  if n>=0 and n<=2 then
    return fib[n]                    -- ถ้า fib[0] ได้ 0 fib[1] หรือ fib[2] ได้ 1
  elseif n>2 then
    return fib3(n-1) + fib3(n-2)     -- เรียกฟังก์ชันแบบ recursive
  else
    return nil                       -- กรณีที่ค่า n น้อยกว่า 0 คืนค่าเป็น nil
  end
end

--[[
fib3(5) =            fib3(4)       +    fib3(3)
        =     fib3(3)    + fib3(2) + fib3(2)+fib3(1)
        = fib3(2)+fib3(1)+   1     +    1   +  1
        =     1  +   1   +   1     +        2
        =            3             +        2
        =                         5
]]

แบบ Tail recursive:

เป็นรูปแบบหนึ่งของ recursion ต่างจากการเรียกซ้ำปกติคือฟังก์ชันจะไม่เอาฟังก์ชันที่เรียกซ้ำไปประมวลผลต่อ แต่จะประมวลผลและส่งค่าไปประมวลผลในฟังก์ชันที่เรียกซ้ำ ดังนั้นในการส่งคืนค่ากลับจะไม่มีการประมวลผลอีกทำให้ลดการใช้หน่วยความจำช่วยไม่ให้เกิด overflow กรณีที่มีการเรียกซ้ำหลายรอบซึ่งเป็นคุณสมบัติหนึ่งที่ภาษา Lua รองรับ ( บางภาษาที่ไม่รองรับ Tail call optimization ก็สามารถเขียนแบบนี้ได้แต่ไม่ได้มีการคืนหน่วยความจำจึงเกิดปัญหาไม่ต่างจากเรียกซ้ำแบบปกติ ) จากตัวอย่างข้างบนค่าที่ return กลับจาก fib3() ต้องเอามาบวกต่ออีกถึงจะได้คำตอบแต่ฟังก์ชั่นแบบ tail recursive พอเรียก tail รอบสุดท้ายก็จะได้ผลลัพธ์เลยดังตัวอย่าง

-- Tail recursion function
function fibtail(n,n2,n1)            -- รับค่า n กำหนดจำนวนรอบ
  if n>3 then                        -- ในแต่ละรอบจะเอา n2+n1 เป็นค่า fibonacci ในแต่ละรอบ
    return fibtail(n-1,n1,n2+n1)     -- เรียกตัวเองโดยลดค่า n ในแต่ละรอบและส่งค่า fibonacci ไปด้วย
  else
    return n2+n1                     -- รอบสุดท้ายคืนค่า n2+n1
  end
end

-- Main function
function fib4(n)
  if n==0 then
    return 0
  elseif n<=2 then
    return 1
  if n>2 then
  	return fibtail(n,1,1)        -- เรียก tail recursion function
  else
  	return nil
  end
end

--[[
fib4(5) = fibtail(5,1,1) <-- ()
        = fibtail(4,1,2) <-- (1+1)
        = fibtail(3,2,3) <-- (1+2)
        = 5              <-- (2+3)
]]

ตัวอย่าง 2 สับไพ่

By Reference กับ By Value:
ปกติการส่งค่าให้ตัวแปรจะมีสองแบบคือส่งเป็นค่าของข้อมูลได้แก่ตัวแปรประเภท nil, boolean, number และ string เช่น a = 10; b = a; a = 20 แล้ว b จะเท่ากับ 10 และ a เท่ากับ 20 เพราะเป็นการเอาค่าเดิมใน a คือ 10 ไปใส่ใน b ดังนั้น a และ b ไม่มีความสัมพันธ์กันเป็นข้อมูลคนละตัว
ส่วนประเภทข้อมูลที่เหลือจะส่งเป็นค่าอ้างอิงไปยังข้อมูลนั้น เช่น a = {1}; b = a; b[2] = 2 แล้ว a และ b จะเท่ากับ {1,2} เพราะ a และ b เก็บตำแหน่งของ table เดียวกัน ดังนั้นเพื่อป้องกันการเปลี่ยนแปลงข้อมูลตั้งต้นจึงต้องสร้างข้อมูลใหม่ที่มีค่าเหมือนกับข้อมูลตั้งต้นเพื่อใช้งานแทน
เช่นการใช้ { table.unpack(...) } สำหรับ copy table ที่มีคีย์เป็นเลขลำดับ

local startDesk = { -- S=Spades, H=Hearts, D=Diamonds, C=Clubs
  "S-2","S-3","S-4","S-5","S-6","S-7","S-8","S-9","S-10","S-J","S-Q","S-K","S-A",
  "H-2","H-3","H-4","H-5","H-6","H-7","H-8","H-9","H-10","H-J","H-Q","H-K","H-A",
  "D-2","D-3","D-4","D-5","D-6","D-7","D-8","D-9","D-10","D-J","D-Q","D-K","D-A",
  "C-2","C-3","C-4","C-5","C-6","C-7","C-8","C-9","C-10","C-J","C-Q","C-K","C-A",
}

function shuffle(desk)
  local newDesk = {table.unpack(desk)}   -- copy สำรับใหม่
  for index = #desk, 2, -1 do            -- วนรอบเรียงจากท้ายสำรับ
    local newIndex = math.random(index)  -- สุ่มลำดับใหม่จาก 1 ถึง index
    -- สลับตำแหน่ง index กับ newIndex
    newDesk[index], newDesk[newIndex] = newDesk[newIndex], newDesk[index]
  end
  return newDesk
end

local desk2 = shuffle(startDesk)
print "index startDesk desk2"
for i=1,5 do
  print(i, startDesk[i], desk2[i])
end

-- ผลลัพธ์
--[[
index startDesk desk2
1	S-2	C-7
2	S-3	S-3
3	S-4	D-8
4	S-5	D-7
5	S-6	H-7
]]

ตัวอย่าง 3 เกม Tic-Tac-Toe

เป็นเกม OX อย่างง่ายสลับกันเล่นระหว่างผู้เล่นสองคน เขียนแบบ Procedural ง่ายๆ ทดสอบการสร้าง/ใช้งานฟังก์ชัน การใช้งาน table การตรวจสอบเงื่อนไข การวนลูป การรับค่าคีย์บอร์ด และการแสดงผล

-- เตรียมข้อมูล
local board = {}                  -- เก็บตาเดินที่แสดงในกระดาน
local turn = {}                   -- เก็บตาเดินของ O หรือ X
local mapb = "----+---+----"      -- เส้นตาราง
local mapm = "  %s | %s | %s  "   -- ช่องตาราง
local message = {}                -- ข้อความแสดงผล
local Omoved, Xmoved = {}, {}     -- เก็บตาเดินของ O และ X
local endGame                     -- สถานะเกมจบ ( true, false)
local winCase = {                 -- เก็บตาเดินที่สามารถจบเกมได้ 8 กรณี
  {1,2,3},                        -- [1] แถวบน
  {4,5,6},                        -- [2] แถวกลาง
  {7,8,9},                        -- [3] แถวล่าง
  {1,4,7},                        -- [4] คอลัมน์ซ้าย
  {2,5,8},                        -- [5] คอลัมน์กลาง
  {3,6,9},                        -- [6] คอลัมน์ขวา
  {1,5,9},                        -- [7] แนวทแยง
  {3,5,7}                         -- [8] แนวทแยง
}

--- แสดงกระดาน OX และข้อความของเกม
function showBoard()
  local space = "          "
  print("\n             เกม Tic-Tac-Toe\n")
                                  -- string.format(mapm,ค่าที่1,ค่าที่2,ค่าที่3)
                                  -- เอาค่าที่ 1,2,3 มาแทนที่ %s ใน mapm
  print(mapm:format(1,2,3),space, mapm:format(board[1],board[2],board[3]))
  print(mapb,space,mapb)          -- พิมพ์เส้นกระดาน
  print(mapm:format(4,5,6),space, mapm:format(board[4],board[5],board[6]))
  print(mapb,space,mapb)
  print(mapm:format(7,8,9),space, mapm:format(board[7],board[8],board[9]))
  print("\n".. message.info1 .. message.info2:format(turn.n,turn.player))
end

--- กำหนดค่าเริ่มต้นของเกมแต่ละรอบ
function initData()
  for i = 1, 9 do
    board[i] = " "                -- ตั้งค่าเป็นกระดานเปล่า
    Omoved[i] = false             -- ล้างตาเดินของ O เป็น false หมดตั้งแต่ช่องที่ 1 - 9
    Xmoved[i] = false             -- ล้างตาเดินของ X เป็น false หมดตั้งแต่ช่องที่ 1 - 9
  end
  endGame = false
  turn.player = "O"               -- เริ่มด้วยตาเดินของ O
  turn.n = 1
  message.info1 = ""
  message.info2 = "          ตาเดินที่ %d โปรดใส่ตำแหน่งของ %s "
  showBoard()
end

--- ตรวจสอบผู้ชนะ
-- @param moved รับค่า table Omoved หรือ Xmoved
function checkWin(moved)
  message.info1 = ""
  for i=1, #winCase do            -- วนรอบดึงค่า table ย่อยใน winCase มาเทียบกับ moved
    if moved[winCase[i][1]] and moved[winCase[i][2]] and moved[winCase[i][3]] then
      message.info1 = "                   "..turn.player.." ชนะ\n"
      endGame = true
      break                       -- ถ้าตำแหน่งในกระดานทั้ง 3 ค่าใน winCase[i] มีใน moved ( moved[winCase[i]]==true ) ให้แสดง
    end                           -- ข้อความฝ่ายที่เล่นในตานั้นเป็นผู้ชนะ ให้สถานะจบเกม ( endGame ) และออกจากลูป
  end
  if not endGame then             -- ถ้ายังไม่มีผู้ชนะ ( ยังไม่จบเกม ) ให้เพิ่มตาเดิน ( turn.n )
    turn.n = turn.n+1
    if turn.n==10 then            -- ถ้าเป็นตาที่ 10 แล้วให้แสดงข้อความเสมอ
      message.info1 = "                   เสมอ\n"
      endGame = true              -- ให้สถานะจบเกม
    end
  end
end

------------- Main Program --------------
initData()                        -- เริ่มเกมโดยกำหนดค่าตั้งต้น
while true do                     -- วนลูปไปเรื่อยๆ ไม่จบ
  local pos = io.read()           -- รับค่าจากคีย์บอร์ดเก็บใน pos
  if pos:lower()=="q" then        -- ถ้าค่าเป็น "q" หรือ "Q" ( string.lower() แปลงเป็นตัวพิมพ์เล็ก )
    break                         -- ออกจากลูปจบโปรแกรม
  elseif endGame and pos:lower()=="c" then
    initData()                    -- ถ้าจบเกมแล้วและรับค่าเป็น "c" หรือ "C" ให้เริ่มเกมใหม่
  else
    if not endGame then           -- ถ้ายังไม่จบเกมให้ทำงานในบล๊อกข้างล่างนี้
      pos = tonumber(pos)         -- แปลงค่า pos เป็นตัวเลข ( หรือ nil ถ้าแปลงไม่ได้ )
      if pos and board[pos]==" " then
        board[pos] = turn.player  -- ถ้ามีค่า pos และเป็นตำแหน่งว่างในกระดาน ( board ) ให้ใส่ฝ่ายผู้เล่นนั้น ( O หรือ X ) ในกระดาน
        if turn.player=="O" then  -- ถ้าเป็นตาของ O
          Omoved[pos] = true      -- ให้ใส่ตำแหน่งที่ลงในตาเดินของ O ( Omoved )
          checkWin(Omoved)        -- ตรวจผลว่าจบเกมหรือยัง
          turn.player = "X"       -- เปลี่ยนให้เป็นตาของ X
        else
          Xmoved[pos] = true      -- ถ้าเป็นตาของ X ก็ใส่ตำแหน่งใน Xmoved แทน
          checkWin(Xmoved)        -- ตรวจว่าจบเกมหรือยัง
          turn.player = "O"       -- เปลี่ยนให้เป็นตาของ O
        end
                                  -- ถ้า pos เป็น  nil หรือไม่ตรงกับตำแหน่งว่างในกระดาน
                                  -- ( board[pos]==nil or board[pos]=="O" or board[pos]=="X" )
      else                        -- if not pos or board[pos]~=" " then แสดงข้อความเตือน
        message.info1 = "โปรดกรอกตัวเลข 1-9 ตำแหน่งที่ยังว่างหรือ Q เพื่อออกจากโปรแกรม\n"
      end
    end
    if endGame then               -- ถ้าจบเกมแสดงข้อความจบ
      message.info2 = " จบเกม พิมพ์ Q เพื่อออกจากเกมหรือ C เพื่อเล่นใหม่"
    end
    showBoard()                   -- แสดงผลออกหน้าจอ
  end
end
จบแล้วครับตอนหน้าเอาเรื่องอะไรก่อนดีระหว่าง การจัดการข้อความด้วย Lua pattern กับ การจัดการไฟล์

บทความถัดไป

ความคิดเห็น