403 lines
7.7 KiB
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;
|
|
}
|
|
}
|