บทความก่อนหน้า
พักเบรก & ซ้อมมือ
หลังจากเนื้อหายาวๆ แล้วก็ลองหัดเขียนโค้ดจากความรู้ที่ผ่านมาดูบ้างครับ
ตัวอย่าง 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 ที่มีคีย์เป็นเลขลำดับ
ปกติการส่งค่าให้ตัวแปรจะมีสองแบบคือส่งเป็นค่าของข้อมูลได้แก่ตัวแปรประเภท 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 กับ การจัดการไฟล์
บทความถัดไป
ความคิดเห็น
แสดงความคิดเห็น