DungeonStart RPC: Implement EncounterService With Guide

by Henrik Larsen 56 views

Hey guys! Let's dive into implementing the EncounterService with the DungeonStart RPC. This is a crucial step in building our RPG API, and this guide will walk you through every aspect, from the initial overview to the final implementation.

Overview

The main goal here is to implement the EncounterService as defined in the protos, with a primary focus on the DungeonStart RPC. This RPC will be responsible for generating a demo room using our trusty toolkit. Remember, the service will orchestrate the process, while the underlying game logic resides within the toolkit. This separation of concerns is key to maintaining a clean and scalable architecture.

Requirements

Before we get our hands dirty with code, let's lay down some ground rules. These requirements are crucial for ensuring the service behaves as expected:

  • Use rpg-toolkit for all spatial logic: The toolkit is our source of truth for anything related to the game world's space. We won't be reinventing the wheel here, so let's leverage the power of the toolkit!
  • Service only orchestrates: This is a big one. The EncounterService should act like a conductor, directing the flow of operations but not getting bogged down in the details of game logic. Think of it as the manager, not the worker.
  • Follow established Input/Output pattern: Consistency is key. We want to adhere to our existing Input/Output patterns to make the codebase more predictable and maintainable.

Tasks

To break down this endeavor into manageable chunks, here’s a list of tasks we need to tackle:

  • [ ] Create internal/services/encounter package
  • [ ] Implement Service struct with dependencies
  • [ ] Implement DungeonStart RPC method
  • [ ] Use toolkit's GenerateDemoRoom() for room creation
  • [ ] Convert toolkit RoomData to proto Room
  • [ ] Wire service into server
  • [ ] Add tests with mocks

Let's break down each of these tasks in more detail.

Create internal/services/encounter package

First things first, we need a home for our EncounterService. This involves creating a new package within our project's internal/services directory. This keeps our service code neatly organized and separate from other parts of the application.

Implement Service struct with dependencies

The Service struct will be the heart of our EncounterService. It will hold any dependencies the service needs, such as database connections, other services, or configuration settings. Defining this struct clearly at the outset helps us manage dependencies and makes testing easier.

Implement DungeonStart RPC method

This is where the magic happens! The DungeonStart RPC method will be the entry point for clients requesting a new dungeon encounter. This method will take an input, generate a room, place characters, and return the result. We'll delve into the specifics of this method in the Implementation Plan section below.

Use toolkit's GenerateDemoRoom() for room creation

As per our requirements, we'll be using the rpg-toolkit's GenerateDemoRoom() function to create the initial room layout. This ensures consistency in room generation and leverages the existing spatial logic within the toolkit. This method will likely take dimensions as input and return a RoomData object.

Convert toolkit RoomData to proto Room

Our proto definitions define the structure of data exchanged over gRPC. Since the rpg-toolkit uses its own RoomData structure, we'll need to convert between the two. This involves mapping the fields from RoomData to the corresponding fields in our proto Room message. This conversion ensures compatibility between our internal representation and the external API.

Wire service into server

Once we've implemented the EncounterService and its methods, we need to wire it up to our gRPC server. This involves registering the service with the server and making it available to clients. This step connects our newly created service to the broader API ecosystem.

Add tests with mocks

Testing is paramount! We'll be writing tests to ensure our EncounterService behaves as expected under various conditions. This includes testing the happy path (everything goes smoothly) and error cases (things go wrong). Mocks will be crucial for isolating the service and testing its interactions with dependencies without relying on real-world systems.

Implementation Plan

Now, let's get into the nitty-gritty details of the implementation. We'll start by outlining the structure of our Service and then dive into the DungeonStart method.

Service Structure

Here's a basic outline of the Service struct and the DungeonStart method:

package encounter

import (
	"context"

	"your-project/rpg-toolkit/spatial" // Replace with actual import path
)

// DungeonStartInput defines the input for the DungeonStart RPC.
type DungeonStartInput struct {
	CharacterIDs []string
}

// DungeonStartOutput defines the output for the DungeonStart RPC.
type DungeonStartOutput struct {
	EncounterID string
	Room        *spatial.RoomData
}

// Service is the struct that implements the EncounterService.
type Service struct {
	// dependencies
	// e.g., ToolkitClient *toolkit.Client
}

// NewService creates a new EncounterService.
func NewService() *Service {
	return &Service{
		// Initialize dependencies here
	}
}

// DungeonStart is the RPC method that generates a new dungeon encounter.
func (s *Service) DungeonStart(ctx context.Context, input *DungeonStartInput) (*DungeonStartOutput, error) {
	// 1. Generate room using toolkit
	room := spatial.GenerateDemoRoom(10, 10)

	// 2. Place characters (Implementation details to be added)
	// ...

	// 3. Return output
	return &DungeonStartOutput{
		EncounterID: "unique-encounter-id", // Replace with actual ID generation
		Room:        room,
	}, nil
}

Let's break this down:

  • DungeonStartInput and DungeonStartOutput: These structs define the input and output types for our DungeonStart RPC method. This adheres to our Input/Output pattern and makes the method signature clear and concise.

  • Service struct: This struct holds the dependencies our service needs. This could include things like a client for interacting with the rpg-toolkit, database connections, or other services.

  • NewService() function: This constructor function creates a new instance of the Service struct and initializes its dependencies. This is a common pattern for dependency injection.

  • DungeonStart method: This is the core of our implementation. It takes a context and an input, generates a room using the toolkit, places characters (we'll flesh this out later), and returns an output. Let's dig deeper into each step.

    • 1. Generate room using toolkit: This is where we leverage the spatial.GenerateDemoRoom(10, 10) function from the rpg-toolkit. We're passing in 10 and 10 as the dimensions of the room, but this could be configurable in the future. The toolkit handles the spatial logic of generating the room's layout.
    • 2. Place characters: This is a placeholder for the logic that will place characters within the generated room. We'll need to consider factors like starting positions, character abilities, and room layout when implementing this. This part is crucial for the gameplay experience and requires careful design and testing. This could involve algorithms for pathfinding, collision detection, and strategic placement based on character roles or abilities.
    • 3. Return output: Finally, we construct a DungeonStartOutput containing the generated room and a unique encounter ID. The encounter ID is a placeholder for now, but we'll need to implement a proper ID generation mechanism in the future. This ID will likely be used to track the specific encounter within our system. Returning a comprehensive output allows clients to immediately start interacting with the generated room.

gRPC Handler

In addition to the service logic, we'll need a gRPC handler to bridge the gap between the proto definitions and our service implementation. This handler will be responsible for converting between proto messages and our internal service types, and vice versa. It will also handle passing the request to the service and returning the response to the client.

Here's a simplified example of what the gRPC handler might look like:

// DungeonStartHandler is the gRPC handler for the DungeonStart RPC.
func (s *server) DungeonStartHandler(ctx context.Context, req *pb.DungeonStartRequest) (*pb.DungeonStartResponse, error) {
	input := &encounter.DungeonStartInput{
		CharacterIDs: req.GetCharacterIDs(),
	}

	output, err := s.encounterService.DungeonStart(ctx, input)
	if err != nil {
		return nil, err
	}

	// Convert output.Room to proto Room
	protoRoom := convertRoomDataToProtoRoom(output.Room)

	return &pb.DungeonStartResponse{
		EncounterID: output.EncounterID,
		Room:        protoRoom,
	}, nil
}

// convertRoomDataToProtoRoom converts a spatial.RoomData to a proto Room.
func convertRoomDataToProtoRoom(roomData *spatial.RoomData) *pb.Room {
	// Implementation details for conversion
	return &pb.Room{}
}

This handler takes the proto request, extracts the character IDs, and creates an encounter.DungeonStartInput object. It then calls the DungeonStart method on our encounterService. After receiving the output, it converts the RoomData to a proto Room and constructs the proto response. Error handling is also included to ensure that any errors from the service are propagated back to the client. This handler acts as a crucial intermediary, ensuring smooth communication between the gRPC layer and our service logic.

Acceptance Criteria

To ensure we're on the right track, let's define some acceptance criteria. These are the conditions that must be met for the implementation to be considered complete:

  • [ ] DungeonStart returns a valid room with hex grid: The generated room should have a valid layout, including a hex grid structure that's usable for gameplay. This is a fundamental requirement for the functionality of the encounter.
  • [ ] Characters are placed in the room: Characters should be placed strategically within the room. This may involve initial placement algorithms to ensure fairness or create tactical advantages. This is a core part of starting an encounter, as characters need a starting point.
  • [ ] Service uses toolkit for all spatial logic: The service must rely on the rpg-toolkit for all spatial calculations and logic. This ensures consistency and avoids duplication of effort. The toolkit is the authority on spatial matters, so we defer to it.
  • [ ] Tests cover happy path and error cases: We need thorough tests to ensure the service works correctly in various scenarios. This includes both successful room generation and error conditions like invalid input. Robust testing is essential for preventing bugs and ensuring reliability.
  • [ ] Follows Input/Output pattern: The service should adhere to our established Input/Output patterns for consistency and maintainability. This makes the code easier to understand and integrate with other services. Consistency in design promotes readability and predictability.

Dependencies

Our implementation has a couple of key dependencies:

  • Depends on: rpg-toolkit#170 (demo room generator): We're relying on the demo room generator from the rpg-toolkit for the initial room layout. This specific version or a later one should include the necessary functionality.
  • Depends on: rpg-api-protos#48 (aligned proto): We need the aligned proto definitions to ensure our service uses the correct message structures for communication. This version should define the DungeonStartRequest and DungeonStartResponse messages, as well as the Room message.

Context

This EncounterService is a crucial component of our