123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773 |
- local love = require("compat")
- local game = {
- GAMESTATE = "",
- SERVERGAMESTATE = "",
- usersMoved = {},
- newUserPositions = {},
- crashedUsers = {}, -- remembers how many rounds the user has to wait
- time = 0,
- maxTime = 0,
- timerEvent = nil,
- time2 = 0,
- maxTime2 = 0,
- timerEvent2 = nil,
- roundTime = 10,
- winnerID = nil
- }
- local tease = {
- "Server: Hah, you crashed, ",
- "Server: Drunk driving, ",
- "Server: Come on, ",
- "Server: ",
- }
- local tease2 = {
- "!",
- "!",
- ", what was that?!",
- ", that was embarrassing...",
- }
- -- Possible gamestates:
- -- "startup": camera should move to start line
- -- "move": players are allowed to make their move.
- -- "wait": waiting for server or other players, or for animtion
- local scr = nil
- local panel = require( "panel" )
- function game:init()
- scr = ui:newScreen( "game" )
- scr:addPanel( "topPanel",
- 0, 0,
- love.graphics.getWidth(), 35 )
- scr:addFunction( "topPanel", "leave", 20, 0, "Leave", "q", game.close )
- scr:addFunction( "topPanel", "help", love.graphics.getWidth() -160, 0, "Help", "h", game.toggleHelp )
- self.winnerPanel = panel:new( love.graphics.getWidth()/3, love.graphics.getHeight()/2 - 100,
- love.graphics.getWidth()/3, love.graphics.getFont():getHeight() + 10 )
- self.userPanel = panel:new( 0, 0, 450, 19 )
- end
- function game:toggleHelp()
- if scr:panelByName( "helpPanel" ) ~= nil then
- scr:removePanel( "helpPanel" )
- else
- local width = 300
- local x = love.graphics.getWidth() - 370
- local y = 0
- scr:addPanel( "helpPanel",
- x,
- 80,
- width, 410 )
- scr:addHeader( "helpPanel", "h1", 0, y, "Help:" )
- y = y + 30
- scr:addText( "helpPanel", "helpText", 10, y, nil, 7, "Move your car by clicking the fields around it. Once every player has planned their move, the cars start driving. The places you can drive to this round depends on how your carr drove during the last movement phase: The same vector you moved last round will be added onto your car's position; from the resulting point, all neighbouring fields are available.\nBe the first to finsih the race, but don't leave the road or crash into other cars!\n\nUse W-A-S-D or cursor keys to move the camera, use mouse wheel or + and - to zoom.\n\nCrashes:\nStay on the road! If you leave the road, you will crash. The time you have to wait afterwards depends on how fast you were.\nYou can crash into other player's cars if you click on the same position. Whoever clicks second, crashes." )
- scr:addFunction( "helpPanel", "close", 10, 360, "Close", "h", nil )
- end
- end
- function game:show()
- STATE = "Game"
- game.winnerID = nil
- map:removeAllCars()
-
- if server then
- -- For exery player who's playing, add one start position to the list of available positions:
- local availablePositions = {}
- local i = 1
- for id, u in pairs( server:getUsers() ) do
- if u.customData.ingame then
- -- add one start position to the list of available positions:
- table.insert( availablePositions, map.startPositions[i] )
- i = i + 1
- end
- end
- for id, u in pairs( server:getUsers() ) do
- if u.customData.ingame then
- local col = {
- u.customData.red,
- u.customData.green,
- u.customData.blue,
- 255
- }
- local x, y = 0,0
- local startPosNum = math.random(1,#availablePositions)
- if availablePositions[startPosNum] then
- x, y = availablePositions[startPosNum].x, availablePositions[startPosNum].y
- table.remove( availablePositions, startPosNum )
- end
- map:newCar( u.id, x, y, col )
- server:send( CMD.NEW_CAR, u.id .. "|" .. x .. "|" .. y )
- else
- -- If the client is not racing this round, let him look like he's
- -- already moved:
- server:setUserValue( u, "moved", true )
- end
- server:setUserValue( u, "crashed", false )
- server:setUserValue( u, "numCrashes", 0 )
- server:setUserValue( u, "topCrashSpeed", 0 )
- server:setUserValue( u, "speed", 0 )
- server:setUserValue( u, "maxSpeed", 0 )
- end
- game.crashedUsers = {}
- -- Start the round after 3 seconds!
- self.timerEvent = function()
- game:startMovementRound()
- end
- self.maxTime = 3
- self.time = 0
- server:send( CMD.SERVERCHAT,
- "Game starting. You have " .. ROUND_TIME .. " seconds for each move." )
- updateAdvertisementInfo()
- end
- if not DEDICATED then
- ui:setActiveScreen( nil )
- stats:clear()
- -- Do a cool camera startup swing:
- map:camSwingAbort()
- map:camSwingToPos( map.startProjPoint.x, map.startProjPoint.y, 1.5 )
- map:camZoom( 0.6, 1.5 )
- self.timerEvent2 = function()
- if client then
- game:camToCar( client:getID() )
- end
- end
- self.maxTime2 = 2
- self.time2 = 0
- ui:setActiveScreen( scr )
- end
- end
- function game:camToCar( id )
- if client then
- if map:hasCar( id ) then
- local x, y = map:getCarPos( id )
- x = x*GRIDSIZE
- y = y*GRIDSIZE
- map:camSwingToPos( x, y, 1 )
- map:camZoom( 0.5, 1 )
- end
- end
- end
- function game:update( dt )
- map:update( dt )
- -- Timer1:
- if self.maxTime > 0 then
- self.time = self.time + dt
- if self.time >= self.maxTime then
- self.maxTime = 0
- self.time = 0
- self.timerEvent()
- end
- end
- -- Timer2:
- if self.maxTime2 > 0 then
- self.time2 = self.time2 + dt
- if self.time2 >= self.maxTime2 then
- self.maxTime2 = 0
- self.time2 = 0
- self.timerEvent2()
- end
- end
- end
- function game:draw()
- if client then
- map:draw()
- if self.GAMESTATE == "move" then
- map:drawTargetPoints( client:getID() )
- end
- --[[if love.keyboard.isDown( "space" ) then
- map:drawCarInfo()
- end]]
- game:drawUserList()
- if game.winnerID then
- local users = network:getUsers()
- if users and users[game.winnerID] then
- self.winnerPanel:draw()
- love.graphics.setColor( 64,255,64, 255 )
- love.graphics.printf( users[game.winnerID].playerName .. " wins the round!",
- love.graphics.getWidth()/3, love.graphics.getHeight()/2-95,
- love.graphics.getWidth()/3, "center" )
- end
- end
- map:drawDebug()
- end
- end
- function game:drawUserList()
- -- Print list of users:
- local users, num = network:getUsers()
- local x, y = 20, 60
- local i = 1
- if client and users then
- for k, u in pairs( users ) do
- self.userPanel:draw( x - 5, y - 3 )
- love.graphics.setColor( 255,255,255, 255 )
- love.graphics.printf( i .. ":", x, y, 20, "right" )
- love.graphics.printf( u.playerName, x + 25, y, 250, "left" )
- local dx = love.graphics.getFont():getWidth( u.playerName ) + 40
- local lapString = ""
- if map:hasCar( u.id ) then
- local speedString = (u.customData.speed or 0).. " km/h"
- love.graphics.print( speedString, x + dx, y )
- dx = dx + love.graphics.getFont():getWidth( speedString ) + 15
- lapString = "Lap: " .. map:getCarRound( u.id ) .. "/" .. (self.numberOfLaps or LAPS)
- love.graphics.print( lapString, x + dx, y )
- end
- -- Show crashed users in list:
- if u.customData.crashed == true then
- love.graphics.setColor( 255, 128, 128, 255 )
- dx = dx + love.graphics.getFont():getWidth( lapString ) + 20
- local rounds = u.customData.waitingRounds or 1
- love.graphics.print( "[Crashed! (" .. rounds .. ")]", x + dx, y )
- elseif not u.customData.moved == true then
- love.graphics.setColor( 255, 255, 128, 255 )
- dx = dx + love.graphics.getFont():getWidth( lapString ) + 20
- love.graphics.print( "[Waiting for move]", x + dx, y )
- elseif not u.customData.ingame == true then
- love.graphics.setColor( 128, 128, 255, 255 )
- dx = dx + love.graphics.getFont():getWidth( lapString )
- love.graphics.print( "[spectate]", x + dx, y )
- end
- if map:hasCar( u.id) then
- map:getCar( u.id ):drawOnUI( 435, y + 5, 0.2 )
- end
- y = y + 20
- i = i + 1
- end
- end
- end
- function game:keypressed( key )
- end
- function game:mousepressed( x, y, button )
- if button == "l" then
- if client then
- if self.GAMESTATE == "move" then
- -- Turn screen coordinates into grid coordinates:
- local gX, gY = map:screenToGrid( x, y )
- gX = math.floor( gX + 0.5 )
- gY = math.floor( gY + 0.5 )
- print( "Grid position clicked:", gX, gY )
- if map:isThisAValidTargetPos( client:getID(), gX, gY ) then
- print("\t->is valid.")
- self:sendNewCarPosition( gX, gY )
- end
- end
- end
- end
- end
- function game:setState( state )
- self.GAMESTATE = state
- --[[if self.GAMESTATE == "move" then
- if client then
- map:resetCarNextMovement( client:getID() )
- end
- end]]
- end
- function game:newCar( msg )
- if not server then
- local id, x, y = msg:match( "(.*)|(.*)|(.*)")
- id = tonumber(id)
- x = tonumber(x)
- y = tonumber(y)
- local users = client:getUsers()
- local u = users[id]
- if u then
- local col = {
- u.customData.red,
- u.customData.green,
- u.customData.blue,
- 255
- }
- map:newCar( id, x, y, col )
- end
- end
- end
- function game:sendNewCarPosition( x, y )
- -- CLIENT ONLY!
- if client then
- client:send( CMD.MOVE_CAR, x .. "|" .. y )
- print("\t->Sent:", CMD.MOVE_CAR, x .. "|" .. y, os.time())
- --map:setCarNextMovement( client:getID(), x, y )
- end
- end
- function game:startMovementRound()
- --SERVER ONLY!
- if server then
- self.SERVERGAMESTATE = "move"
- game.usersMoved = {}
- for k, u in pairs( server:getUsers() ) do
- -- On all crashed users, count one down because we're starting a new round...
- if game.crashedUsers[u.id] then
- -- If this is the first crash round:
- --[[if game.crashedUsers[u.id] == SKIP_ROUNDS_ON_CRASH + 1 then
- if math.random(20) == 1 then
- local i = math.random(#tease)
- server:send( CMD.SERVERCHAT, tease[i] .. u.playerName .. tease2[i] )
- end
- end]]
- game.crashedUsers[u.id] = game.crashedUsers[u.id] - 1
- if game.crashedUsers[u.id] <= 0 then
- -- If I've waited long enough, let me rejoin the game:
- server:setUserValue( u, "crashed", false )
- game.crashedUsers[u.id] = nil
- end
- end
- -- If a user crashed, let everyone know:
- if game.crashedUsers[u.id] then
- server:setUserValue( u, "waitingRounds", game.crashedUsers[u.id] )
- server:setUserValue( u, "crashed", true )
- -- Consider this user to be finished...
- game.usersMoved[u.id] = true
- else
- -- Only let users move if they haven't crashed and aren't spectating:
- if u.customData.ingame then
- server:send( CMD.GAMESTATE, "move", u )
- server:setUserValue( u, "moved", false )
- end
- end
- end
- self.timerEvent = function() game:roundTimeout() end
- self.maxTime = ROUND_TIME
- self.time = 0
- print("----------------------------------------")
- print("New round starting:", os.time())
- -- If all users crashed, continue:
- game:checkForRoundEnd()
- end
- end
- function game:roundTimeout()
- local found = false
- if server then
- for k, u in pairs( server:getUsers() ) do
- -- If the user did not move their car in time, move it according to last velocity:
- if not self.usersMoved[u.id] and u.customData.ingame == true then
- print("Round timeout for user:", u.id )
- print("\tUser Data:")
- for k, v in pairs( u.customData ) do
- print("\t", k, v)
- end
- local x, y = map:getCarCenterVel( u.id )
- -- Check for crashes:
- game:validateCarMovement( u.id, x, y )
- found = true
- end
- end
- if found then
- server:send( CMD.SERVERCHAT, "Server: Time up. Moving on..." )
- end
- end
- end
- function game:moveAll()
- if server then
- for k, u in pairs( server:getUsers() ) do
- if u.customData.ingame and map:hasCar( u.id ) then
- --local x, y = map:getCarPos( u.id )
- local x,y = self.newUserPositions[u.id].x, self.newUserPositions[u.id].y
- server:send( CMD.MOVE_CAR, u.id .. "|" .. x .. "|" .. y )
- -- Calculate and send car speed:
- local car = map:getCar( u.id )
- local vX = x - map:TransCoordPtG(car.x)
- local vY = y - map:TransCoordPtG(car.y)
- local speed = math.floor( math.sqrt(vX*vX + vY*vY)*100 )/10
- server:setUserValue( u, "speed", speed )
- if not u.customData.maxSpeed or u.customData.maxSpeed < speed then
- server:setUserValue( u, "maxSpeed", speed )
- end
- if DEDICATED then
- map:setCarPosDirectly( u.id, x, y )
- end
- end
- end
- self.timerEvent = function()
- game:checkForWinner()
- if not game.winnerID then
- game:startMovementRound()
- else
- game:sendWinner( game.winnerID )
- local winner = server:getUsers()[game.winnerID]
- if winner then
- server:setUserValue( winner, "roundsWon", winner.customData.roundsWon + 1 )
- end
- self.timerEvent = game.sendBackToLobby
- self.maxTime = 5
- self.time = 0
- if DEDICATED then
- if server:getUsers()[game.winnerID] and
- server:getUsers()[game.winnerID].playerName then
- utility.log( "[" .. os.time() .. "] Winner: " ..
- server:getUsers()[game.winnerID].playerName )
- end
- end
- end
- end
- self.maxTime = 1.2
- self.time = 0
- end
- end
- function game:validateCarMovement( id, x, y )
- --SERVER ONLY!
- if server then
- -- if this user has not moved yet:
- if self.usersMoved[id] == nil then
- -- map:setCarPos( id, x, y )
- --map:setCarPosDirectly(id, x, y) --car-id as number, pos as Gridpos
- local oldX, oldY = map:getCarPos( id )
- local user = server:getUsers()[id]
- print("\tValidating at:", os.time())
- print("\tPrevious positions:", id, map:getCar(id), oldX, oldY)
- if map:isThisAValidTargetPos( id, x, y ) then
- print("\tPossition is valid.")
- else
- print("\tPossition is NOT valid. Traceback:", debug.traceback())
- server:send( CMD.SERVERCHAT,
- "DEBUG: Something went wrong. " .. server:getUsers()[id].playerName .. "'s movement was invalid.")
- end
- if not oldX or not oldY then
- print("\tWARNING: oldX or oldY aren't set!", debug.traceback())
- server:send( CMD.SERVERCHAT, "DEBUG: somthing went wrong with the car position of " .. server:getUsers()[id].playerName .. "." )
- oldX, oldY = 0, 0
- end
- -- Step along the path and check if there's a collision. If so, stop there.
- local p = {x = oldX, y = oldY }
- local diff = {x = x-oldX, y = y-oldY}
- local dist = utility.length( diff )
- local speed = math.floor( dist*100 )/10
- print("\tDelta:", diff.x, diff.y)
- print("\tSpeed:", speed )
- diff = utility.normalize(diff)
- print("\tDist:", dist)
- -- Step forward in steps of 0.5 length - this makes sure no small gaps are jumped!
- local crashed, crashSiteFound = false, false
- local movedDist = 0
- for l = 0.5, dist, 0.5 do
- p = {x = oldX + l*diff.x, y = oldY + l*diff.y }
- if not map:isPointOnRoad( p.x*GRIDSIZE, p.y*GRIDSIZE, 0 ) then
- crashed = true
- break
- end
- movedDist = l
- end
- -- Also check the end position!!
- if not crashed then
- -- I have managed to move the entire distance!
- movedDist = dist
- if not map:isPointOnRoad( x*GRIDSIZE, y*GRIDSIZE, 0 ) then
- crashed = true
- end
- end
- local crashedIntoCar = false
- -- If I managed to move the full distance, then check if there's already a car there
- if movedDist == dist then
- for id2, bool in pairs( self.usersMoved ) do
- if bool then
- if self.newUserPositions[id2] then
- if self.newUserPositions[id2].x == x and
- self.newUserPositions[id2].y == y then
- crashedIntoCar = true
- crashed = true
- break
- end
- end
- end
- end
- end
- if crashed then
- print("\tCrashed")
- -- Step backwards:
- for lBack = movedDist-0.5, 0, -0.5 do
- p = {x = oldX + lBack*diff.x, y = oldY + lBack*diff.y }
- p.x = math.floor(p.x)
- p.y = math.floor(p.y)
- if map:isPointOnRoad( p.x*GRIDSIZE, p.y*GRIDSIZE, 0 ) then
- crashSiteFound = true
- x, y = p.x, p.y
- break
- end
- end
- -- remembers how many rounds the user has to wait
- if crashedIntoCar then
- game.crashedUsers[id] = SKIP_ROUNDS_CAR_CAR + 1
- else
- game.crashedUsers[id] = game:speedToCrashTimeout( speed )
- end
- if user then
- server:setUserValue( user, "numCrashes", user.customData.numCrashes + 1 )
- if speed > user.customData.topCrashSpeed then
- server:setUserValue( user, "topCrashSpeed", speed )
- end
- end
- end
- if crashed and not crashSiteFound then
- x, y = oldX, oldY
- print("\tNo crash site found, placed to old pos:", oldX, oldY)
- end
- self.usersMoved[id] = true
- self.newUserPositions[id] = {x=x, y=y}
- if user then
- -- tell this user to wait!
- server:send( CMD.GAMESTATE, "wait", user )
- -- Let all users know this user has already moved:
- server:setUserValue( user, "moved", true )
- end
- game:checkForRoundEnd()
- end
- end
- end
- function game:checkForRoundEnd()
- -- Check if all users have sent their move:
- local doneMoving = true
- for k, u in pairs( server:getUsers() ) do
- if u.customData.ingame and not self.usersMoved[u.id] then
- doneMoving = false
- break
- end
- end
- -- If all users have sent the move, go on to next round:
- if doneMoving then
- self:moveAll()
- end
- end
- -- This function checks for a winner. A winner is found if a user has passed the startline
- -- LAP times. If more than one players have done so, the winner is the one who is _furthest_
- -- from the start line (i.e. crossed the line with the most speed/first)
- function game:checkForWinner()
- if server and not game.winnerID then
- local potentialWinners = {}
- for k, u in pairs( server:getUsers() ) do
- if u.customData.ingame then
- if map:getCarRound( u.id ) >= LAPS + 1 then
- table.insert( potentialWinners, u.id )
- end
- end
- end
- -- Find the winner who has the least
- local winnerID
- local maxDist = -math.huge
- for k, id in pairs( potentialWinners ) do
- local car = map:getCar( id )
- if car then
- local x,y = car:getPos()
- local p = {x=x, y=y}
- local dist = utility.linePointDist( map.startLine.p1,map.startLine.p2, p )
- print( x, y, map.startLine.p1.x, map.startLine.p1.y )
- if dist > maxDist then -- so far the first player:
- winnerID = id
- maxDist = dist
- end
- end
- end
- if winnerID then
- game.winnerID = winnerID
- print("WINNER FOUND!", game.winnerID )
- else
- -- Fallback, just in case:
- if #potentialWinners > 0 then
- game.winnerID = potentialWinners[1]
- end
- end
- end
- end
- function game:moveCar( msg )
- -- CLIENT ONLY!
- if client then
- local id, x, y = msg:match( "(.*)|(.*)|(.*)" )
- id = tonumber(id)
- x = tonumber(x)
- y = tonumber(y)
- map:setCarPos( id, x, y )
- end
- end
- function game:playerWins( msg )
- if client then
- game.winnerID = tonumber(msg)
- game:camToCar( game.winnerID )
- self.timerEvent2 = game.zoomOut
- self.maxTime2 = 3
- self.time2 = 0
- end
- end
- function game:sendWinner()
- if server then
- server:send( CMD.PLAYER_WINS, game.winnerID )
- end
- end
- function game:sendBackToLobby()
- if server then
- -- Create and send statustics:
- game:sendStats()
- server:send( CMD.BACK_TO_LOBBY, "" )
- if DEDICATED then
- lobby:show() -- must be called AFTER generating the stats!
- end
- end
- end
- function game:sendStats()
- -- SERVER ONLY!
- if not server then return end
- local users = network:getUsers()
- -- Create and send top speed string:
- local statStr = "Top Speed:|km/h|"
- for k, u in pairs( users ) do
- if u.customData.ingame then
- statStr = statStr .. u.id .. " " .. u.customData.maxSpeed .. "|"
- end
- end
- server:send( CMD.STAT, statStr )
- -- Create and send crashes string:
- local statStr = "Crashes:||"
- for k, u in pairs( users ) do
- if u.customData.ingame then
- statStr = statStr .. u.id .. " " .. u.customData.numCrashes .. "|"
- end
- end
- server:send( CMD.STAT, statStr )
- -- Create and send top speed at crashes:
- local statStr = "Worst crash:|km/h|"
- for k, u in pairs( users ) do
- if u.customData.ingame then
- statStr = statStr .. u.id .. " " .. u.customData.topCrashSpeed .. "|"
- end
- end
- server:send( CMD.STAT, statStr )
- -- Create and send top speed at crashes:
- local statStr = "Rounds won:||"
- for k, u in pairs( users ) do
- if u.customData.ingame then
- statStr = statStr .. u.id .. " " .. u.customData.roundsWon .. "|"
- end
- end
- server:send( CMD.STAT, statStr )
- end
- function game:zoomOut()
- local cX = map.Boundary.minX + (map.Boundary.maxX - map.Boundary.minX)*0.5
- local cY = map.Boundary.minY + (map.Boundary.maxY - map.Boundary.minY)*0.5
- map:camSwingToPos( cX, cY )
- end
- function game:synchronizeCars( user )
- if server then
- for k, u in pairs( server:getUsers() ) do
- if u.customData.ingame then
- if map:hasCar( u.id ) then
- local c = map:getCar( u.id )
- server:send( CMD.NEW_CAR, u.id .. "|" .. c.targetX .. "|" .. c.targetY, user )
- end
- end
- end
- end
- end
- function game:speedToCrashTimeout( speed )
- return SKIP_ROUNDS_COLLISION_MIN + math.floor(speed*SKIP_ROUNDS_COLLISION_PER_10_KMH/10) + 1
- end
- function game:getNumUsersPlaying()
- local num = 0
- for k, u in pairs( server:getUsers() ) do
- if u.customData.ingame == true then
- num = num + 1
- end
- end
- return num
- end
- function game:close()
- local commands = {}
- commands[1] = { txt = "Yes", key = "y", event = function() network:closeConnection() end }
- commands[2] = { txt = "No", key = "n" }
- scr:newMsgBox( "Game in progress!", "Are you sure you want to leave?", nil, nil, nil, commands)
- end
- return game
|