Skip to content

Instantly share code, notes, and snippets.

@rndstr
Last active March 1, 2017 21:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save rndstr/fd7c85d8ee7fad800000 to your computer and use it in GitHub Desktop.
Save rndstr/fd7c85d8ee7fad800000 to your computer and use it in GitHub Desktop.
jserver API && example game
------ DEBUGGING
-- this is a test function you may call in server console with
-- 'gtest <tablename>'
function gtest_lua()
g:log_debug( "debugging function called, edit your game_NAME.lua and look for gtest_lua()")
end
------ INTERNAL VARIABLES (noone touches it from outside)
-- kept unchanged once determined at the start of game
-- index in table for redeal button
dsid = -1 -- our distribution stack
idle = false -- while waiting on new match/game we do not process any cards NOTINUSE?
matchcount = 0
-- the four stacks on the table
tsid = {}
-- might change over the process of playing --
------ CALLBACK EVENTS
-- also called from C++, they need to exist or the game will abort
-- @return TRUE if <code>num</code> is a valid player count to play this game otherwise FALSE
function cGame:c_valid_numplayers ( num )
return num == 1
end
-- will be called when we load this file.
--
-- load cardset
-- send initinfo
function cGame:c_on_load()
g:log_debug( "g:c_on_load()" )
g:cardset_load( CARDSET_GERMAN )
-- 2x2 table
g:senda_initinfo( 2, 2 )
end
-- all players ready, game starts
function cGame:c_on_start_game()
g:log_debug( "g:c_on_start_game()" )
tsid[1] = g:cardstack_create()
tsid[2] = g:cardstack_create()
tsid[3] = g:cardstack_create()
tsid[4] = g:cardstack_create()
dsid = g:cardstack_create()
g:seta_scores( 0, 0 ) -- reset scores
g:senda_score_game() -- tell everyone
g:on_start_match()
end
-- several cards received by client (<code>clientidx</code>)
-- @param cards { 1 = { color = COL, id = ID }, 2 = {..}, .. }
function cGame:c_on_cards( clientidx, cards )
g:log_debug( "g:c_on_cards( "..clientidx..", "..#cards.." cards )" )
if idle then
g:sendc_error( ERROR_NOTVALID )
return
end
-- redeal?
if #cards == 1 and cards[1].color == COLOR_NONE and cards[1].id == ID_PASS then
if _redeal() then
_request()
end
return
end
g:log_debug("1")
local empty_idx = _empty_piles()
g:log_debug("2")
if #empty_idx > 0 then
if #cards ~= 1 then
g:log_debug( "found empty piles, received more than one card" )
g:sendc_error( ERROR_NOTVALID )
return
end
assert( #cards == 1 )
-- source pile must non-empty
if g:cardstack_size( tsid[cards[1].id+1] ) == 0 then
g:log_debug( "source pile is empty" )
g:sendc_error( ERROR_NOTVALID )
return
end
-- pop
local color, id = g:cardstack_pop( tsid[cards[1].id+1])
-- push on first empty pile
g:cardstack_push( tsid[empty_idx[1]], color, id )
_senda_tablecards()
_request()
return
end
g:log_debug("3")
-- all other cards must have
-- a1) COLOR_BOARD and a2) valid index
-- b) not empty
-- c) selected cards on board must have same color
local current_color = COLOR_NONE
local valid = true
for i=1,#cards do
local color,id = cards[i].color,cards[i].id
-- a1)
g:log_debug("a1")
if color ~= COLOR_BOARD then
g:log_debug( "not a board card received" )
valid = false
break
end
-- a2)
g:log_debug("a2")
if id < 0 or id > 3 then
g:log_debug( "invalid board index" )
valid = false
break
end
-- b)
g:log_debug("b")
if g:cardstack_size( tsid[id+1] ) == 0 then
g:log_debug( "empty board index" )
valid = false
break
end
g:log_debug("c")
-- selected card
local selected_color, selected_id = _top( tsid[id+1] )
if current_color == COLOR_NONE then
current_color = selected_color
end
g:log_debug("d")
-- this could be allowed actually.. if there are two doublesuits on the board the
-- player could selected the lower
if selected_color ~= current_color then
g:log_debug( "selected cards have different suits!" )
valid = false
break
end
end
if not valid then
g:sendc_error( ERROR_NOTVALID )
return -- try again
end
-- verify:
-- a) there are at least two cards on board with suit `selected_color`
-- b) there must be at least one higher id of same suit than selected
local highest_id = _highest_of_color( current_color )
-- highest id is not allowed to be selected
for i=1,#cards do
local selected_color, selected_id = _top( tsid[cards[i].id+1] )
if selected_id == highest_id then
g:log_debug( "highest card is not allowed to be selected" )
g:sendc_error( ERROR_NOTVALID )
return
end
end
-- everything fine, now remove top of each selected
for i=1,#cards do
g:cardstack_pop( tsid[cards[i].id+1] )
end
_senda_tablecards()
_request()
end
-- @returns Highest id on board of given color
function _highest_of_color(color)
local highest = 0
for i=1,4 do
local cardcolor, cardid = g:cardstack_top( tsid[i] )
if cardcolor == color then
highest = max(highest, cardid)
end
end
return highest
end
function _empty_piles()
local empty_idx = {}
local idx=1
for i=1,4 do
if g:cardstack_size( tsid[i] ) == 0 then
empty_idx[idx] = i
idx = idx + 1
end
end
return empty_idx
end
-- color received by client (<code>clientidx</code>)
function cGame:c_on_color( clientidx, color )
g:sendc_error( ERROR_NOTVALID )
end
-- called if time has run out for <code>g:curplayer()</code>
function cGame:c_on_timeup()
g:log_debug( "time run out for " .. g:curplayer() )
end
-- called when a player left the table
-- @return TRUE if game should be aborted otherwise FALSE
function cGame:c_on_player_left()
return true -- abort
end
------ INTERNAL EVENTS
-- helps you separate game logic, only called from within this script
-- load cardsets
-- create cardstacks
-- shuffle cardstack
-- distribute cards
-- reset score
-- send initial table layout
-- request a card
function cGame:on_start_match()
g:log_debug( "g:on_start_match()" )
g:cardstack_copy_from_cardset( dsid )
g:cardstack_shuffle( dsid )
g:set_curplayer(0)
g:sendc_handclear() -- there will never be cards in his hand
-- deal first four cards
_redeal()
_request()
end
-- calculate scores
-- broadcast winner of match
-- broadcast scores
-- check whether game has ended
-- if so: announce winner, call g:on_end_game()
-- else: delay next match start
function cGame:on_end_match()
g:log_debug( "g:on_end_match()" )
matchcount = matchcount + 1
-- figure out whether we won
local c0,v0 = _top(tsid[1])
local c1,v1 = _top(tsid[2])
local c2,v2 = _top(tsid[3])
local c3,v3 = _top(tsid[4])
local result = RESULT_NONE
if v0 == ID_ACE and v1 == ID_ACE and v2 == ID_ACE and v3 == ID_ACE then
-- win
g:set_score_match(0, 1)
g:add_score_game(0, 1)
result = RESULT_WIN
else
-- lose
g:seta_score_match(0)
result = RESULT_LOSE
end
g:senda_score_match()
g:senda_score_game()
g:senda_results_match(result, 0)
for i=1,4 do
g:cardstack_clear(tsid[i])
end
_senda_tablecards()
if matchcount >= scorelimit then
-- just display his results...
g:senda_results_game(RESULT_NONE)
g:on_end_game()
else
-- wait a bit till starting of next match
g:start_timer( DEFAULT_MATCH_PAUSE, "on_start_match" )
end
end
function cGame:on_end_game()
g:log_debug( "g:on_end_game()" )
g:start_timer( DEFAULT_GAME_PAUSE, "c_on_start_game" )
end
------ HELPER
-- means either
-- a) fill up if there are empty piles or
-- b) deal 4 new cards
-- @return true if game is still running
function _redeal()
g:log_debug( "redeal" )
if g:cardstack_size( dsid ) == 0 then
g:on_end_match()
return false
end
for i=1,4 do
if g:cardstack_size(dsid) == 0 then
break -- client has to `redeal' again to get notified whether it's a win or not.. he still has the chance
end
local color,id = g:cardstack_pop(dsid)
g:cardstack_push( tsid[i], color, id )
end
g:log_debug( "CARDS LEFT ON STACK = " .. g:cardstack_size(dsid) )
_senda_tablecards()
return true
end
-- possible actions:
-- 1) redeal(pass) (1 card)
-- 2) remove cards from board (1-3)
-- 3) move to empty pile (1) (or redeal)
function _request()
local empty_idx = _empty_piles()
if #empty_idx > 0 then
g:sendca_requestcard( 1, 1, BM_REQUESTCARD_BOARDONLY + FLAG_REQUESTCARD_ALLOWPASS, "Select a card to be moved to the empty pile" ) -- 3)
else
g:sendca_requestcard( 1, 3, BM_REQUESTCARD_BOARDONLY + FLAG_REQUESTCARD_ALLOWPASS, "Remove lowest cards of suited cards" ) -- 1) or 2)
end
end
-- send table cards to everyone
function _senda_tablecards()
g:log_debug("_senda_tablecards")
local c0,v0 = _top(tsid[1])
local c1,v1 = _top(tsid[2])
local c2,v2 = _top(tsid[3])
local c3,v3 = _top(tsid[4])
g:senda_tablecards(0, c0, v0, 1, c1, v1, 2, c2, v2, 3, c3, v3 )
--[[
local color, id
for i=1,4 do
color,id = _top( tsid[i] )
g:senda_tablecards(i-1, color, id)
end
]]
end
function _top(tsid)
if g:cardstack_size(tsid) == 0 then
return COLOR_NONE, ID_FOUNDATION
else
return g:cardstack_top(tsid)
end
end
-- * attach functions to our existing cGame class. ('cGame:_blah') Use a preceeding underscore to make sure there is no name clash.
-- * access the game instance through 'g'
-- * access global exported C++ functions:
-- j_log( string )
-- write to jserver log stream (you may not use '\n', a newline is appended automatically on each function call)
-- NOTE: for tablelog use g:log() and g:log_debug()
-- j_stackdump() -- dumps the stack
--
-- * convention:
-- send_* - needs the client_idx as first argument (NOTE not id!)
-- sendc_* - sends to current player (can be set with set_curplayer/nextplayer)
-- senda_* - sends to all players on the table
-- * game_NAME.lua -- where NAME is also the gamename
--
-- exported cpp functions:
-- returns number of players.
int
g:numplayers() const
--- will call the function after given seconds
--- NOTE that the callback function must be in class cGame
void
g:start_timer( <int:seconds>, <string:callback_function> )
-- translates text to client's locale (currently it's only a dummy... as we don't have translation yet)
<string:translated>
g:textc( <string:text> ) -- use locale from current player
<string:translated>
g:text( <int:clientidx>, <string:text> ) -- use locale from given client
-- localized color names
<string:translated>
g:textc_color( <int:color> )
-- sets current player to given client index (NOTE: not id!)(NOTE2: not modulo! FIXME?)
void
g:set_curplayer( <int:clientindex> )
-- returns the index of the would-be next player. (does not change anything)
<int:clientindex>
g:get_nextplayer() const
-- moves the curplayer to the next client (+1 and modulo).
void
g:nextplayer( )
-- returns the index of the would-be previous player. (does not change anything)
<int:clientidx>
g:get_prevplayer() const
-- moves the curplayer pointer one back (-1 and modulo)
void
g:prevplayer()
-- advances the playerpointer for <how_many>. g:nextplayer == g:advanceplayer(1), g:prevplayer == g:advanceplayer(-1)
void
g:advanceplayer( <int:how_many> )
-- advances the playerpointer for <how_many> and returns that index but does not modify anything
<int:clientidx>
g:get_advanceplayer( <int:how_many> ) const
-- returns current player index.
<int:clientindex>
g:curplayer() const
-- returns true if clientidx matches with curplayer
<bool>
g:is_curplayer( <int:clientidx> )
-- sends a packet to our current player.
void
g:sendc( <string:command>, <string:params> )
-- loads a predefined cardset. (see game.h:eGameCardset for constants )
void
g:cardset_load( <int:cardset> )
-- adds a card to the cardset.
void
g:cardset_addcard( <int:color>, <int:id> )
-- creates empty cardstack.
<int:cardstackid>
g:cardstack_create()
-- creates a new cardstack by copying the cardset.
<int:cardstackid>
g:cardstack_create_from_cardset()
-- clears the given cardstack and copies the cardset to the cardstack.
void
g:cardstack_copy_from_cardset( <int:cardstackid> )
-- shuffles the cardstack.
void
g:cardstack_shuffle( <int:cardstackid> )
-- returns the card at the end of the stack and removes it. TODO: split in 2 funcs?
<int:color>,<int:id>,<int:owneridx>
g:cardstack_pop( <int:cardstackid> )
-- returns the card at the top of the stack.
<int:color>,<int:id>,<int:owneridx>
g:cardstack_top( <int:cardstackid> )
-- adds a card at the end of the given cardstack.
void
g:cardstack_push( <int:cardstackid>, <int:color>, <int:id> [, <int:owner>] )
-- pushes all values (fifo style) from 2nd cardstack to first.
void
g:cardstack_push_cardstack( <int:to_cardstackid>, <int:from_cardstackid> )
-- returns true if the card exists in the given cardstack.
bool
g:cardstack_cardexists( <int:cardstackid>, <int:color>, <int:id> )
-- removes all cards from the given cardstack.
void
g:cardstack_clear( <int:cardstackid> )
-- returns the number of cards in there.
<int:size>
g:cardstack_size( <int:cardstackid> )
-- returns the card at the given position.
<int:color>,<int:id>,<int:owneridx>
g:cardstack_at( <int:cardstackid>, <int:idx> )
-- sets up teams automatically, considering the clients' wishes (where they are already seaten)
-- or assign them to teams if they have no preference
-- NOTE after calling this, there is no guarantee player indecies are in the same order..
void
g:teams_auto( <int:teamsizes> )
-- tells everyone what the teams are
void
g:senda_teams()
-- you can store scores as match or game.
-- score_game is the overall score, score_match only from the current match.
-- you can keep score_match as helpers to count stuff during the game, just make
-- sure you don't call senda_score_match() :)
-- set scores of all players.
void
seta_scores( <int:match>, <int:game> )
-- set match scores of all players.
void
seta_score_match( <int:score> )
--- set match score of single player
void set_score_match( <int:clientidx>, <int:score> )
<int:score> get_score_match( <int:clientidx> ) -- retrieve match score of given player.
void add_score_match( <int:clientidx>, <int:score> )
<int:score> get_score_game( <int:clientidx> )
void add_score_game( <int:clientidx>, <int:score> )
-- send everyone on table the info about gamescores.
void
senda_score_game( [int:goal] )
-- send everyone on table the info about matchscores (most games don't even supply this)
void
senda_score_match( [int:goal] )
-- For result announcements, if no clients suplied, we tell them noone has won the round.
-- result_type = RESULT_NONE | RESULT_WIN | RESULT_LOSE | RESULT_DRAw
void
g:senda_results_round( <int:result_type>, [<int:clientidx>[, <int:clientidx>, ..]] )
void
g:senda_results_match( <int:result_type>, [<int:clientidx>[, <int:clientidx>, ..]] )
void
g:senda_results_game( <int:result_type>, [<int:clientidx>[, <int:clientidx>, ..]] )
void
g:sendc_error( <int:error_num> )
void
g:send_error( <int:clientidx>, <int:error_num> )
-- NOTE: the following hand modifiers will also be sent to all the other players on the table, but always
-- as COLOR_NONE:CARD_HIDDEN
-- sends <numcards> cards to current player, pop'ping cards from given cardstack, and pushing on his own cardstack.
void
g:sendc_handadd_from_cardstack( <int:cardstackid>, <int:numcards> )
-- removes a card from the clients own cardstack.
void
g:sendc_handdel( <int:color>, <int:id> )
-- clears all cards of the clients own cardstack.
void
g:sendc_handclear()
-- returns the number of cards in hand.
<int:size>
g:getc_hand_size( )
<int:size>
g:get_hand_size( )
-- returns the card at given index.
<int:color>,<int:id>
g:getc_hand_at( <int:cardidx> )
<int:color>,<int:id>
g:get_hand_at( <int:clientidx>, <int:cardidx> )
-- sends info that is known at initialization-time.
void
g:senda_initinfo( <int:gridwidth>, <int:gridheight> )
void
g:sendc_tableclear -- clear table
void
g:senda_tableclear -- clear table but to all clients
void
g:send_tablecards( <int:grid_idx>, <int:color>, <int:id> [, <int:grid_idx>, <int:color, <int:id>, ...] )
void
g:senda_tablecards( <int:grid_idx>, <int:color>, <int:id> [, <int:grid_idx>, <int:color, <int:id>, ...] )
-- in g:c_on_card() after card was verified we notify all other players.
void
g:sendca_card_notify( <int:color>, <int:id> )
void
g:senda_card_notify( <int:clientidx>, <int:color>, <int:id> )
-- requests cards from current player but sends the info to all
-- see gamelib.lua:FlAG_REQUESTCARD_* for options. by default we request hand cards only.
void
g:sendca_requestcard( <int:min>, [<int:max>], [<int:options>], [<string:hint>] )
-- sends all clientapps the info that they should erase all info about who is up now, there will soon a new info follow :)
-- @deprecated
void
g:senda_requestreset()
-- requests color from current player but sends the info to all.
void
g:sendca_requestselect(<int:item_idx>, <string:item_description> [, <int:item_idx>, <string:item_desc>, ...][, <string:hint>])
-- sends notification information, that given/current player has chosen a color.
void
g:sendca_select_notify( <string:description> )
void
g:senda_select_notify( <int:clientidx>, <string:description> )
-- @return true If curplayer has a card in ownstack with color <color>.
bool
g:hasc_color( <int:color> )
bool
g:has_card( <int:clientidx>, <int:color>, <int:id> )
-- @return true If player has the card in ownstack.
bool
g:hasc_card( <int:color>, <int:id> )
bool
g:has_color( <int:clientidx>, <int:color> )
-- @return true If curplayer has a card in hand with id <id>.
bool
g:hasc_id ( <int:id> )
bool
g:has_id( <int:clientidx>, <int:id> )
-- @return The cardinfo of the first encountered card in hand with given id or COLOR_NONE,ID_NONE if the id wasn't found.
<int:color>,<int:id>
g:getc_card_with_id( <int:id> )
-- returns number of cards with given id the player has.
-- @see g:getc_hand_size
<int:size>
g:getc_numcards_id( <int:id> )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment