Interactive Event Loops
This page shows a full game of Tetris written by Fiendish that runs as a single mobprog.
It uses a background event loop (see the end where it says "makewait(function()...") that, while active, wakes up approximately once every second to check if the player has issued any new commands. (It actually does whatever work it needs to do and then sleeps for about a second. This is technically different than waking up once per second, but about the same in practice for small quantities of work.)
Background event loops can be useful because they allow for continuous processing even without any player interaction.
https://en.wikipedia.org/wiki/Game_programming(approve sites) has this to say on the subject of event loops:
Most traditional software programs respond to user input and do nothing without it. For example, a word processor formats words and text as a user types. If the user doesn't type anything, the word processor does nothing. Some functions may take a long time to complete, but all are initiated by a user telling the program to do something. Games, on the other hand, must continue to operate regardless of a user's input. The game loop allows this. A highly simplified game loop, in pseudocode, might look something like this : while( user doesn't exit ) check for user input run AI move enemies resolve collisions draw graphics play sounds end while
Example code
The player interacts with the loop via proxy objects that are created/destroyed when the player uses the available command keywords.
The proxy command objects used by this example program are as follows:
test-30 1 Player input - LEFT Trash test-31 1 Player input - RIGHT Trash test-32 1 Player input - ROTATE Trash test-33 1 Game status - ACTIVE Trash test-34 1 Player input - DROP Trash
An example proxy command definition is:
Input Left T Key Short Desc Type Usage Trigger - -------------- -------------------- ------- ----- ------------------------------ R test-21 Tetris Room Command 11 left Code: 1. if (carries(self,"test-33")) then 2. purgeobj("all", LP_CARRIEDONLY+LP_SEEALL) 3. oload("test-33") 4. oload("test-30") 5. end
The Tetris game code mobprog is:
if progactive(self.gid,ch.gid) then cancelthreads(self.gid,ch.gid) end local LEFT_COMMAND_OBJECT = "test-30" local RIGHT_COMMAND_OBJECT = "test-31" local ROTATE_COMMAND_OBJECT = "test-32" local DROP_COMMAND_OBJECT = "test-34" local GAME_ACTIVE_COMMAND_OBJECT = "test-33" local SURRENDER_COMMAND = "surrender" --- Each tetromino is constructed by an arrangement of several "[]" atoms. --- The pattern for each piece shape is designated by indicating which Y,X coordinates --- of a 4x4 grid need to be marked in order to draw that shape. Because "[]" is --- 2 characters, the X coordinate increments by 2 while the Y coordinate increments --- by 1, making this not a display-agnostic representation. --- Each piece rotation is specified in order. Thus the rotate_piece function just --- steps through the different layouts for that piece. local piece_set = { { {{0,0},{0,2},{1,0},{1,2}} --- O rotation 1 }, { {{0,0},{0,2},{0,4},{0,6}}, --- I rotation 1 {{0,2},{1,2},{2,2},{3,2}} --- I rotation 2 }, { {{0,0},{0,2},{1,0},{2,0}}, --- J rotation 1 {{0,-2},{0,0},{0,2},{1,2}}, --- J rotation 2 {{0,2},{1,2},{2,0},{2,2}}, --- J rotation 3 {{0,-2},{1,-2},{1,0},{1,2}} --- J rotation 4 }, { {{0,0},{0,2},{1,2},{2,2}}, --- L rotation 1 {{0,-2},{0,0},{0,2},{1,-2}}, --- L rotation 2 {{0,0},{1,0},{2,0},{2,2}}, --- L rotation 3 {{0,2},{1,-2},{1,0},{1,2}} --- L rotation 4 }, { {{0,0},{0,2},{0,4},{1,2}}, --- T rotation 1 {{0,2},{1,0},{1,2},{2,2}}, --- T rotation 2 {{0,2},{1,0},{1,2},{1,4}}, --- T rotation 3 {{0,2},{1,2},{1,4},{2,2}} --- T rotation 4 }, { {{0,0},{0,2},{1,2},{1,4}}, --- Z rotation 1 {{0,2},{1,0},{1,2},{2,0}} --- Z rotation 2 }, { {{0,0},{0,2},{1,-2},{1,0}}, --- S rotation 1 {{0,0},{1,0},{1,2},{2,2}} --- S rotation 2 } } local game_board = {} local displayed_board = {} local piece_index, rotation_index local current_piece local game_over = false local HEIGHT = 20 local WIDTH = 20 --- must be a multiple of 2, because the "[]" atom has 2 characters local x_loc,y_loc local score = 0 local total_lines = 0 local function put_piece_on_board(board) for a=1,4 do board[current_piece[a][1]][current_piece[a][2]] = "\[" board[current_piece[a][1]][current_piece[a][2]+1] = "\]" end end local function blit_game_board() for j=1,HEIGHT do for i=1,WIDTH do displayed_board[j][i] = game_board[j][i] end end put_piece_on_board(displayed_board) end --- The game board gets drawn between fake MAP tags --- so that an automap capture client plugin produces --- image persistence instead of scrolling past boards away. local function draw() blit_game_board() local output_block = "\<MAPSTART\>\n\n" --- table.concat({"a","b","c","d","e"}) is more efficient than "a".."b".."c".."d".."e", --- but I use the latter form here for novice clarity. output_block = output_block.."Score: "..score.." - Lines: "..total_lines.."\n\n" output_block = output_block.."+--------------------+\n" for j=1,HEIGHT do output_block = output_block.."|"..table.concat(displayed_board[j]).."|\n" end output_block = output_block.."+--------------------+\n\n" output_block = output_block.."\<MAPEND\>\n" send(ch, output_block) end local function check_for_game_over() for a=1,4 do if game_board[current_piece[a][1]][current_piece[a][2]] ~= " " then return true end end end --- Generate a random piece and place it at the top of the --- game board. If any atom of the new piece would be placed in --- an already occupied space, then the game is over. local function new_piece() x_loc = 7 y_loc = 1 piece_index = math.random(7) current_piece = piece_set[piece_index][1] rotation_index = 1 current_piece = { {current_piece[1][1]+y_loc,current_piece[1][2]+x_loc}, {current_piece[2][1]+y_loc,current_piece[2][2]+x_loc}, {current_piece[3][1]+y_loc,current_piece[3][2]+x_loc}, {current_piece[4][1]+y_loc,current_piece[4][2]+x_loc}, } game_over = check_for_game_over() end --- Moves the piece down one row. local function lower_piece() for a=1,4 do if current_piece[a][1]+1 > HEIGHT or game_board[current_piece[a][1]+1][current_piece[a][2]] ~= " " then return false end end y_loc = y_loc+1 for a=1,4 do current_piece[a][1] = current_piece[a][1]+1 end return true end --- Blank out the current piece, choose the next sequential rotation --- layout for the piece, and then use that layout instead. But only --- if the new layout would not put part of the piece off the edge of --- the game board or overlap with an already dropped piece. local function rotate_piece() local temp_rotation if rotation_index == #piece_set[piece_index] then temp_rotation = 1 else temp_rotation = rotation_index+1 end for a=1,4 do if game_board[piece_set[piece_index][temp_rotation][a][1]+y_loc][piece_set[piece_index][temp_rotation][a][2]+x_loc] ~= " " then return end end rotation_index = temp_rotation current_piece = piece_set[piece_index][rotation_index] current_piece = { {current_piece[1][1]+y_loc,current_piece[1][2]+x_loc}, {current_piece[2][1]+y_loc,current_piece[2][2]+x_loc}, {current_piece[3][1]+y_loc,current_piece[3][2]+x_loc}, {current_piece[4][1]+y_loc,current_piece[4][2]+x_loc}, } end --- Moving pieces left and right by one atom if it won't make --- the piece go off the edge of the screen or overlap existing --- structure. local function slide_piece(direction) local slidestep = 2 --- slide right if direction == LEFT_COMMAND_OBJECT then slidestep = -2 --- slide left end local new_x for a=1,4 do new_x = current_piece[a][2]+slidestep if new_x >= WIDTH or new_x <= 0 or game_board[current_piece[a][1]][new_x] ~= " " then return -- can't slide in that direction end end x_loc = x_loc+slidestep for a=1,4 do current_piece[a][2] = current_piece[a][2]+slidestep end end --- We don't have a way to directly interact with the game loop, so --- player commands create special objects that we then check for. local function check_for_input() if carries(self,LEFT_COMMAND_OBJECT) then purgeobj(LEFT_COMMAND_OBJECT,LP_CARRIEDONLY+LP_SEEALL) return LEFT_COMMAND_OBJECT elseif carries(self,RIGHT_COMMAND_OBJECT) then purgeobj(RIGHT_COMMAND_OBJECT,LP_CARRIEDONLY+LP_SEEALL) return RIGHT_COMMAND_OBJECT elseif carries(self,ROTATE_COMMAND_OBJECT) then purgeobj(ROTATE_COMMAND_OBJECT,LP_CARRIEDONLY+LP_SEEALL) return ROTATE_COMMAND_OBJECT elseif carries(self,DROP_COMMAND_OBJECT) then purgeobj(DROP_COMMAND_OBJECT,LP_CARRIEDONLY+LP_SEEALL) return DROP_COMMAND_OBJECT else return nil end end --- For each row of the board, check the entire width --- of the row to see if the row is completed. --- Completing 1 row gives 3 points. Completing 2 rows --- gives 9 points. Completing 3 rows gives 27 points. --- Completing 4 rows gives 81 points. local function check_for_full_lines() local temp_score = 1 for j=2,HEIGHT do local line_complete = true for i=1,WIDTH do if game_board[j][i] == " " then line_complete = false break end end if line_complete then total_lines = total_lines + 1 temp_score = temp_score * 3 --- Getting multiple lines at once multiplies your score --- Remove the line and put a new blank one at the top. --- "Traditional versions of Tetris move the stacks of blocks down by a --- distance exactly equal to the height of the cleared rows below them." --- https://en.wikipedia.org/wiki/Tetris#Gravity table.remove(game_board,j) table.insert(game_board,1,{}) for i=1,WIDTH do game_board[1][i] = " " end end end if (temp_score > 1) then score = score + temp_score end end --- Act on player commands local function board_update(command) if command == LEFT_COMMAND_OBJECT or command == RIGHT_COMMAND_OBJECT then slide_piece(command) elseif command == DROP_COMMAND_OBJECT then while(lower_piece()) do end elseif command == ROTATE_COMMAND_OBJECT then rotate_piece() end if not lower_piece() then put_piece_on_board(game_board) check_for_full_lines() new_piece() end end --- Initialize the board. local function init_board(board) for j=1,HEIGHT do --- for each row board[j] = {} --- make a new row for i=1,WIDTH do --- build the row board[j][i] = " " end end end init_board(game_board) init_board(displayed_board) --- This is the main game loop. --- Every second that the game mob holds the activation token, --- check for player input, do something with that input, update the game --- board, then display the board. Player can exit the game by issuing a --- command (surrender) that destroys the activation token. makewait(function() new_piece() local command while carries(self,GAME_ACTIVE_COMMAND_OBJECT) and not game_over do board_update(check_for_input()) draw() fixedpause(1) --- Can't make the game go faster, because you can't pause for less than 1 second at a time. end purgeobj("all",LP_CARRIEDONLY+LP_SEEALL) send(ch,"Game Over!") force(ch,SURRENDER_COMMAND) end )