Пример #1
0
// subscribe subscribes to the topic for this game
// Returns a channel of Messages that can be used to receive messages
// Make sure to send it a new redis.Conn. It will handle closing it
// when finished.
func subscribe(ctx context.Context, con redis.Conn, g *Game) (<-chan *message, error) {
	lc := "Subscribe"
	c := make(chan *message)

	psc := redis.PubSubConn{con}

	logger.Info(ctx, lc, "Subscribing to topic '%v'", g.ID)

	err := psc.Subscribe(g.ID)
	if err != nil {
		logger.Info(ctx, lc, "Error Subscribing. %v", err)
		close(c)
		return c, err
	}

	go func(c chan<- *message) {
		defer con.Close()
		defer close(c)
		for {
			switch v := psc.Receive().(type) {
			case redis.Message:
				msg := new(message)
				err := msg.unmarshalGob(v.Data)
				if err != nil {
					logger.Error(ctx, lc, "Could not decode message. Closing channel. %v, %v", v, err)
					return
				}

				logger.Info(ctx, lc, "Received Message. Sending %#v to channel.", msg)

				c <- msg

				// special case for LostMessage, since we know to close at this point.
				if msg.Type == lostMessage {
					logger.Info(ctx, lc, "Lost Message, Closing Subscribe Pipeline.")
					return
				}
			case error:
				logger.Error(ctx, lc, "Error processing messages. Closing channel. %v", err)
				return
			default:
				logger.Info(ctx, lc, "Received unknown message. Ignored: %#v", v)
			}
		}
	}(c)

	return c, nil
}
Пример #2
0
// findGame finds a game in the list of open games. If one doesn't exist, creates a new gameid
// returns a new Game and if it's a new game or not.
func findGame(ctx context.Context, con redis.Conn) (*Game, bool, error) {
	lc := "FindGame"

	// do we have an open game?
	gameID, err := redis.String(con.Do("RPOP", openGames))

	// ignore nil errors, since that is expected
	if err != nil && err != redis.ErrNil {
		logger.Error(ctx, lc, "Error finding open game: %v", err)
		return new(Game), false, err
	}

	// is this a brand new game?
	isNew := (gameID == "")

	if isNew {
		logger.Info(ctx, lc, "Could not find open game, creating one... ")
		u, err := uuid.NewV4()
		if err != nil {
			return nil, false, err
		}
		gameID = u.String()
	}

	return NewGame(gameID), isNew, nil
}
Пример #3
0
// ensureSubscribers Make sure n number of Game subscriptions at this point.
// Blocks until we have two people. Times out on too many retries.
func ensureSubscribers(ctx context.Context, con redis.Conn, g *Game, n int) error {
	lc := "EnsureSubscribers"

	for i := 0; i <= 5; i++ {
		res, err := con.Do("PUBSUB", "NUMSUB", g.ID)

		if err != nil {
			logger.Error(ctx, lc, "Error getting number of subscriptions: %v", err)
			return err
		}

		vals, err := redis.Values(res, err)

		if err != nil {
			logger.Error(ctx, lc, "Error converting to values: %v", err)
			return err
		}

		if l := len(vals); l != 2 {
			err := fmt.Errorf("Should only be two items in the result. Weird. %#v. %v.", vals, l)
			logger.Error(ctx, lc, err.Error())
			return err
		}

		count, ok := vals[1].(int64)

		if !ok {
			val := vals[1]
			err := fmt.Errorf("Second value should be an integer. %T %#v", val, val)
			logger.Error(ctx, lc, err.Error())
			return err
		}

		logger.Info(ctx, lc, "Found %v subscriptions for Game. Require %v", count, n)

		if int(count) == n {
			return nil
		}

		logger.Info(ctx, lc, "Could not find enough subscriptions, retrying...")
		time.Sleep(100 * time.Millisecond)
	}

	err := errors.New("Timeout attempting to ensure subscriber count of " + strconv.Itoa(n))
	logger.Error(ctx, lc, err.Error())
	return err
}
Пример #4
0
// beginHandler Streams BEGIN to client once we are good to go.
func beginHandler(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer, msg *message) error {
	lc := "beginHandler"
	ctx := stream.Context()
	res := &Response{Event: &Response_Turn{Turn: Response_BEGIN}}

	err := sendResponse(stream, res)

	if err != nil {
		logger.Error(ctx, lc, "Error sending BEGIN event. %v", err)
		return err
	}

	// if not the player that BEGAN (so first player to join), then START your turn
	if msg.Player == player.Id {
		logger.Info(ctx, lc, "Publishing end turn %v", stopTurnMessage)
		return publish(ctx, con, game, message{Player: player.Id, Type: stopTurnMessage, Data: msg.Data})
	}

	logger.Info(ctx, lc, "Not doing anything with Begin. It's not my job.")
	return nil
}
Пример #5
0
// sendResponse Sends a request.
func sendResponse(stream SimonSays_GameServer, r *Response) error {
	lc := "Response"
	ctx := stream.Context()
	logger.Info(ctx, lc, "Sending response: %v", r)
	err := stream.Send(r)

	if err != nil {
		logger.Error(ctx, lc, "Error sending: %v", err)
	}

	return err
}
Пример #6
0
// handle Processing pub/sub events and does things with them
// Think "controller".
func handle(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer, msg *message) error {
	lc := "Handler"
	ctx := stream.Context()
	logger.Info(ctx, lc, "Handling Message: %#v", msg)
	fn, ok := handlers[msg.Type]

	if !ok {
		logger.Error(ctx, lc, "Could not find a handler for this event. %#v", msg)
		return handlerNotFoundError("msg.Type")
	}

	return fn(con, game, player, stream, msg)
}
Пример #7
0
// sendLightupEvent sends out a lightup event to everyone.
func sendLightupEvent(press *Request_Press, stream SimonSays_GameServer, con redis.Conn, game *Game) error {
	buf := new(bytes.Buffer)
	err := gob.NewEncoder(buf).Encode(press.Press)
	ctx := stream.Context()

	if err != nil {
		logger.Info(ctx, "sendLightupEvent", "Error gob encoding Color. %v, %v", press.Press, err)
		return err
	}

	// send out lightup events.
	return publish(ctx, con, game, message{Type: lightUpMessage, Data: buf.Bytes()})
}
Пример #8
0
// handleColorPress handles one color being pressed.
// If it's the player turn it modifies the given game and sends a lightUpMessage to Redis.
// This function is thread safe.
func handleColorPress(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer) (bool, error) {
	lc := "handleColorPress"
	ctx := stream.Context()
	press, err := receivePressRequest(stream)

	if err != nil {
		logger.Error(ctx, lc, "Press Error. Sending to err channel, and shutting down %v", err)
		return true, err
	}

	logger.Info(ctx, lc, "Press Received: %v", press)

	//lock the game for this entire block, since we are doing lots of things with
	//it, and this will prevent any concurrency issues.
	game.mu.Lock()
	defer game.mu.Unlock()

	// only accept input when it is my turn!
	if !game.isMyTurn() {
		logger.Info(ctx, lc, "Not my turn. Ignored press.")
		return false, nil
	}

	err = game.pressColor(press.Press)
	if err == ErrColorPressedOutOfTurn {
		logger.Info(ctx, lc, "Colour pressed out of turn. Ignored.")
	} else if err != nil {
		return true, err
	}

	err = sendLightupEvent(press, stream, con, game)
	if err != nil {
		return true, err
	}

	// When you reach the point that the game has turned.
	return handleEndOfTurn(stream, con, game, player)
}
Пример #9
0
// receiveRequest receives a request.
func receiveRequest(stream SimonSays_GameServer) (*Request, error) {
	lc := "Request"
	ctx := stream.Context()
	req, err := stream.Recv()

	if err != nil {
		logger.Error(ctx, lc, "Error recieving: %v", err)
		return nil, err
	}

	logger.Info(ctx, lc, "Received: %v", req)

	return req, nil
}
Пример #10
0
// lightUpHandler handles LIGHTUP events, letting everyone know to lightup
// their colours.
func lightUpHandler(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer, msg *message) error {
	lc := "lightUpHandler"
	ctx := stream.Context()
	c := new(Color)
	buf := bytes.NewBuffer(msg.Data)

	err := gob.NewDecoder(buf).Decode(c)

	if err != nil {
		logger.Error(ctx, lc, "Could not convert colour. %#v. %v", msg, err)
		return err
	}

	logger.Info(ctx, lc, "Sending stream response to Light Up %v", c)

	return sendResponse(stream, &Response{Event: &Response_Lightup{Lightup: *c}})
}
Пример #11
0
// publish publishes a message to the game's topic.
func publish(ctx context.Context, con redis.Conn, g *Game, msg message) error {
	lc := "Publish"

	logger.Info(ctx, lc, "Sending message: %#v, to topic: '%v'", msg, g.ID)

	data, err := msg.marshalGob()

	if err != nil {
		logger.Error(ctx, lc, "Error encoding message. %#v, %v", msg, err)
		return err
	}

	_, err = con.Do("PUBLISH", g.ID, data)

	if err != nil {
		logger.Error(ctx, lc, "Error publishing message. %#v, %v", msg, err)
	}

	return err
}
Пример #12
0
// what happens when the game is lost. Returns io.EOF to show that the game should be shut down.
func lostHandler(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer, msg *message) error {
	lc := "lostHandler"
	ctx := stream.Context()

	logger.Info(ctx, lc, "Received Lost Event: %#v", msg)

	var err error

	// if I lost...
	turn := Response_WIN
	if msg.Player == player.Id {
		turn = Response_LOSE
	}

	err = sendResponse(stream, &Response{Event: &Response_Turn{Turn: turn}})
	if err != nil {
		return err
	}

	return io.EOF
}
Пример #13
0
// recvPress Manages receiving Press Events through a go-routine.
// Will need it's own Redis connection, that it will close once complete.
// Sends io.EOF when the connection closes, and pushes the error into the chan if it
// occurs.
func recvPress(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer) <-chan error {
	lc := "RecvPress"
	ctx := stream.Context()
	c := make(chan error, 10)

	logger.Info(ctx, lc, "Start recieving Press events...")

	go func() {
		defer close(c)
		defer con.Close()
		for {
			stop, err := handleColorPress(con, game, player, stream)
			if err != nil {
				c <- err
				return
			} else if stop {
				return
			}
		}
	}()

	return c
}
Пример #14
0
// stopTurnHandler My turn has finished, so, tell the other player
// to START_TURN, and me to END_TURN.
func stopTurnHandler(con redis.Conn, game *Game, player *Request_Player, stream SimonSays_GameServer, msg *message) error {
	lc := "stopTurnHandler"
	ctx := stream.Context()

	// if I'm the player that sent out the message, let the client know
	if player.Id == msg.Player {
		return sendResponse(stream, &Response{Event: &Response_Turn{Turn: Response_STOP_TURN}})
	}

	// otherwise, it's time for the other player to start

	buf := bytes.NewBuffer(msg.Data)
	c := []Color{}
	err := gob.NewDecoder(buf).Decode(&c)

	if err != nil {
		logger.Error(ctx, lc, "Error decoding message colors. %#v. %v", msg, err)
		return err
	}

	logger.Info(ctx, lc, "Starting turn with colors: %v", c)
	game.StartTurn(c)
	return sendResponse(stream, &Response{Event: &Response_Turn{Turn: Response_START_TURN}})
}
Пример #15
0
// handleEndOfTurn handles if it is the end of the turn, and if the player has lost (bool).
func handleEndOfTurn(stream SimonSays_GameServer, con redis.Conn, game *Game, player *Request_Player) (bool, error) {
	lc := "handleEndOfTurn"
	ctx := stream.Context()

	// if not my turn, exit early.
	if game.isMyTurn() {
		return false, nil
	}

	if game.match() {
		b, err := game.encodePresses()
		if err != nil {
			logger.Error(ctx, lc, "Error encoding presses: %#v, %v", game, err)
			return false, err
		}

		msg := message{Type: stopTurnMessage, Player: player.Id, Data: b}
		if err := publish(ctx, con, game, msg); err != nil {
			logger.Error(ctx, lc, "error publishing StopTurnMessage %#v, %v", msg, err)
			return false, err
		}

		return false, nil
	}

	// if there is no match, you did something wrong. otherwise, my friend, you have lost the game.
	msg := message{Type: lostMessage, Player: player.Id}
	if err := publish(ctx, con, game, msg); err != nil {
		logger.Error(ctx, lc, "error publishing LostMessage %#v, %v", msg, err)
		return false, err
	}

	// and we are done taking input - end of game!
	logger.Info(ctx, lc, "We are done taking input. Returning that we have lost. %#v", game)
	return true, nil
}
Пример #16
0
// closeOpenGame make sure the open game is removed
// from the open game list.
func closeOpenGame(ctx context.Context, con redis.Conn, g *Game) error {
	logger.Info(ctx, "CloseOpenGame", "Removing open game %v", g.ID)
	_, err := con.Do("LREM", openGames, 1, g.ID)
	return err
}
Пример #17
0
// addOpenGame Adds an open game to the list.
func addOpenGame(ctx context.Context, con redis.Conn, g *Game) error {
	logger.Info(ctx, "AddOpenGame", "Adding open game %v", g.ID)
	_, err := con.Do("LPUSH", openGames, g.ID)
	return err
}
Пример #18
0
// Game function is an implementation of the gRPC Game Service.
// When connected, this is the main functionality of running a
// Game for the connected player.
func (s *SimonSays) Game(stream SimonSays_GameServer) error {
	ctx := stream.Context()
	defer logger.Clear(ctx)

	lc := "Game"
	// first let's get the player
	req, err := receiveRequest(stream)
	if err != nil {
		return err
	}
	player := req.GetJoin()
	if player == nil {
		logger.Error(ctx, lc, "Player was nil on initial join request. %v", req)
		return errors.New("Player was nil on initial join request.")
	}
	logger.Set(ctx, "Player", player.Id)
	logger.Info(ctx, lc, "Player %#v is attempting to join.", player)

	// find what game to join
	con := s.pool.Get()
	defer con.Close()
	game, isNew, err := findGame(ctx, con)

	if err != nil {
		return err
	}
	logger.Set(ctx, "Game", game.ID)
	logger.Info(ctx, lc, "Connecting to game %v. New?: %v", game.ID, isNew)

	logger.Info(ctx, lc, "Start to receive PubSub messages")

	// make sure that you always unjoin, if something happens to go wrong.
	defer func() {
		con := s.pool.Get()
		err := closeOpenGame(ctx, con, game)
		if err != nil {
			logger.Error(ctx, lc, "Error attempting to close game. %v", err)
		}
		err = con.Close()
		if err != nil {
			logger.Error(ctx, lc, "Error closing close open game connection. %v", err)
		}
	}()

	// make sure that at the end, you always unsubscribe.
	defer func() {
		con := s.pool.Get()
		err := redis.PubSubConn{con}.Unsubscribe(game.ID)
		if err != nil {
			logger.Error(ctx, lc, "Error unsubscribing from Game Topic %v, %v", game.ID, err)
		}
		err = con.Close()
		if err != nil {
			logger.Error(ctx, lc, "Error closing unsubscribe connection. %v", err)
		}

	}()

	msgs, err := subscribe(ctx, s.pool.Get(), game)

	if err != nil {
		return err
	}

	if err := connectGame(ctx, con, game, player, isNew); err != nil {
		return err
	}

	// subscribe to incoming key events, and get back a channel of errors.
	perrs := recvPress(s.pool.Get(), game, player, stream)

	for {
		select {

		// process incoming messages from RedisPubSub, and send messages.
		case msg := <-msgs:
			if msg == nil {
				logger.Error(ctx, lc, "Message Channel has closed. Exiting.")
				return nil
			}

			logger.Info(ctx, lc, "Handling incoming messsage...")

			err := handle(con, game, player, stream, msg)
			if err != nil {
				// if we are EOF, then simply exit.
				if err == io.EOF {
					logger.Info(ctx, lc, "[Game] EOF. Closing connection.")
					return nil
				}
				return err
			}

		// check to see if there are any issues with press errors.
		case err := <-perrs:
			// remember, a closed channel, will return a nil err.
			if err != nil {
				logger.Error(ctx, lc, "There was a press error. %v", err)
				return err
			}
		}
	}
}