doodul/source/doodul/game.d

403 lines
7.7 KiB
D

module doodul.game;
import std.algorithm : find, canFind, remove, map, filter;
import std.array : array;
import std.random : choice;
import vibe.vibe;
import doodul.app;
import doodul.lobby;
import doodul.player;
void gameGet(HTTPServerRequest request, HTTPServerResponse response)
{
auto lobbyId = request.params["id"];
response.redirect("/?lobbyId=" ~ lobbyId.urlEncode);
}
void gamePost(HTTPServerRequest request, HTTPServerResponse response)
{
auto lobbyId = request.params["id"];
if(!lobbies.canFind!(l => l.id == lobbyId) || lobbies.find!(l => l.id == lobbyId)[0].game is null)
{
response.redirect("/?lobbyId=" ~ lobbyId.urlEncode);
return;
}
auto nickname = request.form["nickname"];
response.render!("game.dt", lobbyId, nickname);
}
void sendGameState(Player p, Game game)
{
struct GameState
{
string drawer;
string[] players;
int numberOfRounds;
int currentRound;
bool ended;
string word;
string[] wordFinders;
int secondsLeft;
}
auto gameState = GameState(game.currentRound.drawer.nickname,
game.players.map!(p => p.nickname).array,
game.lobby.numberOfRounds,
game.currentRound.number,
game.ended,
p.role != PlayerRole.guesser ? game.currentRound.currentWord : game.currentRound.redactedWord,
game.players.filter!(p => p.role == PlayerRole.wordFinder).map!(p => p.nickname).array,
game.currentRound.secondsLeft);
p.sendMessage("setGameState", gameState.serializeToJson);
}
void wsGame(scope WebSocket socket)
{
Player player;
Game game;
Lobby lobby;
while(socket.connected)
{
string messageText;
try
{
messageText = socket.receiveText();
}
catch(WebSocketException exception)
{
break;
}
auto message = deserializeJson!WebSocketMessage(messageText);
switch(message.type)
{
case "joinGame":
auto nickname = message.data.get!string;
auto lobbyId = socket.request.params["id"];
auto lobbyRange = lobbies.find!(l => l.id == lobbyId);
if(lobbyRange.empty)
{
//this state is invalid
return;
}
lobby = lobbyRange[0];
auto playerRange = lobby.players.find!(p => p.nickname == nickname);
if(playerRange.empty)
{
player = new Player(nickname, null);
lobby.players ~= player;
}
else
{
player = playerRange[0];
}
player.socket = socket;
game = lobby.game;
game.players ~= player;
if(game.players.length != lobby.players.length)
{
continue;
}
if(!game.started)
{
game.start();
}
else
{
game.currentRound.playersLeft ~= player;
player.role = PlayerRole.guesser;
}
foreach(p; game.players)
{
sendGameState(p, game);
}
break;
case "setStartPoint":
case "setEndPoint":
case "setBrushSize":
case "setBrushColour":
case "setErasing":
case "draw":
case "clearCanvas":
if(player.role != PlayerRole.drawer || game.currentRound.currentWord.empty)
{
continue;
}
foreach(guesser; game.players.filter!(p => p.role != PlayerRole.drawer))
{
guesser.socket.send(messageText);
}
break;
case "makeGuess":
auto guessedWord = message.data.get!string;
if(guessedWord.empty || player.role != PlayerRole.guesser)
{
continue;
}
if(guessedWord == game.currentRound.currentWord)
{
player.role = PlayerRole.wordFinder;
foreach(p; game.players)
{
sendGameState(p, game);
}
if(game.players.filter!(p => p.role == PlayerRole.guesser).empty)
{
game.currentRound.nextDrawer();
}
}
else
{
struct Guess
{
string guesser;
string guess;
}
auto guess = Guess(player.nickname, guessedWord);
foreach(p; game.players)
{
p.sendMessage("addGuess", guess.serializeToJson);
}
}
break;
case "sendChatMessage":
auto chatMessageText = message.data.get!string;
if(chatMessageText.empty || player.role == PlayerRole.guesser)
{
continue;
}
struct ChatMessage
{
string sender;
string text;
}
auto chatMessage = ChatMessage(player.nickname, chatMessageText);
foreach(p; game.players.filter!(p => p.role != PlayerRole.guesser))
{
p.sendMessage("addChatMessage", chatMessage.serializeToJson);
}
break;
case "chooseWord":
if(player.role != PlayerRole.drawer)
{
continue;
}
auto word = message.data.get!string;
if(!game.currentRound.wordOptions.canFind(word) || game.currentRound.currentWord != "")
{
continue;
}
game.currentRound.randomWordTimer.stop();
game.currentRound.currentWord = word;
game.currentRound.timer = setTimer(1.seconds, &game.currentRound.tick, true);
foreach(p; game.players.filter!(p => p.role != PlayerRole.drawer))
{
p.role = PlayerRole.guesser;
}
foreach(p; game.players)
{
sendGameState(p, game);
}
break;
case "leaveGame":
socket.close();
break;
default:
logWarn("Unknown WebSocket message type: " ~ message.type);
break;
}
}
game.players = game.players.remove!(p => p == player);
lobby.players = lobby.players.remove!(p => p == player);
game.currentRound.playersLeft = game.currentRound.playersLeft.remove!(p => p == player);
//TODO: Check if player was drawer
foreach(p; game.players)
{
sendGameState(p, game);
}
if(game.players.length == 1)
{
//TODO: If no players left, pause the game instead?
lobby.game = null;
game.players[0].sendMessage("youAreAlone", null.serializeToJson);
}
}
class Game
{
Player[] players;
int roundsLeft;
Round currentRound;
bool started;
bool ended;
Lobby lobby;
this(Lobby lobby, int numberOfRounds)
{
this.lobby = lobby;
this.roundsLeft = numberOfRounds;
}
void start()
{
started = true;
nextRound();
}
void nextRound()
{
if(roundsLeft == 0)
{
ended = true;
foreach(p; players)
{
sendGameState(p, this);
}
return;
}
currentRound = new Round(currentRound is null ? 1 : (currentRound.number + 1), this, players);
roundsLeft--;
}
}
class Round
{
int number;
Game game;
Player drawer;
Player[] playersLeft;
bool ended;
string[] wordOptions = ["apple", "lemon", "love"];
string currentWord;
int secondsLeft;
Timer timer;
Timer randomWordTimer;
this(int number, Game game, Player[] players)
{
this.number = number;
this.game = game;
this.playersLeft = players;
foreach(player; players)
{
player.role = PlayerRole.guesser;
}
nextDrawer();
}
void nextDrawer()
{
if(!currentWord.empty)
{
foreach(p; game.players)
{
p.sendMessage("revealWord", game.currentRound.currentWord.serializeToJson);
}
}
if(drawer !is null)
{
drawer.role = PlayerRole.guesser;
}
if(playersLeft.empty)
{
ended = true;
foreach(player; game.players)
{
player.role = PlayerRole.waiter;
}
game.nextRound();
return;
}
drawer = playersLeft[0];
drawer.role = PlayerRole.drawer;
playersLeft = playersLeft[1 .. $];
currentWord = "";
if(timer)
{
timer.stop();
}
secondsLeft = 80;
foreach(player; game.players)
{
player.sendMessage("setChoosingPlayer", drawer.nickname.serializeToJson);
}
drawer.sendMessage("setWordOptions", wordOptions.serializeToJson);
randomWordTimer = setTimer(15.seconds, delegate() {
game.currentRound.currentWord = choice(wordOptions);
game.currentRound.timer = setTimer(1.seconds, &game.currentRound.tick, true);
foreach(p; game.players.filter!(p => p.role != PlayerRole.drawer))
{
p.role = PlayerRole.guesser;
}
foreach(p; game.players)
{
sendGameState(p, game);
}
});
}
void tick()
{
secondsLeft--;
if(secondsLeft == 0)
{
nextDrawer();
}
}
@property
string redactedWord()
{
return currentWord.map!(c => '_').array.idup;
}
}