Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions src/bowling_game/entity/bowling_game.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package entity

import (
"fmt"

core "github.com/cs-lexliu/practice-event-sourcing/src/core/entity"
)

type BowlingGame struct {
*core.AggregateRootTemplate
gameID string
score int
leavingPins int
bonusRemainHit int
bonusChance []int
finishedFrameCount int
bonusFrame int
}

func newBowlingGame() *BowlingGame {
bowlingGame := &BowlingGame{}
bowlingGame.AggregateRootTemplate = core.NewAggregateRootTemple(bowlingGame)
return bowlingGame
}

func NewBowlingGame(gameID string) *BowlingGame {
bowlingGame := newBowlingGame()
bowlingGame.Apply(NewBowlingGameCreated(gameID))
return bowlingGame
}

func NewBowlingGameFromEvent(domainEvents []core.DomainEvent) *BowlingGame {
bowlingGame := newBowlingGame()
for _, event := range domainEvents {
bowlingGame.Apply(event)
bowlingGame.ClearDomainEvents()
}
return bowlingGame
}

var BowlingGameConstuctor core.Constuctor = func(events []core.DomainEvent) interface{} {
return NewBowlingGameFromEvent(events)
}

func (b BowlingGame) Replay(domainEvents []core.DomainEvent) core.AggregateRoot {
bowlingGame := newBowlingGame()
for _, event := range domainEvents {
bowlingGame.Apply(event)
bowlingGame.ClearDomainEvents()
}
return bowlingGame
}

func (b BowlingGame) ID() string {
return b.gameID
}

func (b BowlingGame) Category() string {
return "BowlingGame"
}

func (b BowlingGame) Score() int {
return b.score
}

func (b BowlingGame) LeavingPins() int {
return b.leavingPins
}

func (b BowlingGame) BonusRemainHit() int {
return b.bonusRemainHit
}

func (b BowlingGame) BonusChance() []int {
cloneBonusChance := make([]int, len(b.bonusChance))
copy(cloneBonusChance, b.bonusChance)
return cloneBonusChance
}

func (b BowlingGame) FinishedFrameCount() int {
return b.finishedFrameCount
}

func (b BowlingGame) BonusFrame() int {
return b.bonusFrame
}

func (b *BowlingGame) RollABall(hit int) error {
if (b.finishedFrameCount-b.bonusFrame > 9) ||
(b.finishedFrameCount > 9 && (b.bonusFrame == 1 && b.bonusRemainHit == 1)) {
return fmt.Errorf("unavailable to roll a ball after game finished")
}
if hit < 0 {
return fmt.Errorf("hit number should not less than 0")
}
b.Apply(NewBowlingGameRollABall(hit))
return nil
}

func (b *BowlingGame) When(domainEvent core.DomainEvent) {
switch event := interface{}(domainEvent).(type) {
case BowlingGameCreated:
b.gameID = event.gameID
b.leavingPins = 10
b.bonusRemainHit = 2
case BowlingGameRolledABall:
b.score += event.hit
b.leavingPins -= event.hit
b.bonusRemainHit--
for i, chance := range b.bonusChance {
if chance > 0 {
b.score += event.hit
b.bonusChance[i] = chance - 1
}
}
if frameHasBonusChance(b.finishedFrameCount) {
if strike(b.leavingPins, b.bonusRemainHit) {
b.bonusChance = append(b.bonusChance, 2)
}
if spare(b.leavingPins, b.bonusRemainHit) {
b.bonusChance = append(b.bonusChance, 1)
}
}
if spare(b.leavingPins, b.bonusRemainHit) && b.finishedFrameCount == 9 {
b.bonusFrame++
}
if strike(b.leavingPins, b.bonusRemainHit) && b.finishedFrameCount >= 9 {
b.bonusFrame++
}
if b.leavingPins == 0 || b.bonusRemainHit == 0 {
b.finishedFrameCount++
b.leavingPins = 10
b.bonusRemainHit = 2
}
}
}

func frameHasBonusChance(finishedFrameCount int) bool {
return finishedFrameCount < 9
}

func strike(leavingPins, bonusRemainHit int) bool {
return leavingPins == 0 && bonusRemainHit == 1
}

func spare(leavingPins, bonusRemainHit int) bool {
return leavingPins == 0 && bonusRemainHit == 0
}
40 changes: 40 additions & 0 deletions src/bowling_game/entity/bowling_game_events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package entity

import core "github.com/cs-lexliu/practice-event-sourcing/src/core/entity"

type BowlingGameEvents struct {
core.DomainEvent
name string
}

func (e BowlingGameEvents) String() string {
return e.name
}

type BowlingGameCreated struct {
BowlingGameEvents
gameID string
}

func NewBowlingGameCreated(gameID string) BowlingGameCreated {
return BowlingGameCreated{
BowlingGameEvents: BowlingGameEvents{
name: "BowlingGameCreated",
},
gameID: gameID,
}
}

type BowlingGameRolledABall struct {
BowlingGameEvents
hit int
}

func NewBowlingGameRollABall(hit int) BowlingGameRolledABall {
return BowlingGameRolledABall{
BowlingGameEvents: BowlingGameEvents{
name: "BowlingGameRolledABall",
},
hit: hit,
}
}
29 changes: 29 additions & 0 deletions src/bowling_game/entity/bowling_game_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package entity

import (
"testing"

core "github.com/cs-lexliu/practice-event-sourcing/src/core/entity"
"github.com/stretchr/testify/assert"
)

func TestCreateBowlingGameGenerateABowlingGameCreatedEvent(t *testing.T) {
bowlingGame := NewBowlingGame("")
assert.Equal(t, 1, len(bowlingGame.DomainEvents()))
assert.IsType(t, BowlingGameCreated{}, bowlingGame.DomainEvents()[0])
}

func TestReplayBowlingGameFromBowlingGameCreatedEvent(t *testing.T) {
event := NewBowlingGameCreated("abc")
bowlingGame := NewBowlingGameFromEvent([]core.DomainEvent{event})
assert.Equal(t, event.gameID, bowlingGame.ID())
assert.Equal(t, 0, len(bowlingGame.DomainEvents()))
}

func TestThrowCommandGenerateBowlingGameThrownEvent(t *testing.T) {
event := NewBowlingGameCreated("abc")
bowlingGame := NewBowlingGameFromEvent([]core.DomainEvent{event})
bowlingGame.RollABall(1)
assert.Equal(t, 1, len(bowlingGame.DomainEvents()))
assert.IsType(t, BowlingGameRolledABall{}, bowlingGame.DomainEvents()[0])
}
28 changes: 28 additions & 0 deletions src/bowling_game/usecase/create_bowling_game.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package usecase

import (
"context"
"fmt"

"github.com/cs-lexliu/practice-event-sourcing/src/bowling_game/entity"
"github.com/cs-lexliu/practice-event-sourcing/src/bowling_game/usecase/port/in"
"github.com/cs-lexliu/practice-event-sourcing/src/bowling_game/usecase/port/out"
)

type CreateBowlingGameService struct {
repository out.BowlingGameRepository
}

func NewCreateBowlingGameService(repository out.BowlingGameRepository) *CreateBowlingGameService {
return &CreateBowlingGameService{
repository: repository,
}
}

func (s *CreateBowlingGameService) execute(ctx context.Context, input in.CreateBowlingGameInput) (string, error) {
bowlingGame := entity.NewBowlingGame(input.GameID)
if err := s.repository.Save(ctx, bowlingGame); err != nil {
return "", fmt.Errorf("repository save bowling game: %w", err)
}
return bowlingGame.ID(), nil
}
47 changes: 47 additions & 0 deletions src/bowling_game/usecase/create_bowling_game_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package usecase

import (
"context"
"testing"
"time"

"github.com/cs-lexliu/practice-event-sourcing/src/adpater/inmem/pubsub"
"github.com/cs-lexliu/practice-event-sourcing/src/adpater/inmem/statestore"
"github.com/cs-lexliu/practice-event-sourcing/src/bowling_game/entity"
"github.com/cs-lexliu/practice-event-sourcing/src/bowling_game/usecase/port/in"
core "github.com/cs-lexliu/practice-event-sourcing/src/core/entity"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)

func TestCreateBowlingGameUseCase(t *testing.T) {
ctx := context.Background()

eventBus := pubsub.NewDomainEventBus()
listener := &fakeCreateBowlingGameListener{}
subscriber := pubsub.NewSubscriber("event-listener", listener.Notifier)
eventBus.Register(ctx, subscriber)

repository := statestore.NewStateRepositoryAdapter[*entity.BowlingGame](eventBus)
createBowlingGameUseCase := NewCreateBowlingGameService(repository)
input := in.CreateBowlingGameInput{
GameID: uuid.New().String(),
}

output, err := createBowlingGameUseCase.execute(ctx, input)
assert.NoError(t, err)
_, err = repository.FindByID(ctx, output)
assert.NoError(t, err)

assert.Eventually(t, func() bool {
return assert.Equal(t, 1, listener.count)
}, 5*time.Millisecond, 1*time.Millisecond)
}

type fakeCreateBowlingGameListener struct {
count int
}

func (l *fakeCreateBowlingGameListener) Notifier(event core.DomainEvent) {
l.count++
}
5 changes: 5 additions & 0 deletions src/bowling_game/usecase/port/in/create_bowling_game_input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package in

type CreateBowlingGameInput struct {
GameID string
}
6 changes: 6 additions & 0 deletions src/bowling_game/usecase/port/in/roll_a_ball_input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package in

type RollABallInput struct {
GameID string
Hit int
}
8 changes: 8 additions & 0 deletions src/bowling_game/usecase/port/out/bowling_game_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package out

import (
"github.com/cs-lexliu/practice-event-sourcing/src/bowling_game/entity"
core "github.com/cs-lexliu/practice-event-sourcing/src/core/usecase"
)

type BowlingGameRepository core.Repository[*entity.BowlingGame]
33 changes: 33 additions & 0 deletions src/bowling_game/usecase/roll_a_ball.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package usecase

import (
"context"
"fmt"

"github.com/cs-lexliu/practice-event-sourcing/src/bowling_game/usecase/port/in"
"github.com/cs-lexliu/practice-event-sourcing/src/bowling_game/usecase/port/out"
)

type RollABallService struct {
repository out.BowlingGameRepository
}

func NewRollABallServiceService(repository out.BowlingGameRepository) *RollABallService {
return &RollABallService{
repository: repository,
}
}

func (s *RollABallService) execute(ctx context.Context, input in.RollABallInput) (string, error) {
bowlingGame, err := s.repository.FindByID(ctx, input.GameID)
if err != nil {
return "", fmt.Errorf("repository find bowling game by id: %w", err)
}
if err := bowlingGame.RollABall(input.Hit); err != nil {
return "", fmt.Errorf("bowling game roll a ball: %w", err)
}
if err := s.repository.Save(ctx, bowlingGame); err != nil {
return "", fmt.Errorf("repository save bowling game: %w", err)
}
return bowlingGame.ID(), nil
}
Loading