Last active
March 1, 2017 21:34
-
-
Save rndstr/fd7c85d8ee7fad800000 to your computer and use it in GitHub Desktop.
jserver API && example game
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
------ 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- * 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