DungeonStart RPC: Implement EncounterService With Guide
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 protoRoom
- [ ] 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
andDungeonStartOutput
: These structs define the input and output types for ourDungeonStart
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 therpg-toolkit
, database connections, or other services. -
NewService()
function: This constructor function creates a new instance of theService
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 therpg-toolkit
. We're passing in10
and10
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.
- 1. Generate room using toolkit: This is where we leverage the
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 therpg-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 theDungeonStartRequest
andDungeonStartResponse
messages, as well as theRoom
message.
Context
This EncounterService
is a crucial component of our