refactor: designed based on hypermedia principles

dev
LeRoyce Pearson 2024-02-18 14:39:52 -07:00
parent e87b3c75bf
commit bcfa629419
1 changed files with 329 additions and 319 deletions

648
rummy.lua
View File

@ -16,8 +16,9 @@ cards_in_meld_draft=nil
melds_on_tabletop=nil melds_on_tabletop=nil
-- `points_of_interest` is an array of things that can be hovered/selected by the controller -- `points_of_interest` is an array of things that can be hovered/selected by the controller
current_page=nil
hovered=nil hovered=nil
player_state=nil current_handler=nil
function BOOT() function BOOT()
draw_pile = create_deck() draw_pile = create_deck()
@ -30,380 +31,376 @@ function BOOT()
discard_pile = draw_pile:draw_stack(1) discard_pile = draw_pile:draw_stack(1)
player_state = player_state_draw_card current_handler=handler_draw_card
melds_on_tabletop={} melds_on_tabletop={}
end end
function TIC() function TIC()
cls(12) local c_sel=btnp(5)
local points_of_interest = player_state.get_points_of_interest() local request=nil
if c_sel and hovered then
request=hovered.action
current_page=nil
end
local discard_x=((240 - 24) / 2) while not current_page do
local draw_pile_x=discard_x - 24 - 4 trace(current_handler)
local end_turn_x=discard_x + 24 + 4 trace(request)
local response=current_handler(request)
if response.redirect then
current_handler=response.redirect
request=nil
elseif response.page then
current_page=response.page
else
cls(12)
print("Invalid response!")
for key,val in pairs(response) do
trace("key="..key)
trace("val="..val)
end
return nil
end
end
if not hovered and #points_of_interest>0 then local actions = get_actions_from_page(current_page)
hovered=points_of_interest[1]
if not hovered and #actions>0 then
hovered=actions[1]
end end
local c_up=btnp(0,10,3) local c_up=btnp(0,10,3)
local c_down=btnp(1,10,3) local c_down=btnp(1,10,3)
local c_left=btnp(2,10,3) local c_left=btnp(2,10,3)
local c_right=btnp(3,10,3) local c_right=btnp(3,10,3)
local c_sel=btnp(5)
local c_back=btnp(4) local c_back=btnp(4)
if c_sel and hovered then -- update hovered action
player_state = player_state.update(hovered)
end
-- update hovered position
local moved = c_up or c_down or c_left or c_right local moved = c_up or c_down or c_left or c_right
if moved and #points_of_interest > 0 then if moved and #actions > 0 then
local start = hovered or {x=0,y=0,interest=nil} local start=hovered or {x=0,y=0,action=nil}
local dir = {0, 0} local dir={0, 0}
if c_left then dir[1] = dir[1] - 1 end if c_left then dir[1]=dir[1] - 1 end
if c_right then dir[1] = dir[1] + 1 end if c_right then dir[1]=dir[1] + 1 end
if c_up then dir[2] = dir[2] - 1 end if c_up then dir[2]=dir[2] - 1 end
if c_down then dir[2] = dir[2] + 1 end if c_down then dir[2]=dir[2] + 1 end
local nearest = nil local nearest=nil
for i,point in ipairs(points_of_interest) do for i,action in ipairs(actions) do
if hovered and point.interest == hovered.interest then if hovered and action.action==hovered.action then
-- pass -- pass
elseif not nearest then elseif not nearest then
local distance = distance_to_point_of_interest(start,dir,point) local distance=distance_to_point_of_interest(start,dir,action)
if distance > 0 then if distance>0 then
nearest = point nearest=action
end end
else else
local distance = distance_to_point_of_interest(start,dir,point) local distance=distance_to_point_of_interest(start,dir,action)
if distance > 0 and distance < distance_to_point_of_interest(start,dir,nearest) then if distance>0 and distance<distance_to_point_of_interest(start,dir,nearest) then
nearest = point nearest=action
end end
end end
end end
if nearest then if nearest then
hovered = nearest hovered=nearest
end end
end end
-- render melds on tabletop -- render current page
cls(12)
for i,element in ipairs(current_page) do
if element.visual then
if getmetatable(element.visual)==Card then
element.visual:render(element.x, element.y, element.hidden, element.sel_state)
elseif getmetatable(element.visual)==Sprite then
spr(element.visual.sid,
element.x,
element.y,
element.visual.colorkey,
1, 0, 0,
element.visual.tw,
element.visual.th)
else
local x = element.textx or element.x
local y = element.texty or element.y
local text_color=13
if element.action then
text_color=0
end
print(element.visual, x, y, text_color)
end
end
if hovered and hovered.action==element.action then
spr(Card.spr_hilight.sid,
element.x,
element.y,
Card.spr_hilight.colorkey,
1, 0, 0,
Card.spr_hilight.tw,
Card.spr_hilight.th)
end
end
end
function get_actions_from_page(page)
local actions={}
for i,element in ipairs(page) do
if element.action then
table.insert(actions, {
action=element.action,
x=element.action_x or element.x,
y=element.action_y or element.y,
})
end
end
return actions
end
function build_meld_ui(elements, draft, new_meld_allowed)
local meld_x=2 local meld_x=2
local meld_y=10
for j,meld in ipairs(melds_on_tabletop) do for j,meld in ipairs(melds_on_tabletop) do
local sel_state = nil local meld_action=nil
if hovered and hovered.interest == meld then if #cards_in_meld_draft>0 then
sel_state = 1 local meld_with_draft=meld:with(draft)
if rummy_is_valid_meld(meld_with_draft) then
meld_action=meld
end
end end
for i,card in ipairs(meld) do for i,card in ipairs(meld) do
card:render(meld_x, 10, false, sel_state) table.insert(elements, {
visual=card,
x=meld_x, y=meld_y,
action=meld_action,
})
meld_x=meld_x + 8 meld_x=meld_x + 8
end end
meld_x=meld_x + 28 meld_x=meld_x + 28
end end
if is_an_interest(points_of_interest, "New\nMeld") then table.insert(elements, {
print("New\nMeld", meld_x, 17, 0) visual="New\nMeld",
if hovered and hovered.interest=="New\nMeld" then textx=meld_x, texty=meld_y+7,
spr(Card.spr_hilight.sid, x=meld_x, y=meld_y,
meld_x, action=((new_meld_allowed and rummy_is_valid_meld(draft)) and "New\nMeld" or nil),
10, action_x=meld_x, action_y=meld_y,
Card.spr_hilight.colorkey, })
1, 0, 0, end
Card.spr_hilight.tw,
Card.spr_hilight.th) function build_draw_discard_ui(elements, options)
local discard_x=((240 - 24) / 2)
local discard_y=80
local draw_pile_x=discard_x - 24 - 4
local end_turn_x=discard_x + 24 + 4
-- render draw pile
for i,card in ipairs(draw_pile) do
if i==#draw_pile and options.is_drawing then
table.insert(elements, {
visual=card,
x=draw_pile_x, y=discard_y + (i - 1) * -0.25,
hidden=true,
action=card,
})
else
table.insert(elements, {
visual=card,
x=draw_pile_x, y=discard_y + (i - 1) * -0.25,
hidden=true,
})
end end
else
print("New\nMeld", meld_x, 17, 13)
end end
-- render discard pile -- render discard pile
for i,card in ipairs(discard_pile) do for i,card in ipairs(discard_pile) do
local sel_state = nil if i==#discard_pile and options.is_drawing then
if hovered and hovered.interest == card then table.insert(elements, {
sel_state = 1 visual=card,
end x=discard_x, y=discard_y + (i - 1) * -0.25,
action=card,
card:render(discard_x, 80 + (i - 1) * -0.25, false, sel_state) })
end else
if is_an_interest(points_of_interest, "Discard") then table.insert(elements, {
if hovered and hovered.interest=="Discard" then visual=card,
spr(Card.spr_hilight.sid, x=discard_x, y=discard_y + (i - 1) * -0.25
discard_x, })
80 + (#discard_pile - 1) * -0.25,
Card.spr_hilight.colorkey,
1, 0, 0,
Card.spr_hilight.tw,
Card.spr_hilight.th)
end end
end end
table.insert(elements, {
visual=(#discard_pile==0 and Card.sprite),
x=discard_x, y=discard_y + (#discard_pile - 1) * -0.25,
action=options.discard_action,
})
-- render draw pile table.insert(elements, {
for i,card in ipairs(draw_pile) do visual="End\nTurn",
local sel_state = nil textx=end_turn_x, texty=discard_y + 7,
if hovered and hovered.interest == card then x=end_turn_x, y=discard_y,
sel_state = 1 action=options.end_turn_action,
end })
end
card:render(draw_pile_x, 80 + (i - 1) * -0.25, true, sel_state)
end
if is_an_interest(points_of_interest, "End\nTurn") then
print("End\nTurn", end_turn_x, 80+7, 0)
if hovered and hovered.interest=="End\nTurn" then
spr(Card.spr_hilight.sid,
end_turn_x, 80,
Card.spr_hilight.colorkey,
1, 0, 0,
Card.spr_hilight.tw,
Card.spr_hilight.th)
end
else
print("End\nTurn", end_turn_x, 80+7, 13)
end
function build_hand_ui(elements, hand, draft, can_select)
local hand_start_x=((240 - #cards_in_hand * 12 - 24) / 2) local hand_start_x=((240 - #cards_in_hand * 12 - 24) / 2)
for i,card in ipairs(cards_in_hand) do local hand_y=110
local ty=0 for i,card in ipairs(hand) do
local sel_state = nil if can_select then
local ty=0
local sel_state = nil
if cards_in_meld_draft:contains(card) then if draft:contains(card) then
ty = -4 ty = -4
sel_state = 2 sel_state = 2
end
table.insert(elements, {
visual=card,
x=hand_start_x + (i - 1) * 12, y=hand_y + ty,
sel_state=sel_state,
action=card, action_y=hand_y,
})
else
table.insert(elements, {
visual=card,
x=hand_start_x + (i - 1) * 12, y=hand_y
})
end end
if hovered and hovered.interest == card then
sel_state = 1
end
card:render(hand_start_x + (i - 1) * 12, 110 + ty, false, sel_state)
end end
end end
player_state_draw_card = { function handler_draw_card(action)
update=function(point_of_interest) if draw_pile:contains(action) then
-- only accept drawing cards from the draw pile or table.insert(cards_in_hand, draw_pile:draw())
if draw_pile:contains(point_of_interest.interest) then cards_in_hand:sort(rummy_hand_sort_comparison)
table.insert(cards_in_hand, draw_pile:draw()) hovered = nil
cards_in_hand:sort(rummy_hand_sort_comparison) return { redirect=handler_player_action }
hovered = nil elseif discard_pile:contains(action) then
return player_state_action table.insert(cards_in_hand, discard_pile:draw())
elseif discard_pile:contains(point_of_interest.interest) then cards_in_hand:sort(rummy_hand_sort_comparison)
table.insert(cards_in_hand, discard_pile:draw()) hovered = nil
cards_in_hand:sort(rummy_hand_sort_comparison) return { redirect=handler_player_action }
hovered = nil
return player_state_action
end
return player_state_draw_card
end,
get_points_of_interest=function()
local points_of_interest = {}
if #draw_pile > 0 then
table.insert(points_of_interest, {
x=3 + 12,
y=80 + (#draw_pile - 1) * -0.25 + 12,
interest=draw_pile[#draw_pile]
})
end
if #discard_pile > 0 then
table.insert(points_of_interest, {
x=39 + 12,
y=80 + (#discard_pile - 1) * -0.25 + 12,
interest=discard_pile[#discard_pile]
})
end
return points_of_interest
end end
}
player_state_action = { local elements={}
update=function(point_of_interest)
if cards_in_hand:contains(point_of_interest.interest) then build_meld_ui(elements, cards_in_meld_draft, false)
if cards_in_meld_draft:contains(point_of_interest.interest) then build_draw_discard_ui(elements, { is_drawing=true })
table.remove(cards_in_meld_draft, cards_in_meld_draft:index_of(point_of_interest.interest)) build_hand_ui(elements, cards_in_hand, cards_in_meld_draft, false)
else
table.insert(cards_in_meld_draft, point_of_interest.interest) return { page=elements }
cards_in_meld_draft:sort(rummy_hand_sort_comparison) end
end
elseif point_of_interest.interest=="New\nMeld" then function handler_player_action(action)
if rummy_is_valid_meld(cards_in_meld_draft) then if cards_in_hand:contains(action) then
for i,card in ipairs(cards_in_meld_draft) do if cards_in_meld_draft:contains(action) then
table.remove(cards_in_hand, cards_in_hand:index_of(card)) table.remove(cards_in_meld_draft, cards_in_meld_draft:index_of(action))
end else
table.insert(melds_on_tabletop, cards_in_meld_draft) table.insert(cards_in_meld_draft, action)
cards_in_meld_draft=CardStack:new() cards_in_meld_draft:sort(rummy_hand_sort_comparison)
hovered = nil
return player_state_secondary_action
end
elseif point_of_interest.interest=="Discard" then
if #cards_in_meld_draft==1 then
table.remove(cards_in_hand, cards_in_hand:index_of(cards_in_meld_draft[1]))
table.insert(discard_pile, cards_in_meld_draft[1])
cards_in_meld_draft=CardStack:new()
hovered = nil
return player_state_discard_confirm
end
elseif table_index_of(melds_on_tabletop, point_of_interest.interest) then
local meld=point_of_interest.interest
local meld_with_draft=meld:with(cards_in_meld_draft)
if rummy_is_valid_meld(meld_with_draft) then
for i,card in ipairs(cards_in_meld_draft) do
table.remove(cards_in_hand, cards_in_hand:index_of(card))
table.insert(meld, card)
end
meld:sort(rummy_hand_sort_comparison)
cards_in_meld_draft=CardStack:new()
end
end end
return player_state_action elseif action=="New\nMeld" then
end,
get_points_of_interest=function()
local points_of_interest = {}
for i,card in ipairs(cards_in_hand) do
table.insert(points_of_interest, {
x=(i - 1) * 12 + 2 + 5,
y=122,
interest=card
})
end
if rummy_is_valid_meld(cards_in_meld_draft) then if rummy_is_valid_meld(cards_in_meld_draft) then
table.insert(points_of_interest, { for i,card in ipairs(cards_in_meld_draft) do
x=80 + 12, table.remove(cards_in_hand, cards_in_hand:index_of(card))
y=17 + 6,
interest="New\nMeld"
})
end
if #cards_in_meld_draft==1 then
table.insert(points_of_interest, {
x=39,
y=80 + (#discard_pile - 1) * -0.25,
interest="Discard"
})
end
if #cards_in_meld_draft>0 then
for i,meld in ipairs(melds_on_tabletop) do
local meld_with_draft=meld:with(cards_in_meld_draft)
if rummy_is_valid_meld(meld_with_draft) then
table.insert(points_of_interest, {
x=i * 36,
y=10,
interest=meld
})
end
end end
end table.insert(melds_on_tabletop, cards_in_meld_draft)
cards_in_meld_draft=CardStack:new()
return points_of_interest
end
}
-- In this state the player may only lay off a card or end their turn
player_state_secondary_action = {
update=function(point_of_interest)
if cards_in_hand:contains(point_of_interest.interest) then
if cards_in_meld_draft:contains(point_of_interest.interest) then
table.remove(cards_in_meld_draft, cards_in_meld_draft:index_of(point_of_interest.interest))
else
table.insert(cards_in_meld_draft, point_of_interest.interest)
cards_in_meld_draft:sort(rummy_hand_sort_comparison)
end
elseif point_of_interest.interest=="Discard" then
if #cards_in_meld_draft==1 then
table.remove(cards_in_hand, cards_in_hand:index_of(cards_in_meld_draft[1]))
table.insert(discard_pile, cards_in_meld_draft[1])
cards_in_meld_draft=CardStack:new()
hovered = nil
return player_state_discard_confirm
end
elseif table_index_of(melds_on_tabletop, point_of_interest.interest) then
local meld=point_of_interest.interest
local meld_with_draft=meld:with(cards_in_meld_draft)
if rummy_is_valid_meld(meld_with_draft) then
for i,card in ipairs(cards_in_meld_draft) do
table.remove(cards_in_hand, cards_in_hand:index_of(card))
table.insert(meld, card)
end
meld:sort(rummy_hand_sort_comparison)
cards_in_meld_draft=CardStack:new()
end
end
return player_state_secondary_action
end,
get_points_of_interest=function()
local points_of_interest = {}
for i,card in ipairs(cards_in_hand) do
table.insert(points_of_interest, {
x=(i - 1) * 12 + 2 + 5,
y=122,
interest=card
})
end
if #cards_in_meld_draft==1 then
table.insert(points_of_interest, {
x=39,
y=80 + (#discard_pile - 1) * -0.25,
interest="Discard"
})
end
if #cards_in_meld_draft>0 then
for i,meld in ipairs(melds_on_tabletop) do
local meld_with_draft=meld:with(cards_in_meld_draft)
if rummy_is_valid_meld(meld_with_draft) then
table.insert(points_of_interest, {
x=i * 36,
y=10,
interest=meld
})
end
end
end
return points_of_interest
end
}
player_state_discard_confirm = {
update=function(point_of_interest)
if point_of_interest.interest=="End\nTurn" then
hovered = nil hovered = nil
return player_state_draw_card return { redirect=handler_player_secondary_action }
end end
elseif action=="Discard" then
return player_state_discard_confirm if #cards_in_meld_draft==1 then
end, table.remove(cards_in_hand, cards_in_hand:index_of(cards_in_meld_draft[1]))
get_points_of_interest=function() table.insert(discard_pile, cards_in_meld_draft[1])
local points_of_interest = {} cards_in_meld_draft=CardStack:new()
hovered = nil
table.insert(points_of_interest, { return { redirect=handler_discard_confirm }
x=80 + 12, end
y=87 + 6, elseif table_index_of(melds_on_tabletop, action) then
interest="End\nTurn" local meld=point_of_interest.interest
}) local meld_with_draft=meld:with(cards_in_meld_draft)
if rummy_is_valid_meld(meld_with_draft) then
return points_of_interest for i,card in ipairs(cards_in_meld_draft) do
end table.remove(cards_in_hand, cards_in_hand:index_of(card))
} table.insert(meld, card)
end
function is_an_interest(points_of_interest,interest_sought) meld:sort(rummy_hand_sort_comparison)
for i,point in ipairs(points_of_interest) do cards_in_meld_draft=CardStack:new()
if point.interest==interest_sought then
return true
end end
end end
return false
local elements={}
build_meld_ui(elements, cards_in_meld_draft, true)
build_draw_discard_ui(elements, {
discard_action=(#cards_in_meld_draft==1 and "Discard" or nil),
})
build_hand_ui(elements, cards_in_hand, cards_in_meld_draft, true)
return { page=elements }
end
function handler_player_secondary_action(action)
if cards_in_hand:contains(action) then
if cards_in_meld_draft:contains(action) then
table.remove(cards_in_meld_draft, cards_in_meld_draft:index_of(action))
else
table.insert(cards_in_meld_draft, action)
cards_in_meld_draft:sort(rummy_hand_sort_comparison)
end
elseif action=="Discard" then
if #cards_in_meld_draft==1 then
table.remove(cards_in_hand, cards_in_hand:index_of(cards_in_meld_draft[1]))
table.insert(discard_pile, cards_in_meld_draft[1])
cards_in_meld_draft=CardStack:new()
hovered = nil
return { redirect=handler_discard_confirm }
end
elseif table_index_of(melds_on_tabletop, action) then
local meld=point_of_interest.interest
local meld_with_draft=meld:with(cards_in_meld_draft)
if rummy_is_valid_meld(meld_with_draft) then
for i,card in ipairs(cards_in_meld_draft) do
table.remove(cards_in_hand, cards_in_hand:index_of(card))
table.insert(meld, card)
end
meld:sort(rummy_hand_sort_comparison)
cards_in_meld_draft=CardStack:new()
end
end
local elements={}
build_meld_ui(elements, cards_in_meld_draft, false)
build_draw_discard_ui(elements, {
discard_action=(#cards_in_meld_draft==1 and "Discard" or nil),
})
build_hand_ui(elements, cards_in_hand, cards_in_meld_draft, true)
return { page=elements }
end
function handler_discard_confirm(action)
if action=="End\nTurn" then
hovered = nil
return { redirect=handler_draw_card }
end
local elements={}
build_meld_ui(elements, cards_in_meld_draft, false)
build_draw_discard_ui(elements, {
end_turn_action="End\nTurn",
})
build_hand_ui(elements, cards_in_hand, cards_in_meld_draft, false)
return { page=elements }
end end
function distance_to_point_of_interest(start,dir,point_of_interest) function distance_to_point_of_interest(start,dir,point_of_interest)
@ -559,46 +556,59 @@ function CardStack:with(other)
end end
Sprite={}
Sprite.__index=Sprite
function Sprite:new(obj)
local o = obj or {}
setmetatable(o, Sprite)
return o
end
-- Card rendering stuff -- Card rendering stuff
suit_icon={34,35,33,36} suit_icon={34,35,33,36}
suit_color={0,0,2,2} suit_color={0,0,2,2}
suit_names={'clubs','spades','hearts','diamonds'} suit_names={'clubs','spades','hearts','diamonds'}
rank_symbols={'A','2','3','4','5','6','7','8','9','10','J','Q','K'} rank_symbols={'A','2','3','4','5','6','7','8','9','10','J','Q','K'}
Card = { Card = {
sprite={ sprite=Sprite:new({
sid=5, sid=5,
tw=3, tw=3,
th=3, th=3,
colorkey=14 colorkey=14
}, }),
spr_hilight={ spr_hilight=Sprite:new({
sid=8, sid=8,
tw=3, tw=3,
th=3, th=3,
colorkey=14 colorkey=14
}, }),
spr_selected={ spr_selected=Sprite:new({
sid=11, sid=11,
tw=3, tw=3,
th=3, th=3,
colorkey=14 colorkey=14
}, }),
spr_hidden={ spr_hidden=Sprite:new({
sid=56, sid=56,
tw=3, tw=3,
th=3, th=3,
colorkey=14 colorkey=14
}, }),
} }
Card.__index = Card
function Card:new(suit, rank) function Card:new(suit, rank)
local card = { local card = {
suit=suit, suit=suit,
rank=rank, rank=rank,
} }
setmetatable(card, { __index=Card }) setmetatable(card, Card)
return card return card
end end
function Card:__tostring(suit, rank)
return "<"..rank_symbols[self.rank].." of "..suit_names[self.suit]..">"
end
function Card:render(x, y, hidden, sel_state) function Card:render(x, y, hidden, sel_state)
if hidden then if hidden then
spr(self.spr_hidden.sid, spr(self.spr_hidden.sid,