├── .dockerignore ├── src ├── main │ ├── webapp │ │ └── resources │ │ │ ├── img │ │ │ ├── button.png │ │ │ ├── favicon.png │ │ │ ├── bigBlind.png │ │ │ └── smallBlind.png │ │ │ ├── modules │ │ │ ├── table │ │ │ │ ├── ActionOnTimer.tsx │ │ │ │ ├── MyCards.tsx │ │ │ │ ├── CardData.ts │ │ │ │ ├── CommonCards.tsx │ │ │ │ ├── SeatContainer.tsx │ │ │ │ ├── Seat.tsx │ │ │ │ └── tableStateSubscriber.ts │ │ │ ├── signup │ │ │ │ ├── sign-up-step-2-success.tsx │ │ │ │ ├── sign-up-step-1-success.tsx │ │ │ │ ├── sign-up-step-2-form.tsx │ │ │ │ ├── sign-up-step-1-form.tsx │ │ │ │ ├── sign-up-step-1.tsx │ │ │ │ └── sign-up-step-2.tsx │ │ │ ├── home │ │ │ │ ├── Chat │ │ │ │ │ ├── ChatLine.tsx │ │ │ │ │ └── selectors.ts │ │ │ │ ├── InterceptRedirect.tsx │ │ │ │ ├── GameTabs │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── GameTab.tsx │ │ │ │ ├── Logout.ts │ │ │ │ ├── MainTabs.tsx │ │ │ │ └── Navigation │ │ │ │ │ └── index.tsx │ │ │ ├── game │ │ │ │ └── GamePage.tsx │ │ │ ├── lobby │ │ │ │ ├── Lobby.tsx │ │ │ │ ├── GameList │ │ │ │ │ └── index.tsx │ │ │ │ ├── JoinGameDialog │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── JoinGameDialog.tsx │ │ │ │ └── CreateGameDialog │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── CreateGameDialog.tsx │ │ │ ├── webSocket │ │ │ │ ├── WebSocketService.ts │ │ │ │ └── WebSocketSubscriptionManager.ts │ │ │ └── login │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── main.css │ ├── kotlin │ │ └── com │ │ │ └── flexpoker │ │ │ ├── exception │ │ │ └── FlexPokerException.kt │ │ │ ├── game │ │ │ ├── command │ │ │ │ ├── StaticGameData.kt │ │ │ │ ├── repository │ │ │ │ │ ├── GameEventRepository.kt │ │ │ │ │ └── InMemoryGameEventRepository.kt │ │ │ │ ├── aggregate │ │ │ │ │ └── eventproducers │ │ │ │ │ │ ├── IncrementBlindsEventProducer.kt │ │ │ │ │ │ └── CreateGameEventProducer.kt │ │ │ │ ├── commands │ │ │ │ │ └── GameCommands.kt │ │ │ │ ├── handlers │ │ │ │ │ ├── CreateGameCommandHandler.kt │ │ │ │ │ ├── JoinGameCommandHandler.kt │ │ │ │ │ ├── IncrementBlindsCommandHandler.kt │ │ │ │ │ └── AttemptToStartNewHandCommandHandler.kt │ │ │ │ └── commandreceivers │ │ │ │ │ └── InMemoryAsyncGameCommandReceiver.kt │ │ │ └── query │ │ │ │ ├── repository │ │ │ │ ├── GamePlayerRepository.kt │ │ │ │ ├── GameListRepository.kt │ │ │ │ ├── OpenGameForPlayerRepository.kt │ │ │ │ └── impl │ │ │ │ │ ├── InMemoryGamePlayerRepository.kt │ │ │ │ │ └── RedisGamePlayerRepository.kt │ │ │ │ ├── dto │ │ │ │ └── GameQueryDTOs.kt │ │ │ │ └── handlers │ │ │ │ ├── PlayerBustedGameEventHandler.kt │ │ │ │ ├── BlindsIncreasedEventHandler.kt │ │ │ │ └── GameCreatedEventHandler.kt │ │ │ ├── framework │ │ │ ├── processmanager │ │ │ │ └── ProcessManager.kt │ │ │ ├── event │ │ │ │ ├── subscriber │ │ │ │ │ └── EventSubscriber.kt │ │ │ │ └── Interfaces.kt │ │ │ ├── command │ │ │ │ └── Interfaces.kt │ │ │ └── pushnotifier │ │ │ │ └── Interfaces.kt │ │ │ ├── table │ │ │ ├── query │ │ │ │ ├── repository │ │ │ │ │ ├── TableRepository.kt │ │ │ │ │ ├── CardsUsedInHandRepository.kt │ │ │ │ │ └── impl │ │ │ │ │ │ └── RedisTableRepository.kt │ │ │ │ ├── handlers │ │ │ │ │ ├── HandCompletedEventHandler.kt │ │ │ │ │ ├── PlayerBustedTableEventHandler.kt │ │ │ │ │ ├── WinnersDeterminedEventHandler.kt │ │ │ │ │ ├── RoundCompletedEventHandler.kt │ │ │ │ │ ├── PotClosedEventHandler.kt │ │ │ │ │ ├── ActionOnChangedEventHandler.kt │ │ │ │ │ ├── PotAmountIncreasedEventHandler.kt │ │ │ │ │ ├── PotCreatedEventHandler.kt │ │ │ │ │ ├── TurnCardDealtEventHandler.kt │ │ │ │ │ ├── RiverCardDealtEventHandler.kt │ │ │ │ │ └── FlopCardsDealtEventHandler.kt │ │ │ │ └── dto │ │ │ │ │ └── TableQueryDTOs.kt │ │ │ └── command │ │ │ │ ├── service │ │ │ │ ├── CardService.kt │ │ │ │ └── HandEvaluatorService.kt │ │ │ │ ├── repository │ │ │ │ ├── TableEventRepository.kt │ │ │ │ ├── InMemoryTableEventRepository.kt │ │ │ │ └── RedisTableEventRepository.kt │ │ │ │ ├── aggregate │ │ │ │ ├── eventproducers │ │ │ │ │ ├── TableUtils.kt │ │ │ │ │ ├── hand │ │ │ │ │ │ ├── FinishHandIfAppropriateEventProducer.kt │ │ │ │ │ │ ├── HandUtils.kt │ │ │ │ │ │ ├── DetermineWinnersIfAppropriate.kt │ │ │ │ │ │ ├── DealCommonCardsIfAppropriateEventProducer.kt │ │ │ │ │ │ └── HandlePotAndRoundCompleteEventProducer.kt │ │ │ │ │ ├── PauseResumeTableEventProducer.kt │ │ │ │ │ ├── CreateTableEventProducer.kt │ │ │ │ │ └── AddRemovePlayerEventProducer.kt │ │ │ │ └── RandomNumberGenerator.kt │ │ │ │ ├── handlers │ │ │ │ ├── TickActionOnTimerCommandHandler.kt │ │ │ │ ├── PauseCommandHandler.kt │ │ │ │ ├── CallCommandHandler.kt │ │ │ │ ├── FoldCommandHandler.kt │ │ │ │ ├── ResumeCommandHandler.kt │ │ │ │ ├── CheckCommandHandler.kt │ │ │ │ ├── CreateTableCommandHandler.kt │ │ │ │ ├── RaiseCommandHandler.kt │ │ │ │ ├── RemovePlayerCommandHandler.kt │ │ │ │ ├── AutoMoveHandForwardCommandHandler.kt │ │ │ │ ├── ExpireActionOnTimerCommandHandler.kt │ │ │ │ └── AddPlayerCommandHandler.kt │ │ │ │ ├── StaticTableData.kt │ │ │ │ └── eventpublishers │ │ │ │ └── InMemoryAsyncTableEventPublisher.kt │ │ │ ├── web │ │ │ ├── dto │ │ │ │ ├── OutgoingWebDTOs.kt │ │ │ │ └── IncomingWebDTOs.kt │ │ │ ├── controller │ │ │ │ ├── HomeController.kt │ │ │ │ └── LoginController.kt │ │ │ └── commandsenders │ │ │ │ ├── InMemoryAsyncGameCommandSender.kt │ │ │ │ └── InMemoryAsyncTableCommandSender.kt │ │ │ ├── Application.kt │ │ │ ├── util │ │ │ ├── PasswordUtils.kt │ │ │ ├── PCollectionExtensions.kt │ │ │ └── MessagingConstants.kt │ │ │ ├── chat │ │ │ ├── repository │ │ │ │ └── ChatRepository.kt │ │ │ └── service │ │ │ │ └── ChatService.kt │ │ │ ├── login │ │ │ └── repository │ │ │ │ └── LoginRepository.kt │ │ │ ├── signup │ │ │ ├── repository │ │ │ │ └── SignUpRepository.kt │ │ │ └── SignUpUser.kt │ │ │ ├── config │ │ │ ├── WebConfig.kt │ │ │ ├── ProfileNames.kt │ │ │ ├── WebSocketConfig.kt │ │ │ └── SecurityConfig.kt │ │ │ ├── pushnotificationhandlers │ │ │ ├── ChatSentPushNotificationHandler.kt │ │ │ ├── TickActionOnTimerPushNotificationHandler.kt │ │ │ ├── GameListUpdatedPushNotificationHandler.kt │ │ │ ├── TableUpdatedPushNotificationHandler.kt │ │ │ ├── OpenTableForUserPushNotificationHandler.kt │ │ │ ├── SendUserPocketCardsPushNotificationHandler.kt │ │ │ └── OpenGamesForPlayerUpdatedPushNotificationHandler.kt │ │ │ ├── processmanagers │ │ │ ├── PauseTableProcessManager.kt │ │ │ ├── ResumeTableProcessManager.kt │ │ │ ├── AttemptToStartNewHandForExistingTableProcessManager.kt │ │ │ ├── StartNewHandForExistingTableProcessManager.kt │ │ │ ├── StartFirstHandProcessManager.kt │ │ │ ├── CreateInitialTablesForGameProcessManager.kt │ │ │ ├── MovePlayerBetweenTablesProcessManager.kt │ │ │ ├── AutoMoveHandForwardProcessManager.kt │ │ │ └── IncrementBlindsCountdownProcessManager.kt │ │ │ └── pushnotifications │ │ │ └── PushNotifications.kt │ └── resources │ │ ├── redis-scripts │ │ └── check-and-set.lua │ │ └── index.html └── test │ └── kotlin │ └── com │ └── flexpoker │ ├── test │ └── util │ │ ├── TestContainersUtils.kt │ │ ├── datageneration │ │ ├── DeckGenerator.kt │ │ └── CardGenerator.kt │ │ ├── TestClassAnnotations.kt │ │ └── CommonAssertions.kt │ ├── table │ ├── command │ │ ├── aggregate │ │ │ ├── testhelpers │ │ │ │ └── TestRandomNumberGenerator.kt │ │ │ ├── DefaultRandomNumberGeneratorTest.kt │ │ │ ├── pot │ │ │ │ └── PotTestUtils.kt │ │ │ ├── generic │ │ │ │ ├── PauseTableTest.kt │ │ │ │ └── RemovePlayerFromTableTest.kt │ │ │ └── TableUtilsTest.kt │ │ ├── repository │ │ │ ├── InMemoryTableEventRepositoryTest.kt │ │ │ └── RedisTableEventRepositoryTest.kt │ │ └── service │ │ │ └── DefaultCardServiceTest.kt │ └── query │ │ └── repository │ │ └── impl │ │ ├── InMemoryCardsUsedInHandRepositoryTest.kt │ │ ├── RedisCardsUsedInHandRepositoryTest.kt │ │ ├── InMemoryTableRepositoryTest.kt │ │ └── RedisTableRepositoryTest.kt │ ├── chat │ └── repository │ │ ├── InMemoryChatRepositoryTest.kt │ │ └── RedisChatRepositoryTest.kt │ ├── game │ ├── query │ │ └── repository │ │ │ └── impl │ │ │ ├── InMemoryGamePlayerRepositoryTest.kt │ │ │ ├── RedisGamePlayerRepositoryTest.kt │ │ │ ├── InMemoryOpenGameForPlayerRepositoryTest.kt │ │ │ ├── SharedGamePlayerRepositoryTest.kt │ │ │ ├── InMemoryGameListRepositoryTest.kt │ │ │ ├── RedisOpenGameForPlayerRepositoryTest.kt │ │ │ └── RedisGameListRepositoryTest.kt │ └── command │ │ ├── aggregate │ │ ├── tablebalancer │ │ │ ├── TableBalancerTestUtils.kt │ │ │ └── TableBalancerResumedTableTest.kt │ │ ├── CreateNewGameTest.kt │ │ └── BlindScheduleTest.kt │ │ └── repository │ │ └── InMemoryGameEventRepositoryTest.kt │ ├── login │ └── repository │ │ ├── InMemoryLoginRepositoryTest.kt │ │ ├── RedisLoginRepositoryTest.kt │ │ └── SharedLoginRepositoryTest.kt │ ├── archtest │ ├── ServiceArchTests.kt │ ├── ControllerArchTests.kt │ └── Utils.kt │ └── signup │ └── repository │ ├── InMemorySignUpRepositoryTest.kt │ └── RedisSignUpRepositoryTest.kt ├── tsconfig.json ├── .gitignore ├── Dockerfile ├── package.json └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules/ 3 | target/ 4 | -------------------------------------------------------------------------------- /src/main/webapp/resources/img/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwoolner/flex-poker/HEAD/src/main/webapp/resources/img/button.png -------------------------------------------------------------------------------- /src/main/webapp/resources/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwoolner/flex-poker/HEAD/src/main/webapp/resources/img/favicon.png -------------------------------------------------------------------------------- /src/main/webapp/resources/img/bigBlind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwoolner/flex-poker/HEAD/src/main/webapp/resources/img/bigBlind.png -------------------------------------------------------------------------------- /src/main/webapp/resources/img/smallBlind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwoolner/flex-poker/HEAD/src/main/webapp/resources/img/smallBlind.png -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/table/ActionOnTimer.tsx: -------------------------------------------------------------------------------- 1 | export default ({actionOnTick}) => { 2 | return ( 3 |
{actionOnTick}
4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/exception/FlexPokerException.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.exception 2 | 3 | class FlexPokerException(message: String) : RuntimeException(message) 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/command/StaticGameData.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command 2 | 3 | enum class GameStage { 4 | REGISTERING, STARTING, INPROGRESS, FINISHED 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2022", 4 | "target": "es2022", 5 | "jsx": "react-jsx", 6 | "moduleResolution": "Bundler", 7 | "allowSyntheticDefaultImports": true, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/signup/sign-up-step-2-success.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return ( 3 | <> 4 |

Confirmed!

5 |

Go to Login

6 | 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/framework/processmanager/ProcessManager.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.framework.processmanager 2 | 3 | import com.flexpoker.framework.event.Event 4 | 5 | fun interface ProcessManager { 6 | fun handle(event: T) 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/framework/event/subscriber/EventSubscriber.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.framework.event.subscriber 2 | 3 | import com.flexpoker.framework.event.Event 4 | 5 | fun interface EventSubscriber { 6 | fun receive(event: T) 7 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/home/Chat/ChatLine.tsx: -------------------------------------------------------------------------------- 1 | const displayChat = chat => { 2 | return chat.systemMessage 3 | ? `System: ${chat.message}` 4 | : `${chat.senderUsername}: ${chat.message}` 5 | } 6 | 7 | export default ({chat}) =>
{displayChat(chat)}
8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/query/repository/GamePlayerRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository 2 | 3 | import java.util.UUID 4 | 5 | interface GamePlayerRepository { 6 | fun addPlayerToGame(playerId: UUID, gameId: UUID) 7 | fun fetchAllPlayerIdsForGame(gameId: UUID): Set 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .project 3 | .classpath 4 | .settings/ 5 | log.out 6 | .idea/ 7 | dump.rdb 8 | output/ 9 | node_modules/ 10 | src/main/webapp/resources/app.bundle.js 11 | src/main/webapp/resources/vendor.bundle.js 12 | src/main/webapp/resources/vendor.bundle.js.LICENSE.txt 13 | bin/ 14 | flexpoker.iml 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/repository/TableRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.repository 2 | 3 | import com.flexpoker.table.query.dto.TableDTO 4 | import java.util.UUID 5 | 6 | interface TableRepository { 7 | fun fetchById(tableId: UUID): TableDTO 8 | fun save(tableDTO: TableDTO) 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/web/dto/OutgoingWebDTOs.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.web.dto 2 | 3 | import java.util.UUID 4 | 5 | data class OutgoingChatMessageDTO (val id: UUID, val gameId: UUID?, val tableId: UUID?, val message: String, 6 | val senderUsername: String?, val systemMessage: Boolean) 7 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/test/util/TestContainersUtils.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.test.util 2 | 3 | import com.redis.testcontainers.RedisContainer 4 | import org.testcontainers.utility.DockerImageName 5 | 6 | fun redisContainer(): RedisContainer { 7 | return RedisContainer(DockerImageName.parse("redis:7.0.11-alpine3.18")) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/Application.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class Application 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/table/MyCards.tsx: -------------------------------------------------------------------------------- 1 | import CardData from './CardData' 2 | 3 | export default ({myLeftCardId, myRightCardId}) => { 4 | return ( 5 |
6 | 7 | 8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/service/CardService.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.service 2 | 3 | import com.flexpoker.table.command.Card 4 | import com.flexpoker.table.command.CardsUsedInHand 5 | 6 | interface CardService { 7 | fun createShuffledDeck(): List 8 | fun createCardsUsedInHand(cards: List, numberOfPlayers: Int): CardsUsedInHand 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/framework/command/Interfaces.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.framework.command 2 | 3 | fun interface CommandHandler { 4 | fun handle(command: T) 5 | } 6 | 7 | fun interface CommandReceiver { 8 | fun receive(command: T) 9 | } 10 | 11 | fun interface CommandSender { 12 | fun send(command: T) 13 | } 14 | 15 | interface DomainState {} 16 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/signup/sign-up-step-1-success.tsx: -------------------------------------------------------------------------------- 1 | export default ({ username, email }) => { 2 | return ( 3 | <> 4 |

Confirm your sign up

5 |

Email sent to {email} (TODO: not working yet)

6 |

Click here to confirm

7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/repository/TableEventRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.repository 2 | 3 | import com.flexpoker.table.command.events.TableEvent 4 | import java.util.UUID 5 | 6 | interface TableEventRepository { 7 | fun fetchAll(id: UUID): List 8 | fun setEventVersionsAndSave(basedOnVersion: Int, events: List): List 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/framework/pushnotifier/Interfaces.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.framework.pushnotifier 2 | 3 | import com.flexpoker.pushnotifications.PushNotification 4 | 5 | fun interface PushNotificationHandler { 6 | fun handle(pushNotification: T) 7 | } 8 | 9 | fun interface PushNotificationPublisher { 10 | fun publish(pushNotification: PushNotification) 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/framework/event/Interfaces.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.framework.event 2 | 3 | import java.time.Instant 4 | import java.util.UUID 5 | 6 | interface Event { 7 | val aggregateId: UUID 8 | var version: Int 9 | val time: Instant 10 | } 11 | 12 | fun interface EventHandler { 13 | fun handle(event: T) 14 | } 15 | 16 | fun interface EventPublisher { 17 | fun publish(event: T) 18 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/table/CardData.ts: -------------------------------------------------------------------------------- 1 | const urlTemplate = (cardSuit: string, cardRank: string) => `/resources/img/nicubunu_Ornamental_deck_${cardRank}_of_${cardSuit}.svg` 2 | const cardSuits = ['hearts', 'spades', 'diamonds', 'clubs'] 3 | const cardRanks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'Jack', 'Queen', 'King', 'Ace'] 4 | 5 | export default cardSuits.map(cardSuit => cardRanks.map(cardRank => urlTemplate(cardSuit, cardRank))).flat() -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/aggregate/eventproducers/TableUtils.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.eventproducers 2 | 3 | import com.flexpoker.table.command.aggregate.TableState 4 | 5 | fun checkHandIsBeingPlayed(state: TableState) { 6 | requireNotNull(state.currentHand) { "no hand in progress" } 7 | } 8 | 9 | fun numberOfPlayersAtTable(state: TableState): Int { 10 | return state.seatMap.values.filterNotNull().count() 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/util/PasswordUtils.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.util 2 | 3 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 4 | import org.springframework.security.crypto.password.DelegatingPasswordEncoder 5 | 6 | val passwordEncoder = DelegatingPasswordEncoder("bcrypt", mapOf("bcrypt" to BCryptPasswordEncoder())) 7 | 8 | fun encodePassword(password: String): String { 9 | return passwordEncoder.encode(password) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/table/CommonCards.tsx: -------------------------------------------------------------------------------- 1 | import CardData from './CardData' 2 | 3 | export default ({visibleCommonCards}) => { 4 | return ( 5 |
6 | { 7 | visibleCommonCards.map((card, index) => { 8 | return ( 9 | 10 | 11 | 12 | ) 13 | }) 14 | } 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:21.0.1_12-jdk-alpine 2 | 3 | RUN apk add nodejs-current # v20.8.1 4 | RUN apk add npm # v9.6.6 5 | RUN apk add maven # v3.9.2 6 | 7 | RUN mkdir flex-poker 8 | 9 | COPY / /flex-poker/ 10 | 11 | WORKDIR /flex-poker 12 | 13 | RUN npm install 14 | RUN npm run prod 15 | RUN mvn package -DskipTests 16 | 17 | 18 | FROM eclipse-temurin:21.0.1_12-jre-alpine 19 | 20 | COPY --from=0 /flex-poker/target/flexpoker.war . 21 | 22 | ENTRYPOINT java -jar flexpoker.war 23 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/game/GamePage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux' 3 | import { changeChatMsgStream } from '../../reducers' 4 | import { useParams } from 'react-router-dom' 5 | 6 | export default () => { 7 | const { gameId } = useParams() 8 | const dispatch = useDispatch() 9 | 10 | useEffect(() => { 11 | dispatch(changeChatMsgStream(gameId, null)) 12 | }, [gameId]) 13 | 14 | return
Game page
15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/redis-scripts/check-and-set.lua: -------------------------------------------------------------------------------- 1 | local currentRaw = redis.call('GET', KEYS[1]) 2 | local newDecoded = cjson.decode(ARGV[1]) 3 | 4 | if currentRaw then 5 | local currentVersion = cjson.decode(currentRaw).version 6 | local newVersion = newDecoded.version 7 | if newVersion > currentVersion then 8 | redis.call('SET', KEYS[1], ARGV[1]) 9 | return true 10 | else 11 | return false 12 | end 13 | else 14 | redis.call('SET', KEYS[1], ARGV[1]) 15 | return true 16 | end 17 | 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/chat/repository/ChatRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.chat.repository 2 | 3 | import com.flexpoker.web.dto.OutgoingChatMessageDTO 4 | import java.util.UUID 5 | 6 | interface ChatRepository { 7 | fun saveChatMessage(chatMessage: OutgoingChatMessageDTO) 8 | fun fetchAllLobbyChatMessages(): List 9 | fun fetchAllGameChatMessages(gameId: UUID): List 10 | fun fetchAllTableChatMessages(tableId: UUID): List 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/command/repository/GameEventRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.repository 2 | 3 | import com.flexpoker.game.command.events.GameCreatedEvent 4 | import com.flexpoker.game.command.events.GameEvent 5 | import java.util.UUID 6 | 7 | interface GameEventRepository { 8 | fun fetchAll(id: UUID): List 9 | fun setEventVersionsAndSave(basedOnVersion: Int, events: List): List 10 | fun fetchGameCreatedEvent(gameId: UUID): GameCreatedEvent 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/login/repository/LoginRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.login.repository 2 | 3 | import org.springframework.security.core.userdetails.UserDetailsService 4 | import java.util.UUID 5 | 6 | interface LoginRepository : UserDetailsService { 7 | fun saveUsernameAndPassword(username: String, encryptedPassword: String) 8 | fun fetchAggregateIdByUsername(username: String): UUID 9 | fun saveAggregateIdAndUsername(aggregateId: UUID, username: String) 10 | fun fetchUsernameByAggregateId(aggregateId: UUID): String 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/query/repository/GameListRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository 2 | 3 | import com.flexpoker.game.query.dto.GameInListDTO 4 | import com.flexpoker.game.command.GameStage 5 | import java.util.UUID 6 | 7 | interface GameListRepository { 8 | fun saveNew(gameInListDTO: GameInListDTO) 9 | fun fetchAll(): List 10 | fun incrementRegisteredPlayers(aggregateId: UUID) 11 | fun fetchGameName(aggregateId: UUID): String 12 | fun changeGameStage(aggregateId: UUID, gameStage: GameStage) 13 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/test/util/datageneration/DeckGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.test.util.datageneration 2 | 3 | import com.flexpoker.table.command.CardsUsedInHand 4 | 5 | object DeckGenerator { 6 | 7 | fun createDeck(): CardsUsedInHand { 8 | return CardsUsedInHand( 9 | CardGenerator.createFlopCards(), 10 | CardGenerator.createTurnCard(), 11 | CardGenerator.createRiverCard(), 12 | listOf(CardGenerator.createPocketCards1(), CardGenerator.createPocketCards2()) 13 | ) 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/util/PCollectionExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.util 2 | 3 | import org.pcollections.HashTreePMap 4 | import org.pcollections.HashTreePSet 5 | import org.pcollections.PMap 6 | import org.pcollections.PSet 7 | import org.pcollections.PVector 8 | import org.pcollections.TreePVector 9 | 10 | fun Map.toPMap(): PMap { 11 | return HashTreePMap.from(this) 12 | } 13 | 14 | fun Collection.toPSet(): PSet { 15 | return HashTreePSet.from(this) 16 | } 17 | 18 | fun Collection.toPVector(): PVector { 19 | return TreePVector.from(this) 20 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/command/aggregate/testhelpers/TestRandomNumberGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.testhelpers 2 | 3 | import com.flexpoker.table.command.aggregate.DefaultRandomNumberGenerator 4 | import com.flexpoker.table.command.aggregate.RandomNumberGenerator 5 | 6 | class TestRandomNumberGenerator(private var counter: Int) : RandomNumberGenerator by DefaultRandomNumberGenerator() { 7 | override fun int(exclusiveMax: Int): Int { 8 | val current = minOf(counter, exclusiveMax.dec()) 9 | counter = counter.inc() 10 | return current 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/service/HandEvaluatorService.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.service 2 | 3 | import com.flexpoker.table.command.FlopCards 4 | import com.flexpoker.table.command.PocketCards 5 | import com.flexpoker.table.command.RiverCard 6 | import com.flexpoker.table.command.TurnCard 7 | import com.flexpoker.table.command.aggregate.HandEvaluation 8 | 9 | interface HandEvaluatorService { 10 | fun determineHandEvaluation(flopCards: FlopCards, turnCard: TurnCard, riverCard: RiverCard, 11 | pocketCards: List): Map 12 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/lobby/Lobby.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import CreateGameDialog from './CreateGameDialog' 3 | import JoinGameDialog from './JoinGameDialog' 4 | import GameList from './GameList' 5 | import { useDispatch } from 'react-redux' 6 | import { changeChatMsgStream } from '../../reducers' 7 | 8 | export default () => { 9 | const dispatch = useDispatch() 10 | 11 | useEffect(() => { 12 | dispatch(changeChatMsgStream(null, null)) 13 | }) 14 | 15 | return ( 16 | <> 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/table/SeatContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux' 2 | import { Map } from 'immutable' 3 | import Seat from './Seat' 4 | import { RootState } from '../..' 5 | 6 | export default ({ seats, mySeat }) => { 7 | const actionOnTick = useSelector((state: RootState) => 8 | state.actionOnTicks.get(state.activeTable.gameId, Map()).get(state.activeTable.tableId)) 9 | 10 | return ( 11 |
12 | { 13 | seats.map((seat, index) => ) 14 | } 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/chat/service/ChatService.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.chat.service 2 | 3 | import java.util.UUID 4 | 5 | interface ChatService { 6 | fun saveAndPushSystemLobbyChatMessage(message: String) 7 | fun saveAndPushUserLobbyChatMessage(message: String, username: String?) 8 | fun saveAndPushSystemGameChatMessage(gameId: UUID?, message: String) 9 | fun saveAndPushUserGameChatMessage(gameId: UUID?, message: String, username: String?) 10 | fun saveAndPushSystemTableChatMessage(gameId: UUID?, tableId: UUID?, message: String) 11 | fun saveAndPushUserTableChatMessage(gameId: UUID?, tableId: UUID?, message: String, username: String?) 12 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/lobby/GameList/index.tsx: -------------------------------------------------------------------------------- 1 | import GameList from './GameList' 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import { showJoinGameModal, showCreateGameModal } from '../../../reducers' 4 | import { RootState } from '../../..' 5 | 6 | export default () => { 7 | const dispatch = useDispatch() 8 | const gameList = useSelector((state: RootState) => state.openGameList) 9 | 10 | return ( 11 | dispatch(showJoinGameModal(joinGameId))} 13 | openCreateGameModalCallback={() => dispatch(showCreateGameModal())} /> 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/home/InterceptRedirect.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { Navigate } from "react-router-dom"; 3 | import { useSelector, useDispatch } from 'react-redux' 4 | import { clearRedirect } from '../../reducers'; 5 | import { RootState } from '../..'; 6 | 7 | export default ({ children }) => { 8 | const dispatch = useDispatch() 9 | const redirectUrl = useSelector((state: RootState) => state.redirectUrl) 10 | 11 | useEffect(() => { 12 | dispatch(clearRedirect()) 13 | }, [redirectUrl]) 14 | 15 | if (redirectUrl) { 16 | return 17 | } else { 18 | return children 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/command/aggregate/eventproducers/IncrementBlindsEventProducer.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.aggregate.eventproducers 2 | 3 | import com.flexpoker.game.command.GameStage 4 | import com.flexpoker.game.command.aggregate.GameState 5 | import com.flexpoker.game.command.events.BlindsIncreasedEvent 6 | import com.flexpoker.game.command.events.GameEvent 7 | 8 | fun increaseBlinds(state: GameState): List { 9 | require(state.stage === GameStage.INPROGRESS) { "cannot increase blinds if the game isn't in progress" } 10 | return if (!state.blindSchedule.atMaxLevel()) listOf(BlindsIncreasedEvent(state.aggregateId)) else emptyList() 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/query/repository/OpenGameForPlayerRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository 2 | 3 | import com.flexpoker.game.command.GameStage 4 | import com.flexpoker.game.query.dto.OpenGameForUser 5 | import java.util.UUID 6 | 7 | interface OpenGameForPlayerRepository { 8 | fun fetchAllOpenGamesForPlayer(playerId: UUID): List 9 | fun deleteOpenGameForPlayer(playerId: UUID, gameId: UUID) 10 | fun addOpenGameForUser(playerId: UUID, gameId: UUID, gameName: String) 11 | fun changeGameStage(playerId: UUID, gameId: UUID, gameStage: GameStage) 12 | fun assignTableToOpenGame(playerId: UUID, gameId: UUID, tableId: UUID) 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/aggregate/eventproducers/hand/FinishHandIfAppropriateEventProducer.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.eventproducers.hand 2 | 3 | import com.flexpoker.table.command.HandDealerState 4 | import com.flexpoker.table.command.aggregate.HandState 5 | import com.flexpoker.table.command.events.HandCompletedEvent 6 | import com.flexpoker.table.command.events.TableEvent 7 | 8 | fun finishHandIfAppropriate(state: HandState): List { 9 | return if (state.handDealerState === HandDealerState.COMPLETE) 10 | listOf(HandCompletedEvent(state.tableId, state.gameId, state.entityId, state.chipsInBackMap)) 11 | else emptyList() 12 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import { createStore } from 'redux' 3 | import { Provider } from 'react-redux' 4 | import reducer from './reducers' 5 | import App from './App' 6 | import { BrowserRouter } from 'react-router-dom' 7 | 8 | const store = createStore(reducer) 9 | 10 | export type RootState = ReturnType 11 | export type AppDispatch = typeof store.dispatch 12 | 13 | const container = document.getElementById('app') 14 | const root = createRoot(container) 15 | 16 | root.render(( 17 | 18 | 19 | 20 | 21 | 22 | )) 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/aggregate/eventproducers/hand/HandUtils.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.eventproducers.hand 2 | 3 | import com.flexpoker.table.command.PlayerAction 4 | import com.flexpoker.table.command.aggregate.HandState 5 | import java.util.UUID 6 | 7 | 8 | fun checkActionOnPlayer(state: HandState, playerId: UUID) { 9 | require(playerId == state.seatMap[state.actionOnPosition]) { "action is not on the player attempting action" } 10 | } 11 | 12 | fun checkPerformAction(state: HandState, playerId: UUID, playerAction: PlayerAction) { 13 | require(state.possibleSeatActionsMap[playerId]!!.contains(playerAction)) { "not allowed to $playerAction" } 14 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/home/GameTabs/index.tsx: -------------------------------------------------------------------------------- 1 | import { Nav } from 'react-bootstrap' 2 | import { useSelector } from 'react-redux' 3 | import GameTab from './GameTab' 4 | import { LinkContainer } from 'react-router-bootstrap' 5 | import { RootState } from '../../..' 6 | 7 | export default () => { 8 | const openGameTabs = useSelector((state: RootState) => state.openGameTabs) 9 | 10 | return ( 11 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/command/aggregate/eventproducers/CreateGameEventProducer.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.aggregate.eventproducers 2 | 3 | import com.flexpoker.game.command.events.GameCreatedEvent 4 | import com.flexpoker.game.command.events.GameEvent 5 | import java.util.UUID 6 | 7 | fun createGame(gameName: String, maxNumberOfPlayers: Int, numberOfPlayersPerTable: Int, 8 | createdById: UUID, numberOfMinutesBetweenBlindLevels: Int, numberOfSecondsForActionOnTimer: Int 9 | ): List { 10 | return listOf(GameCreatedEvent(UUID.randomUUID(), gameName, maxNumberOfPlayers, numberOfPlayersPerTable, 11 | createdById, numberOfMinutesBetweenBlindLevels, numberOfSecondsForActionOnTimer)) 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/handlers/HandCompletedEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.handlers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.table.command.events.HandCompletedEvent 5 | import com.flexpoker.table.query.repository.CardsUsedInHandRepository 6 | import org.springframework.stereotype.Component 7 | import javax.inject.Inject 8 | 9 | @Component 10 | class HandCompletedEventHandler @Inject constructor( 11 | private val cardsUsedInHandRepository: CardsUsedInHandRepository, 12 | ) : EventHandler { 13 | 14 | override fun handle(event: HandCompletedEvent) { 15 | cardsUsedInHandRepository.removeHand(event.handId) 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/util/MessagingConstants.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.util 2 | 3 | object MessagingConstants { 4 | const val CHAT_LOBBY = "/topic/chat/lobby" 5 | const val CHAT_GAME = "/topic/chat/game/%s" 6 | const val CHAT_TABLE = "/topic/chat/game/%s/table/%s" 7 | const val TABLE_STATUS = "/topic/game/%s/table/%s" 8 | const val TICK_ACTION_ON_TIMER = "/topic/game/%s/table/%s/action-on-tick" 9 | const val PERSONAL_TABLE_STATUS = "/queue/personaltablestatus" 10 | const val POCKET_CARDS = "/queue/pocketcards" 11 | const val OPEN_GAMES_FOR_USER = "/queue/opengamesforuser" 12 | const val OPEN_TABLE_FOR_USER = "/queue/opentable" 13 | const val GAMES_UPDATED = "/topic/availabletournaments" 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/signup/repository/SignUpRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.signup.repository 2 | 3 | import com.flexpoker.signup.SignUpUser 4 | import java.util.UUID 5 | 6 | interface SignUpRepository { 7 | fun usernameExists(username: String): Boolean 8 | fun signUpCodeExists(signUpCode: UUID): Boolean 9 | fun findAggregateIdByUsernameAndSignUpCode(username: String, signUpCode: UUID): UUID? 10 | fun findSignUpCodeByUsername(username: String): UUID 11 | fun storeSignUpInformation(aggregateId: UUID, username: String, signUpCode: UUID) 12 | fun storeNewlyConfirmedUsername(username: String) 13 | fun fetchSignUpUser(signUpUserId: UUID): SignUpUser? 14 | fun saveSignUpUser(signUpUser: SignUpUser) 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/web/controller/HomeController.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.web.controller 2 | 3 | import org.springframework.core.io.ClassPathResource 4 | import org.springframework.http.MediaType 5 | import org.springframework.stereotype.Controller 6 | import org.springframework.web.bind.annotation.GetMapping 7 | import org.springframework.web.bind.annotation.ResponseBody 8 | import java.nio.charset.Charset 9 | 10 | @Controller 11 | class HomeController { 12 | 13 | @GetMapping(path = ["/", "/game/*", "/game/*/table/*"], produces = [MediaType.TEXT_HTML_VALUE]) 14 | @ResponseBody 15 | fun index(): String { 16 | return ClassPathResource("index.html").getContentAsString(Charset.defaultCharset()) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/home/Logout.ts: -------------------------------------------------------------------------------- 1 | import WebSocketService from '../webSocket/WebSocketService' 2 | 3 | export default () => { 4 | WebSocketService.disconnect() 5 | 6 | const headerElem: HTMLMetaElement = document.querySelector("meta[name='_csrf_header']") 7 | const tokenElem: HTMLMetaElement = document.querySelector("meta[name='_csrf']") 8 | 9 | const myHeaders = new Headers() 10 | myHeaders.append(headerElem.content, tokenElem.content) 11 | 12 | const myInit: RequestInit = { 13 | method: 'POST', 14 | headers: myHeaders, 15 | cache: 'no-cache', 16 | redirect: 'follow', 17 | credentials: 'same-origin' 18 | } 19 | 20 | fetch('/logout', myInit).then(response => location.href = response.url) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/query/dto/GameQueryDTOs.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.dto 2 | 3 | import com.flexpoker.game.command.GameStage 4 | import java.util.UUID 5 | 6 | data class GameInListDTO (val id: UUID, val name: String, val stage: String, 7 | val numberOfRegisteredPlayers: Int, val maxNumberOfPlayers: Int, 8 | val maxPlayersPerTable: Int, val blindLevelIncreaseInMinutes: Int, 9 | val actionOnTimerInSeconds: Int, val createdBy: String, val createdOn: String) 10 | 11 | data class OpenGameForUser (val gameId: UUID, val myTableId: UUID?, val name: String, 12 | val gameStage: GameStage, val ordinal: Int, val viewingTables: List) -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/web/commandsenders/InMemoryAsyncGameCommandSender.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.web.commandsenders 2 | 3 | import com.flexpoker.framework.command.CommandReceiver 4 | import com.flexpoker.framework.command.CommandSender 5 | import com.flexpoker.game.command.commands.GameCommand 6 | import org.springframework.context.annotation.Lazy 7 | import org.springframework.stereotype.Component 8 | import javax.inject.Inject 9 | 10 | @Component 11 | class InMemoryAsyncGameCommandSender @Lazy @Inject constructor( 12 | private val gameCommandReceiver: CommandReceiver 13 | ) : CommandSender { 14 | 15 | override fun send(command: GameCommand) { 16 | gameCommandReceiver.receive(command) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/aggregate/RandomNumberGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate 2 | 3 | import java.util.UUID 4 | import kotlin.math.absoluteValue 5 | import kotlin.random.Random 6 | 7 | interface RandomNumberGenerator { 8 | fun int(exclusiveMax: Int): Int 9 | fun pseudoRandomIntBasedOnUUID(uuid: UUID, exclusiveMax: Int): Int 10 | } 11 | 12 | class DefaultRandomNumberGenerator : RandomNumberGenerator { 13 | override fun int(exclusiveMax: Int): Int { 14 | return Random.nextInt(exclusiveMax) 15 | } 16 | 17 | override fun pseudoRandomIntBasedOnUUID(uuid: UUID, exclusiveMax: Int): Int { 18 | return (uuid.leastSignificantBits.absoluteValue.toDouble() % exclusiveMax).toInt() 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/web/commandsenders/InMemoryAsyncTableCommandSender.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.web.commandsenders 2 | 3 | import com.flexpoker.framework.command.CommandReceiver 4 | import com.flexpoker.framework.command.CommandSender 5 | import com.flexpoker.table.command.commands.TableCommand 6 | import org.springframework.context.annotation.Lazy 7 | import org.springframework.stereotype.Component 8 | import javax.inject.Inject 9 | 10 | @Component 11 | class InMemoryAsyncTableCommandSender @Lazy @Inject constructor( 12 | private val tableCommandReceiver: CommandReceiver 13 | ) : CommandSender { 14 | 15 | override fun send(command: TableCommand) { 16 | tableCommandReceiver.receive(command) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/table/Seat.tsx: -------------------------------------------------------------------------------- 1 | import ActionOnTimer from './ActionOnTimer' 2 | 3 | export default ({mySeat, seat, actionOnTick}) => { 4 | return ( 5 |
9 | {seat.isActionOn ? : null} 10 |

{seat.name}

11 |

{seat.chipsInFront}

12 |

{seat.chipsInBack}

13 | {seat.isButton ? : null} 14 | {seat.isSmallBlind ? : null} 15 | {seat.isBigBlind ? : null} 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/command/aggregate/DefaultRandomNumberGeneratorTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate 2 | 3 | import com.flexpoker.test.util.UnitTestClass 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Test 6 | import java.util.UUID 7 | 8 | @UnitTestClass 9 | class DefaultRandomNumberGeneratorTest { 10 | 11 | @Test 12 | fun testInt() { 13 | val actual = DefaultRandomNumberGenerator().int(1) 14 | assertEquals(0, actual) 15 | } 16 | 17 | @Test 18 | fun testPseudoRandomIntBasedOnUUID() { 19 | val uuid = UUID.fromString("5af9acbf-9ae9-45f9-b0c8-0d13531ad6a7") 20 | val actual = DefaultRandomNumberGenerator().pseudoRandomIntBasedOnUUID(uuid, 5) 21 | assertEquals(3, actual) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/config/WebConfig.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.config 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.scheduling.annotation.EnableAsync 5 | import org.springframework.scheduling.annotation.EnableScheduling 6 | import org.springframework.web.servlet.config.annotation.EnableWebMvc 7 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer 9 | 10 | @Configuration 11 | @EnableWebMvc 12 | @EnableScheduling 13 | @EnableAsync 14 | class WebConfig : WebMvcConfigurer { 15 | 16 | override fun addResourceHandlers(registry: ResourceHandlerRegistry) { 17 | registry.addResourceHandler("/resources/**").addResourceLocations("/resources/") 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/web/dto/IncomingWebDTOs.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.web.dto 2 | 3 | import java.util.UUID 4 | 5 | data class CallTableActionDTO (val gameId: UUID, val tableId: UUID) 6 | 7 | data class IncomingChatMessageDTO (val message: String, val receiverUsernames: List?, 8 | val gameId: UUID?, val tableId: UUID?) 9 | 10 | data class CheckTableActionDTO (val gameId: UUID, val tableId: UUID) 11 | 12 | data class CreateGameDTO (val name: String, val players: Int, val playersPerTable: Int, 13 | val numberOfMinutesBetweenBlindLevels: Int, val numberOfSecondsForActionOnTimer: Int) 14 | 15 | data class FoldTableActionDTO (val gameId: UUID, val tableId: UUID) 16 | 17 | data class RaiseTableActionDTO (val gameId: UUID, val tableId: UUID, val raiseToAmount: Int) -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/home/Chat/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { List } from 'immutable' 3 | 4 | const getActiveChatStream = state => state.activeChatStream 5 | 6 | const getChatMessages = state => state.chatMessages 7 | 8 | export const getChats = createSelector( 9 | [ getActiveChatStream, getChatMessages ], 10 | (activeChatStream, chatMessages) => { 11 | if (activeChatStream.gameId && activeChatStream.tableId) { 12 | const { gameId, tableId } = activeChatStream 13 | return chatMessages.tableMessages.get(tableId, List()) 14 | } else if (activeChatStream.gameId) { 15 | const { gameId } = activeChatStream 16 | return chatMessages.gameMessages.get(gameId, List()) 17 | } else { 18 | return chatMessages.lobbyMessages 19 | } 20 | } 21 | ) 22 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/signup/sign-up-step-2-form.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form } from "react-bootstrap" 2 | 3 | export default ({ error, signUpCode, submitFormCallback }) => { 4 | return ( 5 |
6 |

Sign Up

7 | { 8 | error 9 | ?

{error}

10 | : null 11 | } 12 | 13 | 14 | 15 | 16 | 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/home/MainTabs.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from 'react-router' 2 | import GameTabs from './GameTabs' 3 | import Lobby from '../lobby/Lobby' 4 | import GamePage from '../game/GamePage' 5 | import TablePage from '../table/TablePage' 6 | import Chat from './Chat' 7 | import InterceptorRedirect from './InterceptRedirect' 8 | 9 | const wrapRedirect = (component) => { 10 | return {component} 11 | } 12 | 13 | export default () => { 14 | return ( 15 | <> 16 | 17 | 18 | )} /> 19 | )} /> 20 | )} /> 21 | 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/signup/SignUpUser.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.signup 2 | 3 | import com.flexpoker.exception.FlexPokerException 4 | import java.util.UUID 5 | 6 | class SignUpUser(val aggregateId: UUID, val signUpCode: UUID, val email: String, 7 | val username: String?, val encryptedPassword: String) { 8 | 9 | var isConfirmed = false 10 | 11 | fun confirmSignedUpUser(username: String, signUpCode: UUID) { 12 | checkNotNull(this.username) { "username should be set already" } 13 | check(!isConfirmed) { "confirmed should be false" } 14 | if (this.username != username) { 15 | throw FlexPokerException("username does not match") 16 | } 17 | if (this.signUpCode != signUpCode) { 18 | throw FlexPokerException("sign-up code does not match") 19 | } 20 | isConfirmed = true 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/pushnotificationhandlers/ChatSentPushNotificationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.pushnotificationhandlers 2 | 3 | import com.flexpoker.framework.pushnotifier.PushNotificationHandler 4 | import com.flexpoker.pushnotifications.ChatSentPushNotification 5 | import org.springframework.messaging.simp.SimpMessageSendingOperations 6 | import org.springframework.scheduling.annotation.Async 7 | import org.springframework.stereotype.Component 8 | import javax.inject.Inject 9 | 10 | @Component 11 | class ChatSentPushNotificationHandler @Inject constructor(private val messagingTemplate: SimpMessageSendingOperations) : 12 | PushNotificationHandler { 13 | 14 | @Async 15 | override fun handle(pushNotification: ChatSentPushNotification) { 16 | messagingTemplate.convertAndSend(pushNotification.destination, pushNotification) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/chat/repository/InMemoryChatRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.chat.repository 2 | 3 | import com.flexpoker.test.util.InMemoryTestClass 4 | import org.junit.jupiter.api.Test 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.test.annotation.DirtiesContext 7 | 8 | @InMemoryTestClass 9 | class InMemoryChatRepositoryTest { 10 | 11 | @Autowired 12 | private lateinit var repository: ChatRepository 13 | 14 | @Test 15 | @DirtiesContext 16 | fun testSaveChatMessage() { 17 | sharedTestSaveChatMessage(repository) 18 | } 19 | 20 | @Test 21 | @DirtiesContext 22 | fun testFetchAllTypesEmpty() { 23 | sharedTestFetchAllTypesEmpty(repository) 24 | } 25 | 26 | @Test 27 | @DirtiesContext 28 | fun testFetchAllTypes() { 29 | sharedTestFetchAllTypes(repository) 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/main/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Home 16 | 17 | 18 | 19 |
20 |
21 |
22 |

FP © 2023

23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/query/repository/impl/InMemoryGamePlayerRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository.impl 2 | 3 | import com.flexpoker.game.query.repository.GamePlayerRepository 4 | import com.flexpoker.test.util.InMemoryTestClass 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.beans.factory.annotation.Autowired 7 | 8 | @InMemoryTestClass 9 | class InMemoryGamePlayerRepositoryTest { 10 | 11 | @Autowired 12 | private lateinit var repository: GamePlayerRepository 13 | 14 | @Test 15 | fun testAddPlayerToGame() { 16 | sharedTestAddPlayerToGame(repository) 17 | } 18 | 19 | @Test 20 | fun testAddPlayerToGameDuplicate() { 21 | sharedTestAddPlayerToGameDuplicate(repository) 22 | } 23 | 24 | @Test 25 | fun testFetchAllPlayerIdsForGameNoneAdded() { 26 | sharedTestFetchAllPlayerIdsForGameNoneAdded(repository) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/command/commands/GameCommands.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.commands 2 | 3 | import java.time.Instant 4 | import java.util.UUID 5 | 6 | sealed class GameCommand (val id: UUID = UUID.randomUUID(), val time: Instant = Instant.now()) 7 | 8 | data class AttemptToStartNewHandCommand(val aggregateId: UUID, val tableId: UUID, 9 | val playerToChipsAtTableMap: Map) : GameCommand() 10 | 11 | data class CreateGameCommand(val gameName: String, val numberOfPlayers: Int, val numberOfPlayersPerTable: Int, 12 | val createdByPlayerId: UUID, val numberOfMinutesBetweenBlindLevels: Int, 13 | val numberOfSecondsForActionOnTimer: Int) : GameCommand() 14 | 15 | data class IncrementBlindsCommand(val aggregateId: UUID) : GameCommand() 16 | 17 | data class JoinGameCommand(val aggregateId: UUID, val playerId: UUID) : GameCommand() 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/aggregate/eventproducers/PauseResumeTableEventProducer.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.eventproducers 2 | 3 | import com.flexpoker.exception.FlexPokerException 4 | import com.flexpoker.table.command.aggregate.TableState 5 | import com.flexpoker.table.command.events.TableEvent 6 | import com.flexpoker.table.command.events.TablePausedEvent 7 | import com.flexpoker.table.command.events.TableResumedEvent 8 | 9 | fun pause(state: TableState): List { 10 | if (state.paused) { 11 | throw FlexPokerException("table is already paused. can't pause again.") 12 | } 13 | return listOf(TablePausedEvent(state.aggregateId, state.gameId)) 14 | } 15 | 16 | fun resume(state: TableState): List { 17 | if (!state.paused) { 18 | throw FlexPokerException("table is not paused. can't resume.") 19 | } 20 | return listOf(TableResumedEvent(state.aggregateId, state.gameId)) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/query/repository/impl/InMemoryGamePlayerRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository.impl 2 | 3 | import com.flexpoker.config.ProfileNames 4 | import com.flexpoker.game.query.repository.GamePlayerRepository 5 | import org.springframework.context.annotation.Profile 6 | import org.springframework.stereotype.Repository 7 | import java.util.UUID 8 | 9 | @Profile(ProfileNames.DEFAULT, ProfileNames.GAME_QUERY_INMEMORY) 10 | @Repository 11 | class InMemoryGamePlayerRepository : GamePlayerRepository { 12 | 13 | private val gameIdToPlayerId: MutableMap> = HashMap() 14 | 15 | override fun addPlayerToGame(playerId: UUID, gameId: UUID) { 16 | gameIdToPlayerId.putIfAbsent(gameId, HashSet()) 17 | gameIdToPlayerId[gameId]!!.add(playerId) 18 | } 19 | 20 | override fun fetchAllPlayerIdsForGame(gameId: UUID): Set { 21 | return gameIdToPlayerId[gameId] ?: HashSet() 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/test/util/TestClassAnnotations.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.test.util 2 | 3 | import org.junit.jupiter.api.Tag 4 | import org.springframework.boot.test.context.SpringBootTest 5 | import org.springframework.test.annotation.DirtiesContext 6 | import org.springframework.test.context.ActiveProfiles 7 | import org.testcontainers.junit.jupiter.Testcontainers 8 | 9 | @Target(AnnotationTarget.CLASS) 10 | @SpringBootTest 11 | @ActiveProfiles(profiles = ["redis"]) 12 | @Testcontainers 13 | @DirtiesContext 14 | @Tag("repository-redis") 15 | annotation class RedisTestClass 16 | 17 | @Target(AnnotationTarget.CLASS) 18 | @SpringBootTest 19 | @ActiveProfiles(profiles = ["default"]) 20 | @DirtiesContext 21 | @Tag("repository-inmemory") 22 | annotation class InMemoryTestClass 23 | 24 | @Target(AnnotationTarget.CLASS) 25 | @Tag("archunit") 26 | annotation class ArchUnitTestClass 27 | 28 | @Target(AnnotationTarget.CLASS) 29 | @Tag("unit") 30 | annotation class UnitTestClass 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/processmanagers/PauseTableProcessManager.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.processmanagers 2 | 3 | import com.flexpoker.framework.command.CommandSender 4 | import com.flexpoker.framework.processmanager.ProcessManager 5 | import com.flexpoker.game.command.events.TablePausedForBalancingEvent 6 | import com.flexpoker.table.command.commands.PauseCommand 7 | import com.flexpoker.table.command.commands.TableCommand 8 | import org.springframework.scheduling.annotation.Async 9 | import org.springframework.stereotype.Component 10 | import javax.inject.Inject 11 | 12 | @Component 13 | class PauseTableProcessManager @Inject constructor( 14 | private val tableCommandSender: CommandSender 15 | ) : ProcessManager { 16 | 17 | @Async 18 | override fun handle(event: TablePausedForBalancingEvent) { 19 | val command = PauseCommand(event.tableId, event.aggregateId) 20 | tableCommandSender.send(command) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/processmanagers/ResumeTableProcessManager.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.processmanagers 2 | 3 | import com.flexpoker.framework.command.CommandSender 4 | import com.flexpoker.framework.processmanager.ProcessManager 5 | import com.flexpoker.game.command.events.TableResumedAfterBalancingEvent 6 | import com.flexpoker.table.command.commands.ResumeCommand 7 | import com.flexpoker.table.command.commands.TableCommand 8 | import org.springframework.scheduling.annotation.Async 9 | import org.springframework.stereotype.Component 10 | import javax.inject.Inject 11 | 12 | @Component 13 | class ResumeTableProcessManager @Inject constructor( 14 | private val tableCommandSender: CommandSender 15 | ) : ProcessManager { 16 | 17 | @Async 18 | override fun handle(event: TableResumedAfterBalancingEvent) { 19 | val command = ResumeCommand(event.tableId, event.aggregateId) 20 | tableCommandSender.send(command) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/repository/CardsUsedInHandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.repository 2 | 3 | import com.flexpoker.table.command.FlopCards 4 | import com.flexpoker.table.command.PocketCards 5 | import com.flexpoker.table.command.RiverCard 6 | import com.flexpoker.table.command.TurnCard 7 | import java.util.UUID 8 | 9 | interface CardsUsedInHandRepository { 10 | fun saveFlopCards(handId: UUID, flopCards: FlopCards) 11 | fun saveTurnCard(handId: UUID, turnCard: TurnCard) 12 | fun saveRiverCard(handId: UUID, riverCard: RiverCard) 13 | fun savePocketCards(handId: UUID, playerToPocketCardsMap: Map) 14 | fun fetchFlopCards(handId: UUID): FlopCards? 15 | fun fetchTurnCard(handId: UUID): TurnCard? 16 | fun fetchRiverCard(handId: UUID): RiverCard? 17 | fun fetchPocketCards(handId: UUID, playerId: UUID): PocketCards? 18 | fun fetchAllPocketCardsForUser(playerId: UUID): Map 19 | fun removeHand(handId: UUID) 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/config/ProfileNames.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.config 2 | 3 | object ProfileNames { 4 | /** In-memory and default */ 5 | const val DEFAULT = "default" 6 | const val CHAT_INMEMORY = "chat-inmemory" 7 | const val LOGIN_INMEMORY = "login-inmemory" 8 | const val SIGNUP_INMEMORY = "signup-inmemory" 9 | const val GAME_COMMAND_INMEMORY = "game-command-inmemory" 10 | const val GAME_QUERY_INMEMORY = "game-query-inmemory" 11 | const val TABLE_COMMAND_INMEMORY = "table-command-inmemory" 12 | const val TABLE_QUERY_INMEMORY = "table-query-inmemory" 13 | 14 | /** Redis */ 15 | const val REDIS = "redis" 16 | const val CHAT_REDIS = "chat-redis" 17 | const val LOGIN_REDIS = "login-redis" 18 | const val SIGNUP_REDIS = "signup-redis" 19 | const val GAME_COMMAND_REDIS = "game-command-redis" 20 | const val GAME_QUERY_REDIS = "game-query-redis" 21 | const val TABLE_COMMAND_REDIS = "table-command-redis" 22 | const val TABLE_QUERY_REDIS = "table-query-redis" 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/dto/TableQueryDTOs.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.dto 2 | 3 | import java.util.UUID 4 | 5 | data class CardDTO (val id: Int) 6 | 7 | data class PocketCardsDTO (val handId: UUID, val cardId1: Int, val cardId2: Int) 8 | 9 | data class PotDTO (val seats: Set, val amount: Int, val isOpen: Boolean, val winners: Set) 10 | 11 | data class SeatDTO (val position: Int, val name: String, val chipsInBack: Int, val chipsInFront: Int, 12 | val isStillInHand: Boolean, val raiseTo: Int, val callAmount: Int, val isButton: Boolean, 13 | val isSmallBlind: Boolean, val isBigBlind: Boolean, val isActionOn: Boolean) 14 | 15 | data class TableDTO (val id: UUID, val version: Int, val seats: List?, val totalPot: Int, val pots: Set?, 16 | val visibleCommonCards: List?, val currentHandMinRaiseToAmount: Int, val currentHandId: UUID?) 17 | 18 | data class OpenTableForUserDTO(val gameId: UUID, val tableId: UUID) 19 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/home/Navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Navbar, Nav, NavDropdown } from 'react-bootstrap' 2 | import Logout from '../Logout' 3 | 4 | export default ({username}) => { 5 | return ( 6 | 7 | 8 | Flex Poker 9 | { 10 | username 11 | ? 12 | 20 | 21 | : null 22 | } 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/TickActionOnTimerCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 5 | import com.flexpoker.pushnotifications.TickActionOnTimerPushNotification 6 | import com.flexpoker.table.command.commands.TickActionOnTimerCommand 7 | import org.springframework.scheduling.annotation.Async 8 | import org.springframework.stereotype.Component 9 | import javax.inject.Inject 10 | 11 | @Component 12 | class TickActionOnTimerCommandHandler @Inject constructor( 13 | private val pushNotificationPublisher: PushNotificationPublisher 14 | ) : CommandHandler { 15 | 16 | @Async 17 | override fun handle(command: TickActionOnTimerCommand) { 18 | val pushNotification = TickActionOnTimerPushNotification(command.gameId, command.tableId, command.number) 19 | pushNotificationPublisher.publish(pushNotification) 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flex-poker", 3 | "version": "1.0.0", 4 | "description": "", 5 | "engines": { 6 | "node": "20.10.0" 7 | }, 8 | "scripts": { 9 | "dev": "webpack --mode=development --watch", 10 | "prod": "webpack --mode=production" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "immutable": "4.3.7", 16 | "moment": "2.30.1", 17 | "moment-timezone": "0.5.45", 18 | "react": "19.0.0", 19 | "react-bootstrap": "2.10.4", 20 | "react-dom": "19.0.0", 21 | "react-redux": "9.2.0", 22 | "react-router-bootstrap": "0.26.3", 23 | "react-router-dom": "6.26.2", 24 | "redux": "5.0.1", 25 | "reselect": "5.1.1", 26 | "sockjs-client": "1.6.1", 27 | "webstomp-client": "1.2.6" 28 | }, 29 | "devDependencies": { 30 | "http-server": "14.1.1", 31 | "ts-loader": "9.5.1", 32 | "typescript": "5.6.2", 33 | "@types/react-dom": "19.0.2", 34 | "@types/sockjs-client": "1.5.4", 35 | "webpack": "5.94.0", 36 | "webpack-cli": "5.1.4" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/command/repository/InMemoryTableEventRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.repository 2 | 3 | import com.flexpoker.test.util.InMemoryTestClass 4 | import org.junit.jupiter.api.Test 5 | import org.springframework.beans.factory.annotation.Autowired 6 | 7 | @InMemoryTestClass 8 | class InMemoryTableEventRepositoryTest { 9 | 10 | @Autowired 11 | private lateinit var repository: TableEventRepository 12 | 13 | @Test 14 | fun testFetchAll() { 15 | sharedTestFetchAll(repository) 16 | } 17 | 18 | @Test 19 | fun testSetEventVersionsAndSaveBadBasedOnVersion1() { 20 | sharedTestSetEventVersionsAndSaveBadBasedOnVersion1(repository) 21 | } 22 | 23 | @Test 24 | fun testSetEventVersionsAndSaveTwoJoinsInOneRequest() { 25 | sharedTestSetEventVersionsAndSaveTwoFakeEventsInOneRequest(repository) 26 | } 27 | 28 | @Test 29 | fun testSetEventVersionsAndSaveEmptyList() { 30 | sharedTestSetEventVersionsAndSaveEmptyList(repository) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/processmanagers/AttemptToStartNewHandForExistingTableProcessManager.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.processmanagers 2 | 3 | import com.flexpoker.framework.command.CommandSender 4 | import com.flexpoker.framework.processmanager.ProcessManager 5 | import com.flexpoker.game.command.commands.AttemptToStartNewHandCommand 6 | import com.flexpoker.game.command.commands.GameCommand 7 | import com.flexpoker.table.command.events.HandCompletedEvent 8 | import org.springframework.scheduling.annotation.Async 9 | import org.springframework.stereotype.Component 10 | import javax.inject.Inject 11 | 12 | @Component 13 | class AttemptToStartNewHandForExistingTableProcessManager @Inject constructor( 14 | private val gameCommandSender: CommandSender 15 | ) : ProcessManager { 16 | 17 | @Async 18 | override fun handle(event: HandCompletedEvent) { 19 | val command = AttemptToStartNewHandCommand(event.gameId, event.aggregateId, event.playerToChipsAtTableMap) 20 | gameCommandSender.send(command) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/command/aggregate/pot/PotTestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.pot 2 | 3 | import com.flexpoker.table.command.CardRank 4 | import com.flexpoker.table.command.HandRanking 5 | import com.flexpoker.table.command.aggregate.HandEvaluation 6 | import java.util.UUID 7 | 8 | fun createPotHand(playerId: UUID): HandEvaluation { 9 | return HandEvaluation( 10 | playerId = playerId, 11 | handRanking = HandRanking.FLUSH, 12 | primaryCardRank = CardRank.EIGHT, 13 | firstKicker = CardRank.SEVEN, 14 | secondKicker = CardRank.FOUR, 15 | thirdKicker = CardRank.THREE, 16 | fourthKicker = CardRank.TWO, 17 | ) 18 | } 19 | 20 | fun createPotHands(player1: UUID, player2: UUID): List { 21 | val handEvaluation1 = createPotHand(player1) 22 | val handEvaluation2 = HandEvaluation( 23 | playerId = player2, 24 | handRanking = HandRanking.STRAIGHT, 25 | primaryCardRank = CardRank.KING, 26 | ) 27 | return listOf(handEvaluation1, handEvaluation2) 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/handlers/PlayerBustedTableEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.handlers 2 | 3 | import com.flexpoker.chat.service.ChatService 4 | import com.flexpoker.framework.event.EventHandler 5 | import com.flexpoker.login.repository.LoginRepository 6 | import com.flexpoker.table.command.events.PlayerBustedTableEvent 7 | import org.springframework.stereotype.Component 8 | import javax.inject.Inject 9 | 10 | @Component 11 | class PlayerBustedTableEventHandler @Inject constructor( 12 | private val loginRepository: LoginRepository, 13 | private val chatService: ChatService 14 | ) : EventHandler { 15 | 16 | override fun handle(event: PlayerBustedTableEvent) { 17 | handleChat(event) 18 | } 19 | 20 | private fun handleChat(event: PlayerBustedTableEvent) { 21 | val username = loginRepository.fetchUsernameByAggregateId(event.playerId) 22 | val message = "$username is out" 23 | chatService.saveAndPushSystemTableChatMessage(event.gameId, event.aggregateId, message) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/web/controller/LoginController.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.web.controller 2 | 3 | import org.springframework.core.io.ClassPathResource 4 | import org.springframework.http.HttpStatus 5 | import org.springframework.http.MediaType 6 | import org.springframework.http.ResponseEntity 7 | import org.springframework.stereotype.Controller 8 | import org.springframework.web.bind.annotation.GetMapping 9 | import org.springframework.web.bind.annotation.ResponseBody 10 | import java.nio.charset.Charset 11 | import java.security.Principal 12 | 13 | @Controller 14 | class LoginController { 15 | 16 | @GetMapping("/login", produces = [MediaType.TEXT_HTML_VALUE]) 17 | @ResponseBody 18 | fun index(): String = ClassPathResource("index.html").getContentAsString(Charset.defaultCharset()) 19 | 20 | @GetMapping("/userinfo") 21 | fun userInfo(principal: Principal?): ResponseEntity> { 22 | val username = principal?.name 23 | return ResponseEntity(mapOf("username" to username, "loggedIn" to (username != null)), HttpStatus.OK) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/signup/sign-up-step-1-form.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form } from "react-bootstrap" 2 | 3 | export default ({ error, submitFormCallback }) => { 4 | return ( 5 |
6 |

Sign Up

7 | { error ?

{error}

: null } 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/login/repository/InMemoryLoginRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.login.repository 2 | 3 | import com.flexpoker.test.util.InMemoryTestClass 4 | import org.junit.jupiter.api.Test 5 | import org.springframework.beans.factory.annotation.Autowired 6 | 7 | @InMemoryTestClass 8 | class InMemoryLoginRepositoryTest { 9 | 10 | @Autowired 11 | private lateinit var repository: LoginRepository 12 | 13 | @Test 14 | fun testLoadUserByUsername() { 15 | sharedTestLoadUserByUsername(repository) 16 | } 17 | 18 | @Test 19 | fun testFetchAggregateIdByUsername() { 20 | sharedTestFetchAggregateIdByUsername(repository) 21 | } 22 | 23 | @Test 24 | fun testFetchUsernameByAggregateId() { 25 | sharedTestFetchUsernameByAggregateId(repository) 26 | } 27 | 28 | @Test 29 | fun testSaveAggregateIdAndUsername() { 30 | sharedTestSaveAggregateIdAndUsername(repository) 31 | } 32 | 33 | @Test 34 | fun testSaveUsernameAndPassword() { 35 | sharedTestSaveUsernameAndPassword(repository) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/pushnotificationhandlers/TickActionOnTimerPushNotificationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.pushnotificationhandlers 2 | 3 | import com.flexpoker.framework.pushnotifier.PushNotificationHandler 4 | import com.flexpoker.pushnotifications.TickActionOnTimerPushNotification 5 | import com.flexpoker.util.MessagingConstants 6 | import org.springframework.messaging.simp.SimpMessageSendingOperations 7 | import org.springframework.scheduling.annotation.Async 8 | import org.springframework.stereotype.Component 9 | import javax.inject.Inject 10 | 11 | @Component 12 | class TickActionOnTimerPushNotificationHandler @Inject constructor(private val messagingTemplate: SimpMessageSendingOperations) : 13 | PushNotificationHandler { 14 | 15 | @Async 16 | override fun handle(pushNotification: TickActionOnTimerPushNotification) { 17 | messagingTemplate.convertAndSend( 18 | String.format(MessagingConstants.TICK_ACTION_ON_TIMER, pushNotification.gameId, pushNotification.tableId), 19 | pushNotification.number) 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/aggregate/eventproducers/hand/DetermineWinnersIfAppropriate.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.eventproducers.hand 2 | 3 | import com.flexpoker.table.command.HandDealerState 4 | import com.flexpoker.table.command.aggregate.HandState 5 | import com.flexpoker.table.command.aggregate.fetchChipsWon 6 | import com.flexpoker.table.command.aggregate.fetchPlayersRequiredToShowCards 7 | import com.flexpoker.table.command.events.TableEvent 8 | import com.flexpoker.table.command.events.WinnersDeterminedEvent 9 | 10 | fun determineWinnersIfAppropriate(state: HandState): List { 11 | return if (state.handDealerState === HandDealerState.COMPLETE) { 12 | val playersRequiredToShowCards = fetchPlayersRequiredToShowCards(state.pots, state.playersStillInHand) 13 | val playersToChipsWonMap = fetchChipsWon(state.pots, state.playersStillInHand) 14 | listOf(WinnersDeterminedEvent(state.tableId, state.gameId, state.entityId, 15 | playersRequiredToShowCards, playersToChipsWonMap)) 16 | } else { 17 | emptyList() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/processmanagers/StartNewHandForExistingTableProcessManager.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.processmanagers 2 | 3 | import com.flexpoker.framework.command.CommandSender 4 | import com.flexpoker.framework.processmanager.ProcessManager 5 | import com.flexpoker.game.command.events.NewHandIsClearedToStartEvent 6 | import com.flexpoker.table.command.commands.StartNewHandForExistingTableCommand 7 | import com.flexpoker.table.command.commands.TableCommand 8 | import org.springframework.scheduling.annotation.Async 9 | import org.springframework.stereotype.Component 10 | import javax.inject.Inject 11 | 12 | @Component 13 | class StartNewHandForExistingTableProcessManager @Inject constructor( 14 | private val tableCommandSender: CommandSender 15 | ) : ProcessManager { 16 | 17 | @Async 18 | override fun handle(event: NewHandIsClearedToStartEvent) { 19 | val command = StartNewHandForExistingTableCommand(event.tableId, event.aggregateId, 20 | event.blinds.smallBlind, event.blinds.bigBlind) 21 | tableCommandSender.send(command) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: { 6 | app: './src/main/webapp/resources/index.tsx', 7 | }, 8 | 9 | output: { 10 | path: path.join(__dirname, 'src/main/webapp/resources'), 11 | filename: '[name].bundle.js', 12 | }, 13 | 14 | optimization: { 15 | splitChunks: { 16 | cacheGroups: { 17 | vendor: { 18 | test: /node_modules/, 19 | chunks: 'initial', 20 | name: 'vendor', 21 | priority: 10, 22 | enforce: true 23 | } 24 | } 25 | } 26 | }, 27 | 28 | resolve: { 29 | extensions: ['.tsx', '.ts', '.js'], 30 | }, 31 | 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.tsx?$/, 36 | use: [ 37 | { 38 | loader: 'ts-loader' 39 | } 40 | ], 41 | exclude: [ 42 | '/node_modules/', 43 | '/documentation/', 44 | '/bin/', 45 | '/target/', 46 | '/output/', 47 | '/dist/', 48 | ] 49 | } 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/pushnotificationhandlers/GameListUpdatedPushNotificationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.pushnotificationhandlers 2 | 3 | import com.flexpoker.framework.pushnotifier.PushNotificationHandler 4 | import com.flexpoker.game.query.repository.GameListRepository 5 | import com.flexpoker.pushnotifications.GameListUpdatedPushNotification 6 | import com.flexpoker.util.MessagingConstants 7 | import org.springframework.messaging.simp.SimpMessageSendingOperations 8 | import org.springframework.scheduling.annotation.Async 9 | import org.springframework.stereotype.Component 10 | import javax.inject.Inject 11 | 12 | @Component 13 | class GameListUpdatedPushNotificationHandler @Inject constructor( 14 | private val gameListRepository: GameListRepository, 15 | private val messagingTemplate: SimpMessageSendingOperations 16 | ) : PushNotificationHandler { 17 | 18 | @Async 19 | override fun handle(pushNotification: GameListUpdatedPushNotification) { 20 | val allGames = gameListRepository.fetchAll() 21 | messagingTemplate.convertAndSend(MessagingConstants.GAMES_UPDATED, allGames) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/config/WebSocketConfig.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.config 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.messaging.simp.config.ChannelRegistration 5 | import org.springframework.messaging.simp.config.MessageBrokerRegistry 6 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker 7 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry 8 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer 9 | 10 | @Configuration 11 | @EnableWebSocketMessageBroker 12 | class WebSocketConfig : WebSocketMessageBrokerConfigurer { 13 | 14 | override fun registerStompEndpoints(registry: StompEndpointRegistry) { 15 | registry.addEndpoint("/application").withSockJS() 16 | } 17 | 18 | override fun configureMessageBroker(registry: MessageBrokerRegistry) { 19 | registry.enableSimpleBroker("/queue/", "/topic/") 20 | } 21 | 22 | override fun configureClientOutboundChannel(registration: ChannelRegistration) { 23 | registration.taskExecutor().corePoolSize(4).maxPoolSize(10) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/webSocket/WebSocketService.ts: -------------------------------------------------------------------------------- 1 | import webstomp, { Client, Message } from 'webstomp-client' 2 | import SockJS from 'sockjs-client' 3 | 4 | const WebSocketService = () => { 5 | let client: Client; 6 | 7 | const registerSubscription = (location: string, subscription: (message: Message) => any) => { 8 | return new Promise((resolve, reject) => { 9 | if (client.connected) { 10 | resolve(client.subscribe(location, subscription)) 11 | } else { 12 | document.addEventListener('webSocketConnected', evt => { 13 | resolve(client.subscribe(location, subscription)) 14 | }) 15 | } 16 | }) 17 | } 18 | 19 | const send = (location: string, objectToSend) => client.send(location, JSON.stringify(objectToSend)) 20 | 21 | const connect = () => { 22 | client = webstomp.over(new SockJS('/application'), {debug: false}) 23 | client.connect({}, frame => document.dispatchEvent(new Event('webSocketConnected'))) 24 | } 25 | 26 | const disconnect = () => client.disconnect() 27 | 28 | return { registerSubscription, send, connect, disconnect } 29 | } 30 | 31 | export default WebSocketService() 32 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/webSocket/WebSocketSubscriptionManager.ts: -------------------------------------------------------------------------------- 1 | import WebSocketService from './WebSocketService' 2 | 3 | const WebSocketSubscriptionManager = () => { 4 | 5 | const componentToSubscriptionsMap = new Map() 6 | 7 | const subscribe = (component, subscriptions) => { 8 | const existingSubscriptions = componentToSubscriptionsMap.get(component) || [] 9 | 10 | const newSubscriptions = subscriptions.filter(x => 11 | existingSubscriptions.filter(y => y.location === x.location).length === 0) 12 | 13 | const registeredSubs = newSubscriptions.map(x => ({ 14 | location: x.location, 15 | subscription: WebSocketService.registerSubscription(x.location, x.subscription) 16 | })) 17 | componentToSubscriptionsMap.set(component, existingSubscriptions.concat(registeredSubs)) 18 | } 19 | 20 | const unsubscribe = (component) => { 21 | componentToSubscriptionsMap.get(component).forEach(subscription => 22 | subscription.subscription.then(unsubscribeCallback => unsubscribeCallback.unsubscribe())) 23 | } 24 | 25 | return ({subscribe, unsubscribe}) 26 | 27 | } 28 | 29 | export default WebSocketSubscriptionManager() 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/pushnotifications/PushNotifications.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.pushnotifications 2 | 3 | import com.flexpoker.table.command.PocketCards 4 | import java.util.UUID 5 | 6 | sealed class PushNotification 7 | 8 | data class ChatSentPushNotification(val id: UUID, val gameId: UUID?, val tableId: UUID?, 9 | val message: String, val senderUsername: String?, val systemMessage: Boolean, 10 | val destination: String) : PushNotification() 11 | 12 | object GameListUpdatedPushNotification : PushNotification() 13 | 14 | data class OpenGamesForPlayerUpdatedPushNotification(val playerId: UUID) : PushNotification() 15 | 16 | data class OpenTableForUserPushNotification(val gameId: UUID, val tableId: UUID, val playerId: UUID) : PushNotification() 17 | 18 | data class SendUserPocketCardsPushNotification(val playerId: UUID, val handId: UUID, val pocketCards: PocketCards) : PushNotification() 19 | 20 | data class TableUpdatedPushNotification(val gameId: UUID, val tableId: UUID) : PushNotification() 21 | 22 | data class TickActionOnTimerPushNotification(val gameId: UUID, val tableId: UUID, val number: Int) : PushNotification() -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/processmanagers/StartFirstHandProcessManager.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.processmanagers 2 | 3 | import com.flexpoker.framework.command.CommandSender 4 | import com.flexpoker.framework.processmanager.ProcessManager 5 | import com.flexpoker.game.command.events.GameStartedEvent 6 | import com.flexpoker.table.command.commands.StartNewHandForNewGameCommand 7 | import com.flexpoker.table.command.commands.TableCommand 8 | import org.springframework.scheduling.annotation.Async 9 | import org.springframework.stereotype.Component 10 | import javax.inject.Inject 11 | 12 | @Component 13 | class StartFirstHandProcessManager @Inject constructor( 14 | private val tableCommandSender: CommandSender 15 | ) : ProcessManager { 16 | 17 | @Async 18 | override fun handle(event: GameStartedEvent) { 19 | val blinds = event.blindSchedule.currentBlinds() 20 | event.tableIds.forEach { 21 | val startNewHandForNewGameCommand = StartNewHandForNewGameCommand( 22 | it, event.aggregateId, blinds.smallBlind, blinds.bigBlind) 23 | tableCommandSender.send(startNewHandForNewGameCommand) 24 | } 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/query/handlers/PlayerBustedGameEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.handlers 2 | 3 | import com.flexpoker.chat.service.ChatService 4 | import com.flexpoker.framework.event.EventHandler 5 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 6 | import com.flexpoker.game.command.events.PlayerBustedGameEvent 7 | import com.flexpoker.login.repository.LoginRepository 8 | import org.springframework.stereotype.Component 9 | import javax.inject.Inject 10 | 11 | @Component 12 | class PlayerBustedGameEventHandler @Inject constructor( 13 | private val pushNotificationPublisher: PushNotificationPublisher, 14 | private val loginRepository: LoginRepository, 15 | private val chatService: ChatService 16 | ) : EventHandler { 17 | 18 | override fun handle(event: PlayerBustedGameEvent) { 19 | handleChat(event) 20 | } 21 | 22 | private fun handleChat(event: PlayerBustedGameEvent) { 23 | val username = loginRepository.fetchUsernameByAggregateId(event.playerId) 24 | val message = "$username is out" 25 | chatService.saveAndPushSystemGameChatMessage(event.aggregateId, message) 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/processmanagers/CreateInitialTablesForGameProcessManager.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.processmanagers 2 | 3 | import com.flexpoker.framework.command.CommandSender 4 | import com.flexpoker.framework.processmanager.ProcessManager 5 | import com.flexpoker.game.command.events.GameTablesCreatedAndPlayersAssociatedEvent 6 | import com.flexpoker.table.command.commands.CreateTableCommand 7 | import com.flexpoker.table.command.commands.TableCommand 8 | import org.springframework.scheduling.annotation.Async 9 | import org.springframework.stereotype.Component 10 | import javax.inject.Inject 11 | 12 | @Component 13 | class CreateInitialTablesForGameProcessManager @Inject constructor( 14 | private val tableCommandSender: CommandSender 15 | ) : ProcessManager { 16 | 17 | @Async 18 | override fun handle(event: GameTablesCreatedAndPlayersAssociatedEvent) { 19 | event.tableIdToPlayerIdsMap.keys.forEach { 20 | val command = CreateTableCommand( 21 | it, event.aggregateId, event.tableIdToPlayerIdsMap[it]!!, event.numberOfPlayersPerTable) 22 | tableCommandSender.send(command) 23 | } 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/aggregate/eventproducers/hand/DealCommonCardsIfAppropriateEventProducer.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.eventproducers.hand 2 | 3 | import com.flexpoker.table.command.HandDealerState 4 | import com.flexpoker.table.command.aggregate.HandState 5 | import com.flexpoker.table.command.events.FlopCardsDealtEvent 6 | import com.flexpoker.table.command.events.RiverCardDealtEvent 7 | import com.flexpoker.table.command.events.TableEvent 8 | import com.flexpoker.table.command.events.TurnCardDealtEvent 9 | 10 | fun dealCommonCardsIfAppropriate(state: HandState): List { 11 | return when { 12 | state.handDealerState === HandDealerState.FLOP_DEALT && !state.flopDealt -> 13 | listOf(FlopCardsDealtEvent(state.tableId, state.gameId, state.entityId)) 14 | state.handDealerState === HandDealerState.TURN_DEALT && !state.turnDealt -> 15 | listOf(TurnCardDealtEvent(state.tableId, state.gameId, state.entityId)) 16 | state.handDealerState === HandDealerState.RIVER_DEALT && !state.riverDealt -> 17 | listOf(RiverCardDealtEvent(state.tableId, state.gameId, state.entityId)) 18 | else -> 19 | emptyList() 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/pushnotificationhandlers/TableUpdatedPushNotificationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.pushnotificationhandlers 2 | 3 | import com.flexpoker.framework.pushnotifier.PushNotificationHandler 4 | import com.flexpoker.pushnotifications.TableUpdatedPushNotification 5 | import com.flexpoker.table.query.repository.TableRepository 6 | import com.flexpoker.util.MessagingConstants 7 | import org.springframework.messaging.simp.SimpMessageSendingOperations 8 | import org.springframework.scheduling.annotation.Async 9 | import org.springframework.stereotype.Component 10 | import javax.inject.Inject 11 | 12 | @Component 13 | class TableUpdatedPushNotificationHandler @Inject constructor( 14 | private val messagingTemplate: SimpMessageSendingOperations, 15 | private val tableRepository: TableRepository 16 | ) : PushNotificationHandler { 17 | 18 | @Async 19 | override fun handle(pushNotification: TableUpdatedPushNotification) { 20 | val tableDTO = tableRepository.fetchById(pushNotification.tableId) 21 | messagingTemplate.convertAndSend( 22 | String.format(MessagingConstants.TABLE_STATUS, pushNotification.gameId, pushNotification.tableId), 23 | tableDTO) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/query/repository/impl/RedisGamePlayerRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository.impl 2 | 3 | import com.flexpoker.game.query.repository.GamePlayerRepository 4 | import com.flexpoker.test.util.RedisTestClass 5 | import com.flexpoker.test.util.redisContainer 6 | import org.junit.jupiter.api.Test 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection 9 | import org.testcontainers.junit.jupiter.Container 10 | 11 | @RedisTestClass 12 | class RedisGamePlayerRepositoryTest { 13 | 14 | @Autowired 15 | private lateinit var repository: GamePlayerRepository 16 | 17 | companion object { 18 | @Container 19 | @ServiceConnection 20 | val redisContainer = redisContainer() 21 | } 22 | 23 | @Test 24 | fun testAddPlayerToGame() { 25 | sharedTestAddPlayerToGame(repository) 26 | } 27 | 28 | @Test 29 | fun testAddPlayerToGameDuplicate() { 30 | sharedTestAddPlayerToGameDuplicate(repository) 31 | } 32 | 33 | @Test 34 | fun testFetchAllPlayerIdsForGameNoneAdded() { 35 | sharedTestFetchAllPlayerIdsForGameNoneAdded(repository) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/query/repository/impl/InMemoryCardsUsedInHandRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.repository.impl 2 | 3 | import com.flexpoker.table.query.repository.CardsUsedInHandRepository 4 | import com.flexpoker.test.util.InMemoryTestClass 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.beans.factory.annotation.Autowired 7 | 8 | @InMemoryTestClass 9 | class InMemoryCardsUsedInHandRepositoryTest { 10 | 11 | @Autowired 12 | private lateinit var repository: CardsUsedInHandRepository 13 | 14 | @Test 15 | fun testSaveFlopCards() { 16 | sharedTestSaveFlopCards(repository) 17 | } 18 | 19 | @Test 20 | fun testSaveTurnCard() { 21 | sharedTestSaveTurnCard(repository) 22 | } 23 | 24 | @Test 25 | fun testSaveRiverCard() { 26 | sharedTestSaveRiverCard(repository) 27 | } 28 | 29 | @Test 30 | fun testSavePocketCards() { 31 | sharedTestSavePocketCards(repository) 32 | } 33 | 34 | @Test 35 | fun testFetchAllPocketCardsForUser() { 36 | sharedTestFetchAllPocketCardsForUser(repository) 37 | } 38 | 39 | @Test 40 | fun testRemoveHand() { 41 | sharedTestRemoveHand(repository) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/login/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form } from "react-bootstrap" 2 | 3 | export default () => { 4 | return ( 5 | <> 6 |
7 |

Log in to Flex Poker

8 | { window.location.search.includes('error') ?

Invalid username/password

: null } 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 | Don't have an account? Sign Up! 22 |
23 |
24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/archtest/ServiceArchTests.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.archtest 2 | 3 | import com.flexpoker.test.util.ArchUnitTestClass 4 | import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes 5 | import com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_FIELD_INJECTION 6 | import org.junit.jupiter.api.Test 7 | import org.springframework.stereotype.Service 8 | 9 | @ArchUnitTestClass 10 | class ServiceArchTests { 11 | 12 | private val serviceClasses = nonInterfaceTopLevelClasses(".*Service") 13 | 14 | @Test 15 | fun testAnnotations() { 16 | val classesHaveServiceAnnotation = classes().that(serviceClasses) 17 | .should().beAnnotatedWith(Service::class.java) 18 | classesHaveServiceAnnotation.check(classesUnderTest) 19 | } 20 | 21 | @Test 22 | fun testDependencies() { 23 | val dependencyRule = classes().that().resideInAPackage("..command.service..") 24 | .should().onlyBeAccessed().byAnyPackage("..command.service..", "..command.handlers..") 25 | dependencyRule.check(classesUnderTest) 26 | } 27 | 28 | @Test 29 | fun testNoFieldInjection() { 30 | NO_CLASSES_SHOULD_USE_FIELD_INJECTION.check(classesUnderTest.that(serviceClasses)) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/StaticTableData.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command 2 | 3 | enum class HandRanking { 4 | HIGH_CARD, ONE_PAIR, TWO_PAIR, THREE_OF_A_KIND, STRAIGHT, FLUSH, FULL_HOUSE, FOUR_OF_A_KIND, STRAIGHT_FLUSH 5 | } 6 | 7 | enum class PlayerAction { 8 | CHECK, FOLD, CALL, RAISE 9 | } 10 | 11 | enum class HandDealerState { 12 | NONE, POCKET_CARDS_DEALT, FLOP_DEALT, TURN_DEALT, RIVER_DEALT, COMPLETE 13 | } 14 | 15 | data class Card (val id: Int, val cardRank: CardRank, val cardSuit: CardSuit) : Comparable { 16 | override fun compareTo(other: Card): Int { 17 | return cardRank.compareTo(other.cardRank) 18 | } 19 | } 20 | 21 | enum class CardRank { 22 | TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING, ACE 23 | } 24 | 25 | enum class CardSuit { 26 | HEARTS, SPADES, DIAMONDS, CLUBS 27 | } 28 | 29 | data class CardsUsedInHand(val flopCards: FlopCards, val turnCard: TurnCard, 30 | val riverCard: RiverCard, val pocketCards: List) 31 | 32 | data class FlopCards (val card1: Card, val card2: Card, val card3: Card) 33 | 34 | data class PocketCards (val card1: Card, val card2: Card) 35 | 36 | data class RiverCard (val card: Card) 37 | 38 | data class TurnCard (val card: Card) -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/lobby/JoinGameDialog/index.tsx: -------------------------------------------------------------------------------- 1 | import JoinGameDialog from './JoinGameDialog' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { hideJoinGameModal, redirectToGame } from '../../../reducers' 4 | import WebSocketService from '../../webSocket/WebSocketService' 5 | import { RootState } from '../../..' 6 | 7 | const joinGameFormSubmitted = (hideDialogCallback, redirectToGame, gameId, evt) => { 8 | evt.preventDefault() 9 | WebSocketService.send('/app/joingame', gameId) 10 | hideDialogCallback() 11 | redirectToGame(gameId) 12 | } 13 | 14 | export default () => { 15 | const gameId = useSelector((state: RootState) => state.joinGameId) 16 | const showModal = useSelector((state: RootState) => state.showJoinGameModal) 17 | 18 | const dispatch = useDispatch() 19 | const dispatchHideJoinGameModal = () => dispatch(hideJoinGameModal()) 20 | const dispatchRedirectToGame = gameId => dispatch(redirectToGame(gameId)) 21 | 22 | return ( 23 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/lobby/JoinGameDialog/JoinGameDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, Form } from 'react-bootstrap' 2 | 3 | export default ({gameId, showModal, hideDialogCallback, submitFormCallback}) => { 4 | return ( 5 | 6 | 7 | Join Game 8 | 9 |
10 | 11 | 12 | Current Balance 13 | 14 | 15 | 16 | Cost 17 | 18 | 19 | 20 | Remaining Balance 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/processmanagers/MovePlayerBetweenTablesProcessManager.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.processmanagers 2 | 3 | import com.flexpoker.framework.command.CommandSender 4 | import com.flexpoker.framework.processmanager.ProcessManager 5 | import com.flexpoker.game.command.events.PlayerMovedToNewTableEvent 6 | import com.flexpoker.table.command.commands.AddPlayerCommand 7 | import com.flexpoker.table.command.commands.RemovePlayerCommand 8 | import com.flexpoker.table.command.commands.TableCommand 9 | import org.springframework.scheduling.annotation.Async 10 | import org.springframework.stereotype.Component 11 | import javax.inject.Inject 12 | 13 | @Component 14 | class MovePlayerBetweenTablesProcessManager @Inject constructor( 15 | private val tableCommandSender: CommandSender 16 | ) : ProcessManager { 17 | 18 | @Async 19 | override fun handle(event: PlayerMovedToNewTableEvent) { 20 | val addPlayerTableCommand = AddPlayerCommand(event.toTableId, event.aggregateId, event.playerId, event.chips) 21 | tableCommandSender.send(addPlayerTableCommand) 22 | val removePlayerTableCommand = RemovePlayerCommand(event.fromTableId, event.aggregateId, event.playerId) 23 | tableCommandSender.send(removePlayerTableCommand) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/home/GameTabs/GameTab.tsx: -------------------------------------------------------------------------------- 1 | import { NavDropdown, Dropdown, DropdownDivider } from 'react-bootstrap' 2 | import { LinkContainer } from 'react-router-bootstrap' 3 | 4 | const DropdownLink = ({ link, text }) => 5 | 6 | {text} 7 | 8 | 9 | const MyTableItem = ({ openGameTab }) => 10 | openGameTab.myTableId 11 | ? 12 | : null 13 | 14 | const GamePageItem = ({ openGameTab }) => 15 | 16 | 17 | const OtherTableItems = ({ openGameTab }) => { 18 | return ( 19 | <> 20 | {openGameTab.viewingTables.length > 0 ? : null} 21 | {openGameTab.viewingTables.map(viewingTable => 22 | )} 23 | 24 | )} 25 | 26 | export default ({openGameTab}) => { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/processmanagers/AutoMoveHandForwardProcessManager.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.processmanagers 2 | 3 | import com.flexpoker.framework.command.CommandSender 4 | import com.flexpoker.framework.processmanager.ProcessManager 5 | import com.flexpoker.table.command.commands.AutoMoveHandForwardCommand 6 | import com.flexpoker.table.command.commands.TableCommand 7 | import com.flexpoker.table.command.events.AutoMoveHandForwardEvent 8 | import org.springframework.scheduling.annotation.Async 9 | import org.springframework.stereotype.Component 10 | import java.util.concurrent.ScheduledThreadPoolExecutor 11 | import java.util.concurrent.TimeUnit 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class AutoMoveHandForwardProcessManager @Inject constructor( 16 | private val tableCommandSender: CommandSender 17 | ) : ProcessManager { 18 | 19 | private val scheduledThreadPoolExecutor: ScheduledThreadPoolExecutor = ScheduledThreadPoolExecutor(16) 20 | 21 | @Async 22 | override fun handle(event: AutoMoveHandForwardEvent) { 23 | scheduledThreadPoolExecutor.schedule({ 24 | val command = AutoMoveHandForwardCommand(event.aggregateId, event.gameId) 25 | tableCommandSender.send(command) 26 | }, 2, TimeUnit.SECONDS) 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/query/repository/impl/InMemoryOpenGameForPlayerRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository.impl 2 | 3 | import com.flexpoker.game.query.repository.OpenGameForPlayerRepository 4 | import com.flexpoker.test.util.InMemoryTestClass 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.beans.factory.annotation.Autowired 7 | 8 | @InMemoryTestClass 9 | class InMemoryOpenGameForPlayerRepositoryTest { 10 | 11 | @Autowired 12 | private lateinit var repository: OpenGameForPlayerRepository 13 | 14 | @Test 15 | fun testFetchAllOpenGamesForPlayerNoGames() { 16 | sharedTestFetchAllOpenGamesForPlayerNoGames(repository) 17 | } 18 | 19 | @Test 20 | fun testFetchAllOpenGamesOrdered() { 21 | sharedTestFetchAllOpenGamesOrdered(repository) 22 | } 23 | 24 | @Test 25 | fun testDeleteOpenGameForPlayer() { 26 | sharedTestDeleteOpenGameForPlayer(repository) 27 | } 28 | 29 | @Test 30 | fun testAddOpenGameForUser() { 31 | sharedTestAddOpenGameForUser(repository) 32 | } 33 | 34 | @Test 35 | fun testChangeGameStage() { 36 | sharedTestChangeGameStage(repository) 37 | } 38 | 39 | @Test 40 | fun testAssignTableToOpenGame() { 41 | sharedTestAssignTableToOpenGame(repository) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/pushnotificationhandlers/OpenTableForUserPushNotificationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.pushnotificationhandlers 2 | 3 | import com.flexpoker.framework.pushnotifier.PushNotificationHandler 4 | import com.flexpoker.login.repository.LoginRepository 5 | import com.flexpoker.pushnotifications.OpenTableForUserPushNotification 6 | import com.flexpoker.table.query.dto.OpenTableForUserDTO 7 | import com.flexpoker.util.MessagingConstants 8 | import org.springframework.messaging.simp.SimpMessageSendingOperations 9 | import org.springframework.scheduling.annotation.Async 10 | import org.springframework.stereotype.Component 11 | import javax.inject.Inject 12 | 13 | @Component 14 | class OpenTableForUserPushNotificationHandler @Inject constructor( 15 | private val loginRepository: LoginRepository, 16 | private val messagingTemplate: SimpMessageSendingOperations 17 | ) : PushNotificationHandler { 18 | 19 | @Async 20 | override fun handle(pushNotification: OpenTableForUserPushNotification) { 21 | val username = loginRepository.fetchUsernameByAggregateId(pushNotification.playerId) 22 | val dto = OpenTableForUserDTO(pushNotification.gameId, pushNotification.tableId) 23 | messagingTemplate.convertAndSendToUser(username, MessagingConstants.OPEN_TABLE_FOR_USER, dto) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/command/handlers/CreateGameCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.game.command.aggregate.eventproducers.createGame 6 | import com.flexpoker.game.command.commands.CreateGameCommand 7 | import com.flexpoker.game.command.events.GameEvent 8 | import com.flexpoker.game.command.repository.GameEventRepository 9 | import org.springframework.scheduling.annotation.Async 10 | import org.springframework.stereotype.Component 11 | import javax.inject.Inject 12 | 13 | @Component 14 | class CreateGameCommandHandler @Inject constructor( 15 | private val eventPublisher: EventPublisher, 16 | private val gameEventRepository: GameEventRepository 17 | ) : CommandHandler { 18 | 19 | @Async 20 | override fun handle(command: CreateGameCommand) { 21 | val newEvents = createGame(command.gameName, command.numberOfPlayers, command.numberOfPlayersPerTable, 22 | command.createdByPlayerId, command.numberOfMinutesBetweenBlindLevels, command.numberOfSecondsForActionOnTimer) 23 | val eventsWithVersions = gameEventRepository.setEventVersionsAndSave(0, newEvents) 24 | eventsWithVersions.forEach { eventPublisher.publish(it) } 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/archtest/ControllerArchTests.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.archtest; 2 | 3 | import com.flexpoker.test.util.ArchUnitTestClass 4 | import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes 5 | import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses 6 | import com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_FIELD_INJECTION 7 | import org.junit.jupiter.api.Test 8 | import org.springframework.stereotype.Controller 9 | 10 | @ArchUnitTestClass 11 | class ControllerArchTests { 12 | 13 | private val controllerClasses = nonInterfaceTopLevelClasses(".*Controller") 14 | 15 | @Test 16 | fun testAnnotations() { 17 | val classesHaveControllerAnnotation = classes().that(controllerClasses) 18 | .should().beAnnotatedWith(Controller::class.java) 19 | classesHaveControllerAnnotation.check(classesUnderTest) 20 | } 21 | 22 | @Test 23 | fun testDependencies() { 24 | val dependencyRule = noClasses().that().resideInAPackage("com.flexpoker..") 25 | .should().dependOnClassesThat().resideInAPackage("com.flexpoker.web.controller") 26 | dependencyRule.check(classesUnderTest) 27 | } 28 | 29 | @Test 30 | fun testNoFieldInjection() { 31 | NO_CLASSES_SHOULD_USE_FIELD_INJECTION.check(classesUnderTest.that(controllerClasses)) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/aggregate/eventproducers/CreateTableEventProducer.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.eventproducers 2 | 3 | import com.flexpoker.table.command.aggregate.RandomNumberGenerator 4 | import com.flexpoker.table.command.events.TableCreatedEvent 5 | import com.flexpoker.table.command.events.TableEvent 6 | import com.flexpoker.util.toPMap 7 | import java.util.UUID 8 | 9 | fun createTable(tableId: UUID, gameId: UUID, numberOfPlayersPerTable: Int, 10 | playerIds: Set, randomNumberGenerator: RandomNumberGenerator): List { 11 | require(playerIds.size >= 2) { "must have at least two players" } 12 | require(playerIds.size <= numberOfPlayersPerTable) { "player list can't be larger than the number of players per table" } 13 | 14 | val seatMap = mutableMapOf() 15 | (0 until numberOfPlayersPerTable).forEach { seatMap[it] = null } 16 | playerIds.forEach { 17 | do { 18 | val attemptedSeatPosition = randomNumberGenerator.int(numberOfPlayersPerTable) 19 | if (seatMap[attemptedSeatPosition] == null) { 20 | seatMap[attemptedSeatPosition] = it 21 | } 22 | } while (seatMap[attemptedSeatPosition] != it) 23 | } 24 | 25 | return listOf(TableCreatedEvent(tableId, gameId, numberOfPlayersPerTable, seatMap.toPMap(), 1500)) 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/command/handlers/JoinGameCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.game.command.aggregate.applyEvents 6 | import com.flexpoker.game.command.aggregate.eventproducers.joinGame 7 | import com.flexpoker.game.command.commands.JoinGameCommand 8 | import com.flexpoker.game.command.events.GameEvent 9 | import com.flexpoker.game.command.repository.GameEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class JoinGameCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val gameEventRepository: GameEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: JoinGameCommand) { 22 | val gameEvents = gameEventRepository.fetchAll(command.aggregateId) 23 | val state = applyEvents(gameEvents) 24 | val newEvents = joinGame(state, command.playerId) 25 | val eventsWithVersions = gameEventRepository.setEventVersionsAndSave(gameEvents.size, newEvents) 26 | eventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/PauseCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.table.command.aggregate.applyEvents 6 | import com.flexpoker.table.command.aggregate.eventproducers.pause 7 | import com.flexpoker.table.command.commands.PauseCommand 8 | import com.flexpoker.table.command.events.TableEvent 9 | import com.flexpoker.table.command.repository.TableEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class PauseCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val tableEventRepository: TableEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: PauseCommand) { 22 | val existingEvents = tableEventRepository.fetchAll(command.tableId) 23 | val state = applyEvents(existingEvents) 24 | val newEvents = pause(state) 25 | val newlySavedEventsWithVersions = tableEventRepository.setEventVersionsAndSave(existingEvents.size, newEvents) 26 | newlySavedEventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/command/repository/RedisTableEventRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.repository 2 | 3 | import com.flexpoker.test.util.RedisTestClass 4 | import com.flexpoker.test.util.redisContainer 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection 8 | import org.testcontainers.junit.jupiter.Container 9 | 10 | @RedisTestClass 11 | class RedisTableEventRepositoryTest { 12 | 13 | @Autowired 14 | private lateinit var repository: TableEventRepository 15 | 16 | companion object { 17 | @Container 18 | @ServiceConnection 19 | val redisContainer = redisContainer() 20 | } 21 | 22 | @Test 23 | fun testFetchAll() { 24 | sharedTestFetchAll(repository) 25 | } 26 | 27 | @Test 28 | fun testSetEventVersionsAndSaveBadBasedOnVersion1() { 29 | sharedTestSetEventVersionsAndSaveBadBasedOnVersion1(repository) 30 | } 31 | 32 | @Test 33 | fun testSetEventVersionsAndSaveTwoJoinsInOneRequest() { 34 | sharedTestSetEventVersionsAndSaveTwoFakeEventsInOneRequest(repository) 35 | } 36 | 37 | @Test 38 | fun testSetEventVersionsAndSaveEmptyList() { 39 | sharedTestSetEventVersionsAndSaveEmptyList(repository) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/CallCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.table.command.aggregate.applyEvents 6 | import com.flexpoker.table.command.aggregate.eventproducers.call 7 | import com.flexpoker.table.command.commands.CallCommand 8 | import com.flexpoker.table.command.events.TableEvent 9 | import com.flexpoker.table.command.repository.TableEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class CallCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val tableEventRepository: TableEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: CallCommand) { 22 | val existingEvents = tableEventRepository.fetchAll(command.tableId) 23 | val state = applyEvents(existingEvents) 24 | val newEvents = call(state, command.playerId) 25 | val newlySavedEventsWithVersions = tableEventRepository.setEventVersionsAndSave(existingEvents.size, newEvents) 26 | newlySavedEventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/FoldCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.table.command.aggregate.applyEvents 6 | import com.flexpoker.table.command.aggregate.eventproducers.fold 7 | import com.flexpoker.table.command.commands.FoldCommand 8 | import com.flexpoker.table.command.events.TableEvent 9 | import com.flexpoker.table.command.repository.TableEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class FoldCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val tableEventRepository: TableEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: FoldCommand) { 22 | val existingEvents = tableEventRepository.fetchAll(command.tableId) 23 | val state = applyEvents(existingEvents) 24 | val newEvents = fold(state, command.playerId) 25 | val newlySavedEventsWithVersions = tableEventRepository.setEventVersionsAndSave(existingEvents.size, newEvents) 26 | newlySavedEventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/ResumeCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.table.command.aggregate.applyEvents 6 | import com.flexpoker.table.command.aggregate.eventproducers.resume 7 | import com.flexpoker.table.command.commands.ResumeCommand 8 | import com.flexpoker.table.command.events.TableEvent 9 | import com.flexpoker.table.command.repository.TableEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class ResumeCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val tableEventRepository: TableEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: ResumeCommand) { 22 | val existingEvents = tableEventRepository.fetchAll(command.tableId) 23 | val state = applyEvents(existingEvents) 24 | val newEvents = resume(state) 25 | val newlySavedEventsWithVersions = tableEventRepository.setEventVersionsAndSave(existingEvents.size, newEvents) 26 | newlySavedEventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/query/repository/impl/SharedGamePlayerRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository.impl 2 | 3 | import com.flexpoker.game.query.repository.GamePlayerRepository 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Assertions.assertTrue 6 | import java.util.UUID 7 | 8 | fun sharedTestAddPlayerToGame(repository: GamePlayerRepository) { 9 | val playerId = UUID.randomUUID() 10 | val gameId = UUID.randomUUID() 11 | repository.addPlayerToGame(playerId, gameId) 12 | 13 | val playerIds = repository.fetchAllPlayerIdsForGame(gameId) 14 | assertEquals(1, playerIds.size) 15 | assertTrue(playerIds.contains(playerId)) 16 | } 17 | 18 | fun sharedTestAddPlayerToGameDuplicate(repository: GamePlayerRepository) { 19 | val playerId = UUID.randomUUID() 20 | val gameId = UUID.randomUUID() 21 | repository.addPlayerToGame(playerId, gameId) 22 | repository.addPlayerToGame(playerId, gameId) 23 | 24 | val playerIds = repository.fetchAllPlayerIdsForGame(gameId) 25 | assertEquals(1, playerIds.size) 26 | assertTrue(playerIds.contains(playerId)) 27 | } 28 | 29 | fun sharedTestFetchAllPlayerIdsForGameNoneAdded(repository: GamePlayerRepository) { 30 | val gameId = UUID.randomUUID() 31 | val playerIds = repository.fetchAllPlayerIdsForGame(gameId) 32 | assertEquals(0, playerIds.size) 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/CheckCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.table.command.aggregate.applyEvents 6 | import com.flexpoker.table.command.aggregate.eventproducers.check 7 | import com.flexpoker.table.command.commands.CheckCommand 8 | import com.flexpoker.table.command.events.TableEvent 9 | import com.flexpoker.table.command.repository.TableEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class CheckCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val tableEventRepository: TableEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: CheckCommand) { 22 | val existingEvents = tableEventRepository.fetchAll(command.tableId) 23 | val state = applyEvents(existingEvents) 24 | val newEvents = check(state, command.playerId) 25 | val newlySavedEventsWithVersions = tableEventRepository.setEventVersionsAndSave(existingEvents.size, newEvents) 26 | newlySavedEventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/command/handlers/IncrementBlindsCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.game.command.aggregate.applyEvents 6 | import com.flexpoker.game.command.aggregate.eventproducers.increaseBlinds 7 | import com.flexpoker.game.command.commands.IncrementBlindsCommand 8 | import com.flexpoker.game.command.events.GameEvent 9 | import com.flexpoker.game.command.repository.GameEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class IncrementBlindsCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val gameEventRepository: GameEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: IncrementBlindsCommand) { 22 | val gameEvents = gameEventRepository.fetchAll(command.aggregateId) 23 | val state = applyEvents(gameEvents) 24 | val newEvents = increaseBlinds(state) 25 | val eventsWithVersions = gameEventRepository.setEventVersionsAndSave(gameEvents.size, newEvents) 26 | eventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/CreateTableCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.table.command.aggregate.DefaultRandomNumberGenerator 6 | import com.flexpoker.table.command.aggregate.eventproducers.createTable 7 | import com.flexpoker.table.command.commands.CreateTableCommand 8 | import com.flexpoker.table.command.events.TableEvent 9 | import com.flexpoker.table.command.repository.TableEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class CreateTableCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val tableEventRepository: TableEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: CreateTableCommand) { 22 | val newEvents = createTable(command.tableId, command.gameId, command.numberOfPlayersPerTable, 23 | command.playerIds, DefaultRandomNumberGenerator()) 24 | val newlySavedEventsWithVersions = tableEventRepository.setEventVersionsAndSave(0, newEvents) 25 | newlySavedEventsWithVersions.forEach { eventPublisher.publish(it) } 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/lobby/CreateGameDialog/index.tsx: -------------------------------------------------------------------------------- 1 | import WebSocketService from '../../webSocket/WebSocketService' 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import { hideCreateGameModal } from '../../../reducers' 4 | import CreateGameDialog from './CreateGameDialog' 5 | import { RootState } from '../../..' 6 | 7 | const createGameFormSubmitted = (hideDialogCallback, evt) => { 8 | evt.preventDefault() 9 | 10 | WebSocketService.send('/app/creategame', { 11 | name: evt.target.elements.name.value, 12 | players: evt.target.elements.players.value, 13 | playersPerTable: evt.target.elements.playersPerTable.value, 14 | numberOfMinutesBetweenBlindLevels: evt.target.elements.numberOfMinutesBetweenBlindLevels.value, 15 | numberOfSecondsForActionOnTimer: evt.target.elements.secondsForActionOnTimer.value 16 | }) 17 | 18 | hideDialogCallback() 19 | } 20 | 21 | export default () => { 22 | const dispatch = useDispatch() 23 | const dispatchHideCreateGameModal = () => dispatch(hideCreateGameModal()) 24 | 25 | const showModal = useSelector((state: RootState) => state.showCreateGameModal) 26 | 27 | return ( 28 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/login/repository/RedisLoginRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.login.repository 2 | 3 | import com.flexpoker.test.util.RedisTestClass 4 | import com.flexpoker.test.util.redisContainer 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection 8 | import org.testcontainers.junit.jupiter.Container 9 | 10 | @RedisTestClass 11 | class RedisLoginRepositoryTest { 12 | 13 | @Autowired 14 | private lateinit var repository: LoginRepository 15 | 16 | companion object { 17 | @Container 18 | @ServiceConnection 19 | val redisContainer = redisContainer() 20 | } 21 | 22 | @Test 23 | fun testLoadUserByUsername() { 24 | sharedTestLoadUserByUsername(repository) 25 | } 26 | 27 | @Test 28 | fun testFetchAggregateIdByUsername() { 29 | sharedTestFetchAggregateIdByUsername(repository) 30 | } 31 | 32 | @Test 33 | fun testFetchUsernameByAggregateId() { 34 | sharedTestFetchUsernameByAggregateId(repository) 35 | } 36 | 37 | @Test 38 | fun testSaveAggregateIdAndUsername() { 39 | sharedTestSaveAggregateIdAndUsername(repository) 40 | } 41 | 42 | @Test 43 | fun testSaveUsernameAndPassword() { 44 | sharedTestSaveUsernameAndPassword(repository) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/query/repository/impl/RedisGamePlayerRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository.impl 2 | 3 | import com.flexpoker.config.ProfileNames 4 | import com.flexpoker.game.query.repository.GamePlayerRepository 5 | import org.springframework.context.annotation.Profile 6 | import org.springframework.data.redis.core.RedisTemplate 7 | import org.springframework.stereotype.Repository 8 | import java.util.UUID 9 | import java.util.stream.Collectors 10 | import javax.inject.Inject 11 | 12 | @Profile(ProfileNames.REDIS, ProfileNames.GAME_QUERY_REDIS) 13 | @Repository 14 | class RedisGamePlayerRepository @Inject constructor( 15 | private val redisTemplate: RedisTemplate, 16 | ) : GamePlayerRepository { 17 | 18 | companion object { 19 | private const val GAME_PLAYER_NAMESPACE = "game-player" 20 | } 21 | 22 | private fun redisKey(gameId: UUID) = "$GAME_PLAYER_NAMESPACE:$gameId" 23 | 24 | override fun addPlayerToGame(playerId: UUID, gameId: UUID) { 25 | redisTemplate.opsForSet().add(redisKey(gameId), playerId.toString()) 26 | } 27 | 28 | override fun fetchAllPlayerIdsForGame(gameId: UUID): Set { 29 | return redisTemplate 30 | .opsForSet() 31 | .members(redisKey(gameId))!! 32 | .stream() 33 | .map { UUID.fromString(it) } 34 | .collect(Collectors.toSet()) 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/RaiseCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.table.command.aggregate.applyEvents 6 | import com.flexpoker.table.command.aggregate.eventproducers.raise 7 | import com.flexpoker.table.command.commands.RaiseCommand 8 | import com.flexpoker.table.command.events.TableEvent 9 | import com.flexpoker.table.command.repository.TableEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class RaiseCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val tableEventRepository: TableEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: RaiseCommand) { 22 | val existingEvents = tableEventRepository.fetchAll(command.tableId) 23 | val state = applyEvents(existingEvents) 24 | val newEvents = raise(state, command.playerId, command.raiseToAmount) 25 | val newlySavedEventsWithVersions = tableEventRepository.setEventVersionsAndSave(existingEvents.size, newEvents) 26 | newlySavedEventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/test/util/CommonAssertions.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.test.util 2 | 3 | import com.flexpoker.framework.event.Event 4 | import com.flexpoker.table.command.aggregate.TableState 5 | import com.flexpoker.table.command.events.TableEvent 6 | import org.junit.jupiter.api.Assertions.assertArrayEquals 7 | import org.junit.jupiter.api.Assertions.assertEquals 8 | import java.util.UUID 9 | 10 | object CommonAssertions { 11 | 12 | fun verifyEventIds(tableId: UUID, events: List) { 13 | for (event in events) { 14 | assertEquals(tableId, event.aggregateId) 15 | } 16 | } 17 | 18 | fun verifyNumberOfEventsAndEntireOrderByType(events: List, vararg eventClasses: Class) { 19 | assertEquals(eventClasses.size, events.size) 20 | assertArrayEquals(eventClasses, events.map { it.javaClass }.toTypedArray()) 21 | } 22 | 23 | fun verifyAppliedAndNewEventsForAggregate(state: TableState, events: List, vararg eventClasses: Class) { 24 | verifyEventIds(state.aggregateId, events) 25 | verifyNumberOfEventsAndEntireOrderByType(events, *eventClasses) 26 | } 27 | 28 | fun verifyNewEvents(aggregateId: UUID, events: List, vararg eventClasses: Class) { 29 | verifyEventIds(aggregateId, events) 30 | verifyNumberOfEventsAndEntireOrderByType(events, *eventClasses) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/query/repository/impl/InMemoryGameListRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository.impl 2 | 3 | import com.flexpoker.game.query.repository.GameListRepository 4 | import com.flexpoker.test.util.InMemoryTestClass 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.test.annotation.DirtiesContext 8 | 9 | @InMemoryTestClass 10 | class InMemoryGameListRepositoryTest { 11 | 12 | @Autowired 13 | private lateinit var repository: GameListRepository 14 | 15 | @Test 16 | @DirtiesContext 17 | fun testSaveNew() { 18 | sharedTestSaveNew(repository) 19 | } 20 | 21 | @Test 22 | @DirtiesContext 23 | fun testFetchAllEmpty() { 24 | sharedTestFetchAllEmpty(repository) 25 | } 26 | 27 | @Test 28 | @DirtiesContext 29 | fun testFetchAllTwoSaved() { 30 | sharedTestFetchAllTwoSaved(repository) 31 | } 32 | 33 | @Test 34 | @DirtiesContext 35 | fun testIncrementRegisteredPlayers() { 36 | sharedTestIncrementRegisteredPlayers(repository) 37 | } 38 | 39 | @Test 40 | @DirtiesContext 41 | fun testFetchGameName() { 42 | sharedTestFetchGameName(repository) 43 | } 44 | 45 | @Test 46 | @DirtiesContext 47 | fun testChangeGameStage() { 48 | sharedTestChangeGameStage(repository) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/chat/repository/RedisChatRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.chat.repository 2 | 3 | import com.flexpoker.test.util.RedisTestClass 4 | import com.flexpoker.test.util.redisContainer 5 | import org.junit.jupiter.api.BeforeEach 6 | import org.junit.jupiter.api.Test 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection 9 | import org.springframework.data.redis.connection.RedisConnectionFactory 10 | import org.testcontainers.junit.jupiter.Container 11 | 12 | @RedisTestClass 13 | class RedisChatRepositoryTest { 14 | 15 | @Autowired 16 | private lateinit var repository: ChatRepository 17 | 18 | @Autowired 19 | private lateinit var connectionFactory: RedisConnectionFactory 20 | 21 | companion object { 22 | @Container 23 | @ServiceConnection 24 | val redisContainer = redisContainer() 25 | } 26 | 27 | @BeforeEach 28 | fun beforeEach() { 29 | connectionFactory.connection.serverCommands().flushAll() 30 | } 31 | 32 | @Test 33 | fun testSaveChatMessage() { 34 | sharedTestSaveChatMessage(repository) 35 | } 36 | 37 | @Test 38 | fun testFetchAllTypesEmpty() { 39 | sharedTestFetchAllTypesEmpty(repository) 40 | } 41 | 42 | @Test 43 | fun testFetchAllTypes() { 44 | sharedTestFetchAllTypes(repository) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/query/handlers/BlindsIncreasedEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.handlers 2 | 3 | import com.flexpoker.chat.service.ChatService 4 | import com.flexpoker.framework.event.EventHandler 5 | import com.flexpoker.game.command.aggregate.applyEvents 6 | import com.flexpoker.game.command.events.BlindsIncreasedEvent 7 | import com.flexpoker.game.command.repository.GameEventRepository 8 | import org.springframework.stereotype.Component 9 | import javax.inject.Inject 10 | 11 | @Component 12 | class BlindsIncreasedEventHandler @Inject constructor( 13 | private val chatService : ChatService, 14 | private val gameEventRepository: GameEventRepository 15 | ) : EventHandler { 16 | 17 | override fun handle(event: BlindsIncreasedEvent) { 18 | handleChat(event) 19 | } 20 | 21 | private fun handleChat(event: BlindsIncreasedEvent) { 22 | val gameEvents = gameEventRepository.fetchAll(event.gameId) 23 | val state = applyEvents(gameEvents) 24 | val tableIds = state.tableIdToPlayerIdsMap.keys 25 | val blinds = state.blindSchedule.currentBlinds() 26 | 27 | val message = "Blinds will increase next hand to ${blinds.smallBlind} / ${blinds.bigBlind}" 28 | chatService.saveAndPushSystemGameChatMessage(event.gameId, message) 29 | tableIds.forEach { chatService.saveAndPushSystemTableChatMessage(event.gameId, it, message) } 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/RemovePlayerCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.table.command.aggregate.applyEvents 6 | import com.flexpoker.table.command.aggregate.eventproducers.removePlayer 7 | import com.flexpoker.table.command.commands.RemovePlayerCommand 8 | import com.flexpoker.table.command.events.TableEvent 9 | import com.flexpoker.table.command.repository.TableEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class RemovePlayerCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val tableEventRepository: TableEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: RemovePlayerCommand) { 22 | val existingEvents = tableEventRepository.fetchAll(command.tableId) 23 | val state = applyEvents(existingEvents) 24 | val newEvents = removePlayer(state, command.playerId) 25 | val newlySavedEventsWithVersions = tableEventRepository.setEventVersionsAndSave(existingEvents.size, newEvents) 26 | newlySavedEventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/repository/impl/RedisTableRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.repository.impl 2 | 3 | import com.flexpoker.config.ProfileNames 4 | import com.flexpoker.table.query.dto.TableDTO 5 | import com.flexpoker.table.query.repository.TableRepository 6 | import org.springframework.context.annotation.Profile 7 | import org.springframework.data.redis.core.RedisTemplate 8 | import org.springframework.data.redis.core.script.RedisScript 9 | import org.springframework.stereotype.Repository 10 | import java.util.UUID 11 | import javax.inject.Inject 12 | 13 | 14 | @Profile(ProfileNames.REDIS, ProfileNames.TABLE_QUERY_REDIS) 15 | @Repository 16 | class RedisTableRepository @Inject constructor( 17 | private val redisTemplateTableDTO: RedisTemplate, 18 | private val checkAndSetScript: RedisScript, 19 | ) : TableRepository { 20 | 21 | companion object { 22 | private const val TABLE_DTO_NAMESPACE = "table-dto" 23 | } 24 | 25 | private fun redisKey(tableId: UUID) = "$TABLE_DTO_NAMESPACE:$tableId" 26 | 27 | override fun fetchById(tableId: UUID): TableDTO { 28 | return redisTemplateTableDTO.opsForValue()[redisKey(tableId)]!! 29 | } 30 | 31 | override fun save(tableDTO: TableDTO) { 32 | redisTemplateTableDTO.execute( 33 | checkAndSetScript, 34 | listOf(redisKey(tableDTO.id)), 35 | tableDTO, 36 | ) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/signup/repository/InMemorySignUpRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.signup.repository 2 | 3 | import com.flexpoker.test.util.InMemoryTestClass 4 | import org.junit.jupiter.api.Test 5 | import org.springframework.beans.factory.annotation.Autowired 6 | 7 | @InMemoryTestClass 8 | class InMemorySignUpRepositoryTest { 9 | 10 | @Autowired 11 | private lateinit var repository: SignUpRepository 12 | 13 | @Test 14 | fun testUsernameExists() { 15 | sharedTestUsernameExists(repository) 16 | } 17 | 18 | @Test 19 | fun testFetchSignUpUser() { 20 | sharedTestFetchSignUpUser(repository) 21 | } 22 | 23 | @Test 24 | fun testSaveSignUpUser() { 25 | sharedTestSaveSignUpUser(repository) 26 | } 27 | 28 | @Test 29 | fun testFindSignUpCodeByUsername() { 30 | sharedTestFindSignUpCodeByUsername(repository) 31 | } 32 | 33 | @Test 34 | fun testFindAggregateIdByUsernameAndSignUpCode() { 35 | sharedTestFindAggregateIdByUsernameAndSignUpCode(repository) 36 | } 37 | 38 | @Test 39 | fun testStoreNewlyConfirmedUsername() { 40 | sharedTestStoreNewlyConfirmedUsername(repository) 41 | } 42 | 43 | @Test 44 | fun testSignUpCodeExists() { 45 | sharedTestSignUpCodeExists(repository) 46 | } 47 | 48 | @Test 49 | fun testStoreSignUpInformation() { 50 | sharedTestStoreSignUpInformation(repository) 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/login/repository/SharedLoginRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.login.repository 2 | 3 | import com.flexpoker.util.encodePassword 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Assertions.assertNotNull 6 | import java.util.UUID 7 | 8 | fun sharedTestLoadUserByUsername(repository: LoginRepository) { 9 | val user = repository.loadUserByUsername("player1") 10 | assertEquals("player1", user.username) 11 | } 12 | 13 | fun sharedTestFetchAggregateIdByUsername(repository: LoginRepository) { 14 | val userId = repository.fetchAggregateIdByUsername("player1") 15 | assertNotNull(userId) 16 | } 17 | 18 | fun sharedTestFetchUsernameByAggregateId(repository: LoginRepository) { 19 | val userId = repository.fetchAggregateIdByUsername("player1") 20 | val username = repository.fetchUsernameByAggregateId(userId) 21 | assertEquals("player1", username) 22 | } 23 | 24 | fun sharedTestSaveAggregateIdAndUsername(repository: LoginRepository) { 25 | val userId = UUID.randomUUID() 26 | repository.saveAggregateIdAndUsername(userId, "player5") 27 | val savedUserId = repository.fetchAggregateIdByUsername("player5") 28 | assertEquals(userId, savedUserId) 29 | } 30 | 31 | fun sharedTestSaveUsernameAndPassword(repository: LoginRepository) { 32 | repository.saveUsernameAndPassword("player5", encodePassword("password5")) 33 | assertNotNull(repository.loadUserByUsername("player5")) 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/handlers/WinnersDeterminedEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.handlers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 5 | import com.flexpoker.pushnotifications.TableUpdatedPushNotification 6 | import com.flexpoker.table.command.events.WinnersDeterminedEvent 7 | import com.flexpoker.table.query.repository.TableRepository 8 | import org.springframework.stereotype.Component 9 | import javax.inject.Inject 10 | 11 | @Component 12 | class WinnersDeterminedEventHandler @Inject constructor( 13 | private val tableRepository: TableRepository, 14 | private val pushNotificationPublisher: PushNotificationPublisher 15 | ) : EventHandler { 16 | 17 | override fun handle(event: WinnersDeterminedEvent) { 18 | handleUpdatingTable(event) 19 | handlePushNotifications(event) 20 | } 21 | 22 | private fun handleUpdatingTable(event: WinnersDeterminedEvent) { 23 | val tableDTO = tableRepository.fetchById(event.aggregateId) 24 | val updatedTable = tableDTO.copy(version = event.version) 25 | tableRepository.save(updatedTable) 26 | } 27 | 28 | private fun handlePushNotifications(event: WinnersDeterminedEvent) { 29 | val pushNotification = TableUpdatedPushNotification(event.gameId, event.aggregateId) 30 | pushNotificationPublisher.publish(pushNotification) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/AutoMoveHandForwardCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.table.command.aggregate.applyEvents 6 | import com.flexpoker.table.command.aggregate.eventproducers.autoMoveHandForward 7 | import com.flexpoker.table.command.commands.AutoMoveHandForwardCommand 8 | import com.flexpoker.table.command.events.TableEvent 9 | import com.flexpoker.table.command.repository.TableEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class AutoMoveHandForwardCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val tableEventRepository: TableEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: AutoMoveHandForwardCommand) { 22 | val existingEvents = tableEventRepository.fetchAll(command.tableId) 23 | val state = applyEvents(existingEvents) 24 | val newEvents = autoMoveHandForward(state) 25 | val newlySavedEventsWithVersions = tableEventRepository.setEventVersionsAndSave(existingEvents.size, newEvents) 26 | newlySavedEventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/pushnotificationhandlers/SendUserPocketCardsPushNotificationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.pushnotificationhandlers 2 | 3 | import com.flexpoker.framework.pushnotifier.PushNotificationHandler 4 | import com.flexpoker.login.repository.LoginRepository 5 | import com.flexpoker.pushnotifications.SendUserPocketCardsPushNotification 6 | import com.flexpoker.table.query.dto.PocketCardsDTO 7 | import com.flexpoker.util.MessagingConstants 8 | import org.springframework.messaging.simp.SimpMessageSendingOperations 9 | import org.springframework.scheduling.annotation.Async 10 | import org.springframework.stereotype.Component 11 | import javax.inject.Inject 12 | 13 | @Component 14 | class SendUserPocketCardsPushNotificationHandler @Inject constructor( 15 | private val loginRepository: LoginRepository, 16 | private val messagingTemplate: SimpMessageSendingOperations 17 | ) : PushNotificationHandler { 18 | 19 | @Async 20 | override fun handle(pushNotification: SendUserPocketCardsPushNotification) { 21 | val username = loginRepository.fetchUsernameByAggregateId(pushNotification.playerId) 22 | val pocketCardsDTO = PocketCardsDTO( 23 | pushNotification.handId, 24 | pushNotification.pocketCards.card1.id, 25 | pushNotification.pocketCards.card2.id 26 | ) 27 | messagingTemplate.convertAndSendToUser(username, MessagingConstants.POCKET_CARDS, pocketCardsDTO) 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/command/handlers/AttemptToStartNewHandCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.game.command.aggregate.applyEvents 6 | import com.flexpoker.game.command.aggregate.eventproducers.attemptToStartNewHand 7 | import com.flexpoker.game.command.commands.AttemptToStartNewHandCommand 8 | import com.flexpoker.game.command.events.GameEvent 9 | import com.flexpoker.game.command.repository.GameEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class AttemptToStartNewHandCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val gameEventRepository: GameEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: AttemptToStartNewHandCommand) { 22 | val gameEvents = gameEventRepository.fetchAll(command.aggregateId) 23 | val state = applyEvents(gameEvents) 24 | val newEvents = attemptToStartNewHand(state, command.tableId, command.playerToChipsAtTableMap) 25 | val eventsWithVersions = gameEventRepository.setEventVersionsAndSave(gameEvents.size, newEvents) 26 | eventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/ExpireActionOnTimerCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.table.command.aggregate.applyEvents 6 | import com.flexpoker.table.command.aggregate.eventproducers.expireActionOn 7 | import com.flexpoker.table.command.commands.ExpireActionOnTimerCommand 8 | import com.flexpoker.table.command.events.TableEvent 9 | import com.flexpoker.table.command.repository.TableEventRepository 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class ExpireActionOnTimerCommandHandler @Inject constructor( 16 | private val eventPublisher: EventPublisher, 17 | private val tableEventRepository: TableEventRepository 18 | ) : CommandHandler { 19 | 20 | @Async 21 | override fun handle(command: ExpireActionOnTimerCommand) { 22 | val existingEvents = tableEventRepository.fetchAll(command.tableId) 23 | val state = applyEvents(existingEvents) 24 | val newEvents = expireActionOn(state, command.handId, command.playerId) 25 | val newlySavedEventsWithVersions = tableEventRepository.setEventVersionsAndSave(existingEvents.size, newEvents) 26 | newlySavedEventsWithVersions.forEach { eventPublisher.publish(it) } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/pushnotificationhandlers/OpenGamesForPlayerUpdatedPushNotificationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.pushnotificationhandlers 2 | 3 | import com.flexpoker.framework.pushnotifier.PushNotificationHandler 4 | import com.flexpoker.game.query.repository.OpenGameForPlayerRepository 5 | import com.flexpoker.login.repository.LoginRepository 6 | import com.flexpoker.pushnotifications.OpenGamesForPlayerUpdatedPushNotification 7 | import com.flexpoker.util.MessagingConstants 8 | import org.springframework.messaging.simp.SimpMessageSendingOperations 9 | import org.springframework.scheduling.annotation.Async 10 | import org.springframework.stereotype.Component 11 | import javax.inject.Inject 12 | 13 | @Component 14 | class OpenGamesForPlayerUpdatedPushNotificationHandler @Inject constructor( 15 | private val loginRepository: LoginRepository, 16 | private val openGameForUserRepository: OpenGameForPlayerRepository, 17 | private val messagingTemplate: SimpMessageSendingOperations 18 | ) : PushNotificationHandler { 19 | 20 | @Async 21 | override fun handle(pushNotification: OpenGamesForPlayerUpdatedPushNotification) { 22 | val username = loginRepository.fetchUsernameByAggregateId(pushNotification.playerId) 23 | val allOpenGames = openGameForUserRepository.fetchAllOpenGamesForPlayer(pushNotification.playerId) 24 | messagingTemplate.convertAndSendToUser(username, MessagingConstants.OPEN_GAMES_FOR_USER, allOpenGames) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/config/SecurityConfig.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.config 2 | 3 | import com.flexpoker.login.repository.LoginRepository 4 | import com.flexpoker.util.passwordEncoder 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity 8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 9 | import org.springframework.security.crypto.password.PasswordEncoder 10 | import org.springframework.security.web.SecurityFilterChain 11 | import javax.inject.Inject 12 | 13 | @Configuration 14 | @EnableWebSecurity 15 | class SecurityConfig @Inject constructor(private val loginRepository: LoginRepository) { 16 | 17 | @Bean 18 | fun passwordEncoder(): PasswordEncoder { 19 | return passwordEncoder 20 | } 21 | 22 | @Bean 23 | fun httpSecurity(http: HttpSecurity): SecurityFilterChain { 24 | http.authorizeHttpRequests { 25 | it.requestMatchers( 26 | "/login", 27 | "/sign-up", 28 | "/sign-up-confirm", 29 | "/confirm-sign-up", 30 | "/userinfo", 31 | ).permitAll() 32 | it.requestMatchers("/resources/**").permitAll() 33 | it.anyRequest().authenticated() 34 | }.formLogin { 35 | it.loginPage("/login").permitAll() 36 | }.csrf { it.disable() } 37 | return http.build() 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/handlers/AddPlayerCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.handlers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.table.command.aggregate.DefaultRandomNumberGenerator 6 | import com.flexpoker.table.command.aggregate.applyEvents 7 | import com.flexpoker.table.command.aggregate.eventproducers.addPlayer 8 | import com.flexpoker.table.command.commands.AddPlayerCommand 9 | import com.flexpoker.table.command.events.TableEvent 10 | import com.flexpoker.table.command.repository.TableEventRepository 11 | import org.springframework.scheduling.annotation.Async 12 | import org.springframework.stereotype.Component 13 | import javax.inject.Inject 14 | 15 | @Component 16 | class AddPlayerCommandHandler @Inject constructor( 17 | private val eventPublisher: EventPublisher, 18 | private val tableEventRepository: TableEventRepository 19 | ) : CommandHandler { 20 | 21 | @Async 22 | override fun handle(command: AddPlayerCommand) { 23 | val existingEvents = tableEventRepository.fetchAll(command.tableId) 24 | val state = applyEvents(existingEvents) 25 | val newEvents = addPlayer(state, command.playerId, command.chips, DefaultRandomNumberGenerator()) 26 | val newlySavedEventsWithVersions = tableEventRepository.setEventVersionsAndSave(existingEvents.size, newEvents) 27 | newlySavedEventsWithVersions.forEach { eventPublisher.publish(it) } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/table/tableStateSubscriber.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import WebSocketSubscriptionManager from '../webSocket/WebSocketSubscriptionManager' 3 | import { tableUpdateReceived, actionOnTickReceived } from '../../reducers' 4 | import { Store } from 'redux' 5 | 6 | export default dispatch => { 7 | 8 | // used just as a placeholder for the websocket sub since there is no component 9 | const constObj = { unique: Math.random() } 10 | 11 | const activeTable = state => state.activeTable 12 | 13 | const activeTableSelector = createSelector([ activeTable ], 14 | activeTable => registerWebSocketSubs(activeTable)) 15 | 16 | const acceptTableData = (gameId, tableId, message) => 17 | dispatch(tableUpdateReceived(gameId, tableId, JSON.parse(message.body))) 18 | 19 | const receiveActionOnTick = (gameId, tableId, message) => 20 | dispatch(actionOnTickReceived(gameId, tableId, JSON.parse(message.body))) 21 | 22 | const registerWebSocketSubs = activeTable => { 23 | const { gameId, tableId } = activeTable || {} 24 | if (gameId && tableId) { 25 | WebSocketSubscriptionManager.subscribe(constObj, [ 26 | { location: `/topic/game/${gameId}/table/${tableId}`, subscription: acceptTableData.bind(null, gameId, tableId) }, 27 | { location: `/topic/game/${gameId}/table/${tableId}/action-on-tick`, subscription: receiveActionOnTick.bind(null, gameId, tableId) } 28 | ]) 29 | } 30 | } 31 | 32 | return (store: Store) => () => { 33 | activeTableSelector(store.getState()) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/query/repository/impl/RedisCardsUsedInHandRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.repository.impl 2 | 3 | import com.flexpoker.table.query.repository.CardsUsedInHandRepository 4 | import com.flexpoker.test.util.RedisTestClass 5 | import com.flexpoker.test.util.redisContainer 6 | import org.junit.jupiter.api.Test 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection 9 | import org.testcontainers.junit.jupiter.Container 10 | 11 | @RedisTestClass 12 | class RedisCardsUsedInHandRepositoryTest { 13 | 14 | @Autowired 15 | private lateinit var repository: CardsUsedInHandRepository 16 | 17 | companion object { 18 | @Container 19 | @ServiceConnection 20 | val redisContainer = redisContainer() 21 | } 22 | 23 | @Test 24 | fun testSaveFlopCards() { 25 | sharedTestSaveFlopCards(repository) 26 | } 27 | 28 | @Test 29 | fun testSaveTurnCard() { 30 | sharedTestSaveTurnCard(repository) 31 | } 32 | 33 | @Test 34 | fun testSaveRiverCard() { 35 | sharedTestSaveRiverCard(repository) 36 | } 37 | 38 | @Test 39 | fun testSavePocketCards() { 40 | sharedTestSavePocketCards(repository) 41 | } 42 | 43 | @Test 44 | fun testFetchAllPocketCardsForUser() { 45 | sharedTestFetchAllPocketCardsForUser(repository) 46 | } 47 | 48 | @Test 49 | fun testRemoveHand() { 50 | sharedTestRemoveHand(repository) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/aggregate/eventproducers/hand/HandlePotAndRoundCompleteEventProducer.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.eventproducers.hand 2 | 3 | import com.flexpoker.table.command.HandDealerState 4 | import com.flexpoker.table.command.aggregate.HandState 5 | import com.flexpoker.table.command.aggregate.calculatePots 6 | import com.flexpoker.table.command.events.RoundCompletedEvent 7 | import com.flexpoker.table.command.events.TableEvent 8 | 9 | fun handlePotAndRoundCompleted(state: HandState): List { 10 | var updatedState = state 11 | if (updatedState.seatMap[updatedState.actionOnPosition] != updatedState.lastToActPlayerId 12 | && updatedState.playersStillInHand.size > 1) { 13 | return emptyList() 14 | } 15 | 16 | val (potEvents, updatedPots) = calculatePots(updatedState.gameId, updatedState.tableId, updatedState.entityId, 17 | updatedState.pots, updatedState.handEvaluationList, updatedState.chipsInFrontMap, updatedState.chipsInBackMap) 18 | 19 | val tableEvents = ArrayList() 20 | tableEvents.addAll(potEvents) 21 | updatedState = updatedState.copy(pots = updatedPots) 22 | val nextHandDealerState = 23 | if (updatedState.playersStillInHand.size == 1) HandDealerState.COMPLETE 24 | else HandDealerState.values()[updatedState.handDealerState.ordinal + 1] 25 | val roundCompletedEvent = RoundCompletedEvent(updatedState.tableId, updatedState.gameId, 26 | updatedState.entityId, nextHandDealerState) 27 | tableEvents.add(roundCompletedEvent) 28 | return tableEvents 29 | } 30 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/command/aggregate/tablebalancer/TableBalancerTestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.aggregate.tablebalancer 2 | 3 | import com.flexpoker.test.util.UnitTestClass 4 | import com.flexpoker.util.toPMap 5 | import com.flexpoker.util.toPSet 6 | import org.pcollections.PMap 7 | import org.pcollections.PSet 8 | import java.util.UUID 9 | 10 | object TableBalancerTestUtils { 11 | 12 | /** 13 | * Helper method to create a set of table to player maps. The subject 14 | * table/number of players is treated specially in order to keep some 15 | * control over the tests. 16 | */ 17 | fun createTableToPlayersMap(subjectTableId: UUID, numberOfSubjectTablePlayers: Int, 18 | vararg numberOfPlayersAtOtherTables: Int): PMap> { 19 | val tableToPlayersMap = HashMap>() 20 | tableToPlayersMap[subjectTableId] = createRandomSetOfPlayerIds(numberOfSubjectTablePlayers).toPSet() 21 | for (element in numberOfPlayersAtOtherTables) { 22 | tableToPlayersMap[UUID.randomUUID()] = createRandomSetOfPlayerIds(element).toPSet() 23 | } 24 | return tableToPlayersMap.toPMap() 25 | } 26 | 27 | private fun createRandomSetOfPlayerIds(numberOfPlayers: Int): Set { 28 | return (0 until numberOfPlayers).map { UUID.randomUUID() }.toSet() 29 | } 30 | 31 | fun createDefaultChipMapForSubjectTable(subjectTableId: UUID, tableToPlayersMap: Map>): Map { 32 | return tableToPlayersMap[subjectTableId]!!.associateWith { 100 } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/command/aggregate/generic/PauseTableTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.generic 2 | 3 | import com.flexpoker.exception.FlexPokerException 4 | import com.flexpoker.table.command.aggregate.TableState 5 | import com.flexpoker.table.command.aggregate.eventproducers.pause 6 | import com.flexpoker.table.command.aggregate.testhelpers.createBasicTable 7 | import com.flexpoker.table.command.events.TableEvent 8 | import com.flexpoker.table.command.events.TablePausedEvent 9 | import com.flexpoker.test.util.EventProducerApplierBuilder 10 | import com.flexpoker.test.util.UnitTestClass 11 | import org.junit.jupiter.api.Assertions.assertEquals 12 | import org.junit.jupiter.api.Test 13 | import java.util.UUID 14 | 15 | @UnitTestClass 16 | class PauseTableTest { 17 | 18 | @Test 19 | fun testPauseSuccess() { 20 | val initState = createBasicTable(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()) 21 | val (_, events) = EventProducerApplierBuilder() 22 | .initState(initState) 23 | .andRun { pause(it) } 24 | .run() 25 | assertEquals(TablePausedEvent::class.java, events.first().javaClass) 26 | } 27 | 28 | @Test 29 | fun testPauseTwice() { 30 | val initState = createBasicTable(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()) 31 | EventProducerApplierBuilder() 32 | .initState(initState) 33 | .andRun { pause(it) } 34 | .andRunThrows(FlexPokerException::class.java) { pause(it) } 35 | .run() 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/aggregate/eventproducers/AddRemovePlayerEventProducer.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.eventproducers 2 | 3 | import com.flexpoker.table.command.aggregate.RandomNumberGenerator 4 | import com.flexpoker.table.command.aggregate.TableState 5 | import com.flexpoker.table.command.events.PlayerAddedEvent 6 | import com.flexpoker.table.command.events.PlayerRemovedEvent 7 | import com.flexpoker.table.command.events.TableEvent 8 | import java.util.UUID 9 | 10 | fun addPlayer(state: TableState, playerId: UUID, chips: Int, randomNumberGenerator: RandomNumberGenerator): List { 11 | require(!state.seatMap.values.contains(playerId)) { "player already at this table" } 12 | val newPlayerPosition = findRandomOpenSeat(state, randomNumberGenerator) 13 | return listOf(PlayerAddedEvent(state.aggregateId, state.gameId, playerId, chips, newPlayerPosition)) 14 | } 15 | 16 | fun removePlayer(state: TableState, playerId: UUID): List { 17 | require(state.seatMap.values.contains(playerId)) { "player not at this table" } 18 | require(state.currentHand == null) { "can't remove a player while in a hand" } 19 | return listOf(PlayerRemovedEvent(state.aggregateId, state.gameId, playerId)) 20 | } 21 | 22 | private fun findRandomOpenSeat(state: TableState, randomNumberGenerator: RandomNumberGenerator): Int { 23 | while (true) { 24 | val potentialNewPlayerPosition = randomNumberGenerator.int(state.seatMap.size) 25 | if (state.seatMap[potentialNewPlayerPosition] == null) { 26 | return potentialNewPlayerPosition 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/webapp/resources/main.css: -------------------------------------------------------------------------------- 1 | .error-message { 2 | color: #ff0000; 3 | } 4 | 5 | .row-top-buffer { 6 | margin-top: 20px; 7 | } 8 | 9 | .standard-form { 10 | max-width: 350px; 11 | margin: 0 auto; 12 | } 13 | 14 | .hidden { 15 | display: none; 16 | } 17 | 18 | .game-list { 19 | margin-top: 10px; 20 | } 21 | 22 | .poker-table { 23 | background: url(/resources/img/Table.svg) no-repeat; 24 | } 25 | 26 | .my-cards { 27 | width: 100px; 28 | height: 100px; 29 | } 30 | 31 | .common-card { 32 | width: 100px; 33 | height: 100px; 34 | } 35 | 36 | .my-seat { 37 | box-shadow: inset 0 0 1em gold; 38 | } 39 | 40 | div[data-action-on='true'] { 41 | background-color: #00FF00; 42 | } 43 | 44 | div[data-action-on='false'] { 45 | background-color: #FF0000; 46 | } 47 | 48 | div[data-still-in-hand='true'] { 49 | color: #0000FF; 50 | } 51 | 52 | div[data-still-in-hand='false'] { 53 | color: #FF00FF; 54 | } 55 | 56 | .chat-area input[type=text] { 57 | width: 100%; 58 | } 59 | 60 | .chat-text-display-area { 61 | background-color: #eeeeee; 62 | height: 150px; 63 | width: 100%; 64 | overflow-y: scroll; 65 | } 66 | 67 | footer { 68 | position: absolute; 69 | bottom: 0; 70 | width: 100%; 71 | height: 30px; 72 | background-color: #f8f8f8; 73 | } 74 | 75 | footer .container { 76 | display: flex; 77 | justify-content: space-around; 78 | align-items: center; 79 | flex-flow: row wrap; 80 | } 81 | 82 | footer .container p { 83 | color: #777777; 84 | padding-top: 5px; 85 | } 86 | 87 | nav { 88 | min-height: 50px; 89 | margin-bottom: 20px; 90 | border: 1px solid transparent; 91 | border-color: #e7e7e7; 92 | } 93 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/query/repository/impl/InMemoryTableRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.repository.impl 2 | 3 | import com.flexpoker.table.query.repository.TableRepository 4 | import com.flexpoker.test.util.InMemoryTestClass 5 | import org.junit.jupiter.api.RepeatedTest 6 | import org.junit.jupiter.api.Test 7 | import org.springframework.beans.factory.annotation.Autowired 8 | 9 | @InMemoryTestClass 10 | class InMemoryTableRepositoryTest { 11 | 12 | @Autowired 13 | private lateinit var repository: TableRepository 14 | 15 | @Test 16 | fun testFetchNonExistentTable() { 17 | sharedTestFetchNonExistentTable(repository) 18 | } 19 | 20 | @Test 21 | fun testFetchExistingTable() { 22 | sharedTestFetchExistingTable(repository) 23 | } 24 | 25 | @Test 26 | fun testSaveIncrementingVersions() { 27 | sharedTestSaveIncrementingVersions(repository) 28 | } 29 | 30 | @Test 31 | fun testSaveDecrementingVersions() { 32 | sharedTestSaveDecrementingVersions(repository) 33 | } 34 | 35 | @Test 36 | fun testSaveDuplicateVersionsDoNotOverwrite() { 37 | sharedTestSaveDuplicateVersionsDoNotOverwrite(repository) 38 | } 39 | 40 | @RepeatedTest(100) 41 | @Throws(InterruptedException::class) 42 | fun testSaveMultithreadVersionsWithTwoDifferentTables() { 43 | sharedTestSaveMultithreadVersionsWithTwoDifferentTables(repository) 44 | } 45 | 46 | @RepeatedTest(100) 47 | @Throws(InterruptedException::class) 48 | fun testFetchMultithreaded() { 49 | sharedTestFetchMultithreaded(repository) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/command/repository/InMemoryGameEventRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.repository 2 | 3 | import com.flexpoker.test.util.InMemoryTestClass 4 | import org.junit.jupiter.api.Test 5 | import org.springframework.beans.factory.annotation.Autowired 6 | 7 | @InMemoryTestClass 8 | class InMemoryGameEventRepositoryTest { 9 | 10 | @Autowired 11 | private lateinit var repository: GameEventRepository 12 | 13 | @Test 14 | fun testFetchAll() { 15 | sharedTestFetchAll(repository) 16 | } 17 | 18 | @Test 19 | fun testFetchGameCreatedEventSuccess() { 20 | sharedTestFetchGameCreatedEventSuccess(repository) 21 | } 22 | 23 | @Test 24 | fun testFetchGameCreatedEventFail() { 25 | sharedTestFetchGameCreatedEventFail(repository) 26 | } 27 | 28 | @Test 29 | fun testSetEventVersionsAndSaveBadBasedOnVersion1() { 30 | sharedTestSetEventVersionsAndSaveBadBasedOnVersion1(repository) 31 | } 32 | 33 | @Test 34 | fun testSetEventVersionsAndSaveTwoJoinsSameTime() { 35 | sharedTestSetEventVersionsAndSaveTwoJoinsSameTime(repository) 36 | } 37 | 38 | @Test 39 | fun testSetEventVersionsAndSaveTwoJoinsSeparateTime() { 40 | sharedTestSetEventVersionsAndSaveTwoJoinsSeparateTime(repository) 41 | } 42 | 43 | @Test 44 | fun testSetEventVersionsAndSaveTwoJoinsInOneRequest() { 45 | sharedTestSetEventVersionsAndSaveTwoJoinsInOneRequest(repository) 46 | } 47 | 48 | @Test 49 | fun testSetEventVersionsAndSaveEmptyList() { 50 | sharedTestSetEventVersionsAndSaveEmptyList(repository) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/query/repository/impl/RedisOpenGameForPlayerRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository.impl 2 | 3 | import com.flexpoker.game.query.repository.OpenGameForPlayerRepository 4 | import com.flexpoker.test.util.RedisTestClass 5 | import com.flexpoker.test.util.redisContainer 6 | import org.junit.jupiter.api.Test 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection 9 | import org.testcontainers.junit.jupiter.Container 10 | 11 | @RedisTestClass 12 | class RedisOpenGameForPlayerRepositoryTest { 13 | 14 | @Autowired 15 | private lateinit var repository: OpenGameForPlayerRepository 16 | 17 | companion object { 18 | @Container 19 | @ServiceConnection 20 | val redisContainer = redisContainer() 21 | } 22 | 23 | @Test 24 | fun testFetchAllOpenGamesForPlayerNoGames() { 25 | sharedTestFetchAllOpenGamesForPlayerNoGames(repository) 26 | } 27 | 28 | @Test 29 | fun testFetchAllOpenGamesOrdered() { 30 | sharedTestFetchAllOpenGamesOrdered(repository) 31 | } 32 | 33 | @Test 34 | fun testDeleteOpenGameForPlayer() { 35 | sharedTestDeleteOpenGameForPlayer(repository) 36 | } 37 | 38 | @Test 39 | fun testAddOpenGameForUser() { 40 | sharedTestAddOpenGameForUser(repository) 41 | } 42 | 43 | @Test 44 | fun testChangeGameStage() { 45 | sharedTestChangeGameStage(repository) 46 | } 47 | 48 | @Test 49 | fun testAssignTableToOpenGame() { 50 | sharedTestAssignTableToOpenGame(repository) 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/command/aggregate/CreateNewGameTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.aggregate 2 | 3 | import com.flexpoker.game.command.aggregate.eventproducers.createGame 4 | import com.flexpoker.game.command.commands.CreateGameCommand 5 | import com.flexpoker.game.command.events.GameCreatedEvent 6 | import com.flexpoker.test.util.UnitTestClass 7 | import org.junit.jupiter.api.Assertions.assertEquals 8 | import org.junit.jupiter.api.Assertions.assertNotNull 9 | import org.junit.jupiter.api.Test 10 | import java.util.UUID 11 | 12 | @UnitTestClass 13 | class CreateNewGameTest { 14 | 15 | @Test 16 | fun testCreateNewGameSuccess() { 17 | val playerId = UUID.randomUUID() 18 | val command = CreateGameCommand("test", 2, 2, playerId, 19 | 10, 20) 20 | val newEvents = createGame(command.gameName, command.numberOfPlayers, command.numberOfPlayersPerTable, 21 | command.createdByPlayerId, command.numberOfMinutesBetweenBlindLevels, command.numberOfSecondsForActionOnTimer) 22 | assertEquals(1, newEvents.size) 23 | val gameCreatedEvent = newEvents[0] as GameCreatedEvent 24 | assertEquals("test", gameCreatedEvent.gameName) 25 | assertEquals(2, gameCreatedEvent.numberOfPlayers) 26 | assertEquals(2, gameCreatedEvent.numberOfPlayersPerTable) 27 | assertEquals(playerId, gameCreatedEvent.createdByPlayerId) 28 | assertEquals(10, gameCreatedEvent.numberOfMinutesBetweenBlindLevels) 29 | assertEquals(20, gameCreatedEvent.numberOfSecondsForActionOnTimer) 30 | assertNotNull(gameCreatedEvent.gameId) 31 | assertNotNull(gameCreatedEvent.aggregateId) 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/handlers/RoundCompletedEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.handlers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 5 | import com.flexpoker.pushnotifications.TableUpdatedPushNotification 6 | import com.flexpoker.table.command.events.RoundCompletedEvent 7 | import com.flexpoker.table.query.repository.TableRepository 8 | import org.springframework.stereotype.Component 9 | import javax.inject.Inject 10 | 11 | @Component 12 | class RoundCompletedEventHandler @Inject constructor( 13 | private val tableRepository: TableRepository, 14 | private val pushNotificationPublisher: PushNotificationPublisher 15 | ) : EventHandler { 16 | 17 | override fun handle(event: RoundCompletedEvent) { 18 | handleUpdatingTable(event) 19 | handlePushNotifications(event) 20 | } 21 | 22 | private fun handleUpdatingTable(event: RoundCompletedEvent) { 23 | val tableDTO = tableRepository.fetchById(event.aggregateId) 24 | val updatedSeats = tableDTO.seats!! 25 | .map { 26 | it.copy(chipsInFront = 0, raiseTo = minOf(it.chipsInBack, tableDTO.currentHandMinRaiseToAmount)) 27 | } 28 | val updatedTable = tableDTO.copy(version = event.version, seats = updatedSeats) 29 | tableRepository.save(updatedTable) 30 | } 31 | 32 | private fun handlePushNotifications(event: RoundCompletedEvent) { 33 | val pushNotification = TableUpdatedPushNotification(event.gameId, event.aggregateId) 34 | pushNotificationPublisher.publish(pushNotification) 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/handlers/PotClosedEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.handlers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 5 | import com.flexpoker.pushnotifications.TableUpdatedPushNotification 6 | import com.flexpoker.table.command.events.PotClosedEvent 7 | import com.flexpoker.table.query.repository.TableRepository 8 | import org.springframework.stereotype.Component 9 | import javax.inject.Inject 10 | 11 | @Component 12 | class PotClosedEventHandler @Inject constructor( 13 | private val tableRepository: TableRepository, 14 | private val pushNotificationPublisher: PushNotificationPublisher 15 | ) : EventHandler { 16 | 17 | override fun handle(event: PotClosedEvent) { 18 | handleUpdatingTable(event) 19 | handlePushNotifications(event) 20 | } 21 | 22 | private fun handleUpdatingTable(event: PotClosedEvent) { 23 | val tableDTO = tableRepository.fetchById(event.aggregateId) 24 | val updatedPots = tableDTO.pots!! 25 | .map { 26 | if (it.isOpen) { 27 | it.copy(isOpen = false) 28 | } else { 29 | it 30 | } 31 | }.toSet() 32 | val updatedTable = tableDTO.copy(version = event.version, pots = updatedPots) 33 | tableRepository.save(updatedTable) 34 | } 35 | 36 | private fun handlePushNotifications(event: PotClosedEvent) { 37 | val pushNotification = TableUpdatedPushNotification(event.gameId, event.aggregateId) 38 | pushNotificationPublisher.publish(pushNotification) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/repository/InMemoryTableEventRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.repository 2 | 3 | import com.flexpoker.config.ProfileNames 4 | import com.flexpoker.exception.FlexPokerException 5 | import com.flexpoker.table.command.events.TableEvent 6 | import org.springframework.context.annotation.Profile 7 | import org.springframework.stereotype.Repository 8 | import java.util.UUID 9 | 10 | @Profile(ProfileNames.DEFAULT, ProfileNames.TABLE_COMMAND_INMEMORY) 11 | @Repository 12 | class InMemoryTableEventRepository : TableEventRepository { 13 | 14 | private val tableEventMap: MutableMap> = HashMap() 15 | 16 | override fun fetchAll(id: UUID): List { 17 | return tableEventMap[id]!! 18 | } 19 | 20 | override fun setEventVersionsAndSave(basedOnVersion: Int, events: List): List { 21 | if (events.isEmpty()) { 22 | return emptyList() 23 | } else { 24 | val aggregateId = events[0].aggregateId 25 | if (!tableEventMap.containsKey(aggregateId)) { 26 | tableEventMap[aggregateId] = ArrayList() 27 | } 28 | val existingEvents: List = tableEventMap[aggregateId]!! 29 | if (existingEvents.size != basedOnVersion) { 30 | throw FlexPokerException("events to save are based on a different version of the aggregate") 31 | } 32 | for (i in events.indices) { 33 | events[i].version = basedOnVersion + i + 1 34 | } 35 | tableEventMap[aggregateId]!!.addAll(events) 36 | return events 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/signup/sign-up-step-1.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import SignUpStep1Form from "./sign-up-step-1-form" 3 | import SignUpStep1Success from "./sign-up-step-1-success" 4 | 5 | type View = 'form' | 'success' 6 | 7 | const submitFormCallback = (setView: (x: View) => {}, setEmail, setUsername, setError, evt) => { 8 | evt.preventDefault() 9 | 10 | const data = { 11 | username: evt.target.elements.username.value, 12 | password: evt.target.elements.password.value, 13 | emailAddress: evt.target.elements.emailAddress.value, 14 | } 15 | 16 | const myInit: RequestInit = { 17 | method: 'POST', 18 | cache: 'no-cache', 19 | redirect: 'follow', 20 | credentials: 'same-origin', 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | body: JSON.stringify(data), 25 | } 26 | 27 | fetch('/sign-up', myInit) 28 | .then(resp => resp.json()) 29 | .then(data => { 30 | if (data.error) { 31 | setError(data.error) 32 | } else { 33 | setEmail(data.email) 34 | setUsername(data.username) 35 | setView('success') 36 | } 37 | }) 38 | } 39 | 40 | 41 | export default () => { 42 | const [view, setView] = useState('form') 43 | const [email, setEmail] = useState(null) 44 | const [username, setUsername] = useState(null) 45 | const [error, setError] = useState(null) 46 | 47 | switch (view as View) { 48 | case 'form': 49 | return 50 | case 'success': 51 | return 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/test/util/datageneration/CardGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.test.util.datageneration 2 | 3 | import com.flexpoker.table.command.Card 4 | import com.flexpoker.table.command.CardRank 5 | import com.flexpoker.table.command.CardSuit 6 | import com.flexpoker.table.command.FlopCards 7 | import com.flexpoker.table.command.PocketCards 8 | import com.flexpoker.table.command.RiverCard 9 | import com.flexpoker.table.command.TurnCard 10 | 11 | object CardGenerator { 12 | 13 | private val CARD_1 = Card(1, CardRank.ACE, CardSuit.CLUBS) 14 | private val CARD_2 = Card(2, CardRank.TWO, CardSuit.CLUBS) 15 | private val CARD_3 = Card(3, CardRank.THREE, CardSuit.CLUBS) 16 | private val CARD_4 = Card(4, CardRank.FOUR, CardSuit.CLUBS) 17 | private val CARD_5 = Card(5, CardRank.FIVE, CardSuit.CLUBS) 18 | private val CARD_6 = Card(6, CardRank.SIX, CardSuit.CLUBS) 19 | private val CARD_7 = Card(7, CardRank.SEVEN, CardSuit.CLUBS) 20 | private val CARD_8 = Card(8, CardRank.EIGHT, CardSuit.CLUBS) 21 | private val CARD_9 = Card(9, CardRank.NINE, CardSuit.CLUBS) 22 | private val pocketCards1 = PocketCards(CARD_6, CARD_7) 23 | private val pocketCards2 = PocketCards(CARD_8, CARD_9) 24 | 25 | fun createFlopCards(): FlopCards { 26 | return FlopCards(CARD_1, CARD_2, CARD_3) 27 | } 28 | 29 | fun createTurnCard(): TurnCard { 30 | return TurnCard(CARD_4) 31 | } 32 | 33 | fun createRiverCard(): RiverCard { 34 | return RiverCard(CARD_5) 35 | } 36 | 37 | fun createPocketCards1(): PocketCards { 38 | return pocketCards1 39 | } 40 | 41 | fun createPocketCards2(): PocketCards { 42 | return pocketCards2 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/command/commandreceivers/InMemoryAsyncGameCommandReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.commandreceivers 2 | 3 | import com.flexpoker.framework.command.CommandHandler 4 | import com.flexpoker.framework.command.CommandReceiver 5 | import com.flexpoker.game.command.commands.AttemptToStartNewHandCommand 6 | import com.flexpoker.game.command.commands.CreateGameCommand 7 | import com.flexpoker.game.command.commands.GameCommand 8 | import com.flexpoker.game.command.commands.IncrementBlindsCommand 9 | import com.flexpoker.game.command.commands.JoinGameCommand 10 | import org.springframework.scheduling.annotation.Async 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component("gameCommandReceiver") 15 | class InMemoryAsyncGameCommandReceiver @Inject constructor( 16 | private val createGameCommandHandler: CommandHandler, 17 | private val joinGameCommandHandler: CommandHandler, 18 | private val attemptToStartNewHandCommandHandler: CommandHandler, 19 | private val incrementBlindsCommandHandler: CommandHandler 20 | ) : CommandReceiver { 21 | 22 | @Async 23 | override fun receive(command: GameCommand) { 24 | when (command) { 25 | is CreateGameCommand -> createGameCommandHandler.handle(command) 26 | is JoinGameCommand -> joinGameCommandHandler.handle(command) 27 | is AttemptToStartNewHandCommand -> attemptToStartNewHandCommandHandler 28 | .handle(command) 29 | is IncrementBlindsCommand -> incrementBlindsCommandHandler.handle(command) 30 | } 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/command/aggregate/BlindScheduleTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.aggregate 2 | 3 | import com.flexpoker.test.util.UnitTestClass 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Assertions.assertThrows 6 | import org.junit.jupiter.api.Assertions.assertTrue 7 | import org.junit.jupiter.api.Test 8 | 9 | @UnitTestClass 10 | class BlindScheduleTest { 11 | 12 | @Test 13 | fun testInitial() { 14 | val blindSchedule = BlindSchedule.init(10) 15 | assertEquals(1, blindSchedule.currentLevel) 16 | assertEquals(10, blindSchedule.numberOfMinutesBetweenLevels) 17 | assertEquals(10, blindSchedule.currentBlinds().smallBlind) 18 | assertEquals(20, blindSchedule.currentBlinds().bigBlind) 19 | } 20 | 21 | @Test 22 | fun testIncrement() { 23 | val blindSchedule = BlindSchedule.init(10).incrementBlinds() 24 | assertEquals(2, blindSchedule.currentLevel) 25 | assertEquals(20, blindSchedule.currentBlinds().smallBlind) 26 | assertEquals(40, blindSchedule.currentBlinds().bigBlind) 27 | } 28 | 29 | @Test 30 | fun testMaxLevel() { 31 | val blindSchedule = BlindSchedule.init(10).incrementBlinds().incrementBlinds().incrementBlinds().incrementBlinds() 32 | assertTrue(blindSchedule.atMaxLevel()) 33 | assertEquals(5, blindSchedule.currentLevel) 34 | assertEquals(5, blindSchedule.incrementBlinds().currentLevel) 35 | } 36 | 37 | @Test 38 | fun testCurrentLevelBounds() { 39 | assertThrows(IllegalArgumentException::class.java) { BlindSchedule(10, 0) } 40 | assertThrows(IllegalArgumentException::class.java) { BlindSchedule(10, 6) } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/handlers/ActionOnChangedEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.handlers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 5 | import com.flexpoker.login.repository.LoginRepository 6 | import com.flexpoker.pushnotifications.TableUpdatedPushNotification 7 | import com.flexpoker.table.command.events.ActionOnChangedEvent 8 | import com.flexpoker.table.query.repository.TableRepository 9 | import org.springframework.stereotype.Component 10 | import javax.inject.Inject 11 | 12 | @Component 13 | class ActionOnChangedEventHandler @Inject constructor( 14 | private val loginRepository: LoginRepository, 15 | private val tableRepository: TableRepository, 16 | private val pushNotificationPublisher: PushNotificationPublisher 17 | ) : EventHandler { 18 | 19 | override fun handle(event: ActionOnChangedEvent) { 20 | handleUpdatingTable(event) 21 | handlePushNotifications(event) 22 | } 23 | 24 | private fun handleUpdatingTable(event: ActionOnChangedEvent) { 25 | val tableDTO = tableRepository.fetchById(event.aggregateId) 26 | val username = loginRepository.fetchUsernameByAggregateId(event.playerId) 27 | val updatedSeats = tableDTO.seats!!.map { it.copy(isActionOn = it.name == username) } 28 | val updatedTable = tableDTO.copy(version = event.version, seats = updatedSeats) 29 | tableRepository.save(updatedTable) 30 | } 31 | 32 | private fun handlePushNotifications(event: ActionOnChangedEvent) { 33 | val pushNotification = TableUpdatedPushNotification(event.gameId, event.aggregateId) 34 | pushNotificationPublisher.publish(pushNotification) 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/command/aggregate/TableUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate 2 | 3 | import com.flexpoker.table.command.aggregate.eventproducers.checkHandIsBeingPlayed 4 | import com.flexpoker.table.command.aggregate.eventproducers.numberOfPlayersAtTable 5 | import com.flexpoker.table.command.aggregate.testhelpers.createBasicTable 6 | import com.flexpoker.test.util.UnitTestClass 7 | import org.junit.jupiter.api.Assertions.assertEquals 8 | import org.junit.jupiter.api.Assertions.assertThrows 9 | import org.junit.jupiter.api.Test 10 | import org.pcollections.HashTreePMap 11 | import java.util.UUID 12 | 13 | @UnitTestClass 14 | class CheckHandIsBeingPlayed { 15 | 16 | @Test 17 | fun `hand is in play`() { 18 | val state = createBasicTable(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()).copy( 19 | currentHand = genericHandState() 20 | ) 21 | checkHandIsBeingPlayed(state) 22 | } 23 | 24 | @Test 25 | fun `hand is not in play`() { 26 | val state = createBasicTable(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()).copy( 27 | currentHand = null 28 | ) 29 | assertThrows(IllegalArgumentException::class.java) { checkHandIsBeingPlayed(state) } 30 | } 31 | 32 | } 33 | 34 | @UnitTestClass 35 | class NumberOfPlayersAtTable { 36 | 37 | @Test 38 | fun `number of players is correct`() { 39 | val state = createBasicTable(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()).copy( 40 | seatMap = HashTreePMap 41 | .singleton(0, UUID.randomUUID()) 42 | .plus(4, null) 43 | .plus(7, UUID.randomUUID()) 44 | ) 45 | assertEquals(2, numberOfPlayersAtTable(state)) 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/eventpublishers/InMemoryAsyncTableEventPublisher.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.eventpublishers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.framework.event.EventPublisher 5 | import com.flexpoker.framework.event.subscriber.EventSubscriber 6 | import com.flexpoker.framework.processmanager.ProcessManager 7 | import com.flexpoker.table.command.events.ActionOnChangedEvent 8 | import com.flexpoker.table.command.events.AutoMoveHandForwardEvent 9 | import com.flexpoker.table.command.events.HandCompletedEvent 10 | import com.flexpoker.table.command.events.TableEvent 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class InMemoryAsyncTableEventPublisher @Inject constructor( 16 | private val tableEventSubscriber: EventSubscriber, 17 | private val actionOnCountdownProcessManager: ProcessManager, 18 | private val attemptToStartNewHandForExistingTableProcessManager: ProcessManager, 19 | private val autoMoveHandForwardProcessManager: ProcessManager 20 | ) : EventPublisher { 21 | 22 | companion object { 23 | private val NOOP = EventHandler { _: TableEvent -> } 24 | } 25 | 26 | override fun publish(event: TableEvent) { 27 | tableEventSubscriber.receive(event) 28 | when (event) { 29 | is ActionOnChangedEvent -> actionOnCountdownProcessManager.handle(event) 30 | is HandCompletedEvent -> attemptToStartNewHandForExistingTableProcessManager.handle(event) 31 | is AutoMoveHandForwardEvent -> autoMoveHandForwardProcessManager.handle(event) 32 | else -> NOOP 33 | } 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/signup/repository/RedisSignUpRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.signup.repository 2 | 3 | import com.flexpoker.test.util.RedisTestClass 4 | import com.flexpoker.test.util.redisContainer 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection 8 | import org.testcontainers.junit.jupiter.Container 9 | 10 | @RedisTestClass 11 | class RedisSignUpRepositoryTest { 12 | 13 | @Autowired 14 | private lateinit var repository: SignUpRepository 15 | 16 | companion object { 17 | @Container 18 | @ServiceConnection 19 | val redisContainer = redisContainer() 20 | } 21 | 22 | @Test 23 | fun testUsernameExists() { 24 | sharedTestUsernameExists(repository) 25 | } 26 | 27 | @Test 28 | fun testFetchSignUpUser() { 29 | sharedTestFetchSignUpUser(repository) 30 | } 31 | 32 | @Test 33 | fun testSaveSignUpUser() { 34 | sharedTestSaveSignUpUser(repository) 35 | } 36 | 37 | @Test 38 | fun testFindSignUpCodeByUsername() { 39 | sharedTestFindSignUpCodeByUsername(repository) 40 | } 41 | 42 | @Test 43 | fun testFindAggregateIdByUsernameAndSignUpCode() { 44 | sharedTestFindAggregateIdByUsernameAndSignUpCode(repository) 45 | } 46 | 47 | @Test 48 | fun testStoreNewlyConfirmedUsername() { 49 | sharedTestStoreNewlyConfirmedUsername(repository) 50 | } 51 | 52 | @Test 53 | fun testSignUpCodeExists() { 54 | sharedTestSignUpCodeExists(repository) 55 | } 56 | 57 | @Test 58 | fun testStoreSignUpInformation() { 59 | sharedTestStoreSignUpInformation(repository) 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/handlers/PotAmountIncreasedEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.handlers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 5 | import com.flexpoker.pushnotifications.TableUpdatedPushNotification 6 | import com.flexpoker.table.command.events.PotAmountIncreasedEvent 7 | import com.flexpoker.table.query.repository.TableRepository 8 | import org.springframework.stereotype.Component 9 | import javax.inject.Inject 10 | 11 | @Component 12 | class PotAmountIncreasedEventHandler @Inject constructor( 13 | private val tableRepository: TableRepository, 14 | private val pushNotificationPublisher: PushNotificationPublisher 15 | ) : EventHandler { 16 | 17 | override fun handle(event: PotAmountIncreasedEvent) { 18 | handleUpdatingTable(event) 19 | handlePushNotifications(event) 20 | } 21 | 22 | private fun handleUpdatingTable(event: PotAmountIncreasedEvent) { 23 | val tableDTO = tableRepository.fetchById(event.aggregateId) 24 | val updatedPots = tableDTO.pots!! 25 | .map { 26 | if (it.isOpen) { 27 | it.copy(amount = it.amount + event.amountIncreased) 28 | } else { 29 | it 30 | } 31 | }.toSet() 32 | val updatedTable = tableDTO.copy(version = event.version, pots = updatedPots) 33 | tableRepository.save(updatedTable) 34 | } 35 | 36 | private fun handlePushNotifications(event: PotAmountIncreasedEvent) { 37 | val pushNotification = TableUpdatedPushNotification(event.gameId, event.aggregateId) 38 | pushNotificationPublisher.publish(pushNotification) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/handlers/PotCreatedEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.handlers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 5 | import com.flexpoker.login.repository.LoginRepository 6 | import com.flexpoker.pushnotifications.TableUpdatedPushNotification 7 | import com.flexpoker.table.command.events.PotCreatedEvent 8 | import com.flexpoker.table.query.dto.PotDTO 9 | import com.flexpoker.table.query.repository.TableRepository 10 | import org.springframework.stereotype.Component 11 | import javax.inject.Inject 12 | 13 | @Component 14 | class PotCreatedEventHandler @Inject constructor( 15 | private val tableRepository: TableRepository, 16 | private val pushNotificationPublisher: PushNotificationPublisher, 17 | private val loginRepository: LoginRepository 18 | ) : EventHandler { 19 | 20 | override fun handle(event: PotCreatedEvent) { 21 | handleUpdatingTable(event) 22 | handlePushNotifications(event) 23 | } 24 | 25 | private fun handleUpdatingTable(event: PotCreatedEvent) { 26 | val tableDTO = tableRepository.fetchById(event.aggregateId) 27 | val playerUsernames = event.playersInvolved.map { loginRepository.fetchUsernameByAggregateId(it) }.toSet() 28 | val updatedPots = tableDTO.pots!!.plus(PotDTO(playerUsernames, 0, true, emptySet())) 29 | val updatedTable = tableDTO.copy(version = event.version, pots = updatedPots) 30 | tableRepository.save(updatedTable) 31 | } 32 | 33 | private fun handlePushNotifications(event: PotCreatedEvent) { 34 | val pushNotification = TableUpdatedPushNotification(event.gameId, event.aggregateId) 35 | pushNotificationPublisher.publish(pushNotification) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/lobby/CreateGameDialog/CreateGameDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, Form } from 'react-bootstrap' 2 | 3 | export default ({showModal, hideDialogCallback, submitFormCallback}) => { 4 | return ( 5 | 6 | 7 | Create Game 8 | 9 |
10 | 11 | 12 | Name 13 | 14 | 15 | 16 | Number of Players (2 - 90) 17 | 18 | 19 | 20 | Number of Players per Table (2 - 9) 21 | 22 | 23 | 24 | Blind increment in minutes (1 - 60) 25 | 26 | 27 | 28 | Blind timer in seconds (1 - 60) 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/command/aggregate/generic/RemovePlayerFromTableTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.aggregate.generic 2 | 3 | import com.flexpoker.table.command.aggregate.applyEvents 4 | import com.flexpoker.table.command.aggregate.eventproducers.removePlayer 5 | import com.flexpoker.table.command.aggregate.testhelpers.createBasicTable 6 | import com.flexpoker.table.command.aggregate.testhelpers.createBasicTableAndStartHand 7 | import com.flexpoker.table.command.events.PlayerRemovedEvent 8 | import com.flexpoker.test.util.UnitTestClass 9 | import org.junit.jupiter.api.Assertions.assertEquals 10 | import org.junit.jupiter.api.Assertions.assertThrows 11 | import org.junit.jupiter.api.Test 12 | import java.util.UUID 13 | 14 | @UnitTestClass 15 | class RemovePlayerFromTableTest { 16 | 17 | @Test 18 | fun testRemovePlayerSuccess() { 19 | val existingPlayer = UUID.randomUUID() 20 | val initState = createBasicTable(UUID.randomUUID(), UUID.randomUUID(), existingPlayer) 21 | val events = removePlayer(initState, existingPlayer) 22 | assertEquals(PlayerRemovedEvent::class.java, events[0].javaClass) 23 | } 24 | 25 | @Test 26 | fun testRemovingNonExistingPlayer() { 27 | val initState = createBasicTable(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()) 28 | assertThrows(IllegalArgumentException::class.java) { removePlayer(initState, UUID.randomUUID()) } 29 | } 30 | 31 | @Test 32 | fun testRemovingDuringAHand() { 33 | val existingPlayer = UUID.randomUUID() 34 | val events = createBasicTableAndStartHand(UUID.randomUUID(), UUID.randomUUID(), existingPlayer) 35 | val initState = applyEvents(events) 36 | assertThrows(IllegalArgumentException::class.java) { removePlayer(initState, existingPlayer) } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/handlers/TurnCardDealtEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.handlers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 5 | import com.flexpoker.pushnotifications.TableUpdatedPushNotification 6 | import com.flexpoker.table.command.events.TurnCardDealtEvent 7 | import com.flexpoker.table.query.dto.CardDTO 8 | import com.flexpoker.table.query.repository.CardsUsedInHandRepository 9 | import com.flexpoker.table.query.repository.TableRepository 10 | import org.springframework.stereotype.Component 11 | import javax.inject.Inject 12 | 13 | @Component 14 | class TurnCardDealtEventHandler @Inject constructor( 15 | private val tableRepository: TableRepository, 16 | private val cardsUsedInHandRepository: CardsUsedInHandRepository, 17 | private val pushNotificationPublisher: PushNotificationPublisher 18 | ) : EventHandler { 19 | 20 | override fun handle(event: TurnCardDealtEvent) { 21 | handleUpdatingTable(event) 22 | handlePushNotifications(event) 23 | } 24 | 25 | private fun handleUpdatingTable(event: TurnCardDealtEvent) { 26 | val tableDTO = tableRepository.fetchById(event.aggregateId) 27 | val turnCard = cardsUsedInHandRepository.fetchTurnCard(event.handId)!! 28 | val visibleCommonCards = tableDTO.visibleCommonCards!!.plus(CardDTO(turnCard.card.id)) 29 | val updatedTable = tableDTO.copy(version = event.version, visibleCommonCards = visibleCommonCards) 30 | tableRepository.save(updatedTable) 31 | } 32 | 33 | private fun handlePushNotifications(event: TurnCardDealtEvent) { 34 | val pushNotification = TableUpdatedPushNotification(event.gameId, event.aggregateId) 35 | pushNotificationPublisher.publish(pushNotification) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/command/service/DefaultCardServiceTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.service 2 | 3 | import com.flexpoker.test.util.UnitTestClass 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Assertions.assertFalse 6 | import org.junit.jupiter.api.Test 7 | 8 | @UnitTestClass 9 | class DefaultCardServiceTest { 10 | 11 | @Test 12 | fun testCreateShuffledDeck() { 13 | val service = DefaultCardService() 14 | val shuffledDeck = service.createShuffledDeck() 15 | assertEquals(52, shuffledDeck.size) 16 | } 17 | 18 | @Test 19 | fun testShuffledDecksAreDifferent() { 20 | val service = DefaultCardService() 21 | val shuffledDeck1 = service.createShuffledDeck() 22 | val shuffledDeck2 = service.createShuffledDeck() 23 | assertFalse(shuffledDeck1 === shuffledDeck2) 24 | assertFalse(shuffledDeck1 == shuffledDeck2) 25 | } 26 | 27 | @Test 28 | fun testCreateCardsUsedInHand() { 29 | val service = DefaultCardService() 30 | val fullDeckOfCards = service.createShuffledDeck() 31 | val (flopCards, turnCard, riverCard, pocketCards) = service.createCardsUsedInHand(fullDeckOfCards, 2) 32 | assertEquals(2, pocketCards.size) 33 | assertEquals(fullDeckOfCards[0], pocketCards[0].card1) 34 | assertEquals(fullDeckOfCards[1], pocketCards[1].card1) 35 | assertEquals(fullDeckOfCards[2], pocketCards[0].card2) 36 | assertEquals(fullDeckOfCards[3], pocketCards[1].card2) 37 | assertEquals(fullDeckOfCards[5], flopCards.card1) 38 | assertEquals(fullDeckOfCards[6], flopCards.card2) 39 | assertEquals(fullDeckOfCards[7], flopCards.card3) 40 | assertEquals(fullDeckOfCards[9], turnCard.card) 41 | assertEquals(fullDeckOfCards[11], riverCard.card) 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/handlers/RiverCardDealtEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.handlers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 5 | import com.flexpoker.pushnotifications.TableUpdatedPushNotification 6 | import com.flexpoker.table.command.events.RiverCardDealtEvent 7 | import com.flexpoker.table.query.dto.CardDTO 8 | import com.flexpoker.table.query.repository.CardsUsedInHandRepository 9 | import com.flexpoker.table.query.repository.TableRepository 10 | import org.springframework.stereotype.Component 11 | import javax.inject.Inject 12 | 13 | @Component 14 | class RiverCardDealtEventHandler @Inject constructor( 15 | private val tableRepository: TableRepository, 16 | private val cardsUsedInHandRepository: CardsUsedInHandRepository, 17 | private val pushNotificationPublisher: PushNotificationPublisher 18 | ) : EventHandler { 19 | 20 | override fun handle(event: RiverCardDealtEvent) { 21 | handleUpdatingTable(event) 22 | handlePushNotifications(event) 23 | } 24 | 25 | private fun handleUpdatingTable(event: RiverCardDealtEvent) { 26 | val tableDTO = tableRepository.fetchById(event.aggregateId) 27 | val riverCard = cardsUsedInHandRepository.fetchRiverCard(event.handId)!! 28 | val visibleCommonCards = tableDTO.visibleCommonCards!!.plus(CardDTO(riverCard.card.id)) 29 | val updatedTable = tableDTO.copy(version = event.version, visibleCommonCards = visibleCommonCards) 30 | tableRepository.save(updatedTable) 31 | } 32 | 33 | private fun handlePushNotifications(event: RiverCardDealtEvent) { 34 | val pushNotification = TableUpdatedPushNotification(event.gameId, event.aggregateId) 35 | pushNotificationPublisher.publish(pushNotification) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/processmanagers/IncrementBlindsCountdownProcessManager.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.processmanagers 2 | 3 | import com.flexpoker.framework.command.CommandSender 4 | import com.flexpoker.framework.processmanager.ProcessManager 5 | import com.flexpoker.game.command.commands.GameCommand 6 | import com.flexpoker.game.command.commands.IncrementBlindsCommand 7 | import com.flexpoker.game.command.events.GameStartedEvent 8 | import org.springframework.scheduling.annotation.Async 9 | import org.springframework.stereotype.Component 10 | import java.util.Timer 11 | import java.util.TimerTask 12 | import java.util.concurrent.atomic.AtomicInteger 13 | import javax.inject.Inject 14 | 15 | @Component 16 | class IncrementBlindsCountdownProcessManager @Inject constructor( 17 | private val gameCommandSender: CommandSender 18 | ) : ProcessManager { 19 | 20 | @Async 21 | override fun handle(event: GameStartedEvent) { 22 | val actionOnTimer = Timer() 23 | val timerTask: TimerTask = object : TimerTask() { 24 | val numberOfTimesToRun = event.blindSchedule.maxLevel() - 1 25 | val numberOfExecutions: AtomicInteger = AtomicInteger(0) 26 | 27 | override fun run() { 28 | val command = IncrementBlindsCommand(event.aggregateId) 29 | gameCommandSender.send(command) 30 | 31 | if (numberOfExecutions.incrementAndGet() == numberOfTimesToRun) { 32 | actionOnTimer.cancel() 33 | } 34 | } 35 | } 36 | val blindIncrementInMilliseconds = event.blindSchedule.numberOfMinutesBetweenLevels * 60000 37 | actionOnTimer.scheduleAtFixedRate( 38 | timerTask, 39 | blindIncrementInMilliseconds.toLong(), 40 | blindIncrementInMilliseconds.toLong() 41 | ) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/query/handlers/FlopCardsDealtEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.handlers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 5 | import com.flexpoker.pushnotifications.TableUpdatedPushNotification 6 | import com.flexpoker.table.command.events.FlopCardsDealtEvent 7 | import com.flexpoker.table.query.dto.CardDTO 8 | import com.flexpoker.table.query.repository.CardsUsedInHandRepository 9 | import com.flexpoker.table.query.repository.TableRepository 10 | import org.springframework.stereotype.Component 11 | import javax.inject.Inject 12 | 13 | @Component 14 | class FlopCardsDealtEventHandler @Inject constructor( 15 | private val tableRepository: TableRepository, 16 | private val cardsUsedInHandRepository: CardsUsedInHandRepository, 17 | private val pushNotificationPublisher: PushNotificationPublisher 18 | ) : EventHandler { 19 | 20 | override fun handle(event: FlopCardsDealtEvent) { 21 | handleUpdatingTable(event) 22 | handlePushNotifications(event) 23 | } 24 | 25 | private fun handleUpdatingTable(event: FlopCardsDealtEvent) { 26 | val tableDTO = tableRepository.fetchById(event.aggregateId) 27 | val (card1, card2, card3) = cardsUsedInHandRepository.fetchFlopCards(event.handId)!! 28 | val visibleCommonCards = listOf(CardDTO(card1.id), CardDTO(card2.id), CardDTO(card3.id)) 29 | val updatedTable = tableDTO.copy(version = event.version, visibleCommonCards = visibleCommonCards) 30 | tableRepository.save(updatedTable) 31 | } 32 | 33 | private fun handlePushNotifications(event: FlopCardsDealtEvent) { 34 | val pushNotification = TableUpdatedPushNotification(event.gameId, event.aggregateId) 35 | pushNotificationPublisher.publish(pushNotification) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/query/repository/impl/RedisGameListRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.repository.impl 2 | 3 | import com.flexpoker.game.query.repository.GameListRepository 4 | import com.flexpoker.test.util.RedisTestClass 5 | import com.flexpoker.test.util.redisContainer 6 | import org.junit.jupiter.api.BeforeEach 7 | import org.junit.jupiter.api.Test 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection 10 | import org.springframework.data.redis.connection.RedisConnectionFactory 11 | import org.testcontainers.junit.jupiter.Container 12 | 13 | @RedisTestClass 14 | class RedisGameListRepositoryTest { 15 | 16 | @Autowired 17 | private lateinit var repository: GameListRepository 18 | 19 | @Autowired 20 | private lateinit var connectionFactory: RedisConnectionFactory 21 | 22 | companion object { 23 | @Container 24 | @ServiceConnection 25 | val redisContainer = redisContainer() 26 | } 27 | 28 | @BeforeEach 29 | fun beforeEach() { 30 | connectionFactory.connection.serverCommands().flushAll() 31 | } 32 | 33 | @Test 34 | fun testSaveNew() { 35 | sharedTestSaveNew(repository) 36 | } 37 | 38 | @Test 39 | fun testFetchAllEmpty() { 40 | sharedTestFetchAllEmpty(repository) 41 | } 42 | 43 | @Test 44 | fun testFetchAllTwoSaved() { 45 | sharedTestFetchAllTwoSaved(repository) 46 | } 47 | 48 | @Test 49 | fun testIncrementRegisteredPlayers() { 50 | sharedTestIncrementRegisteredPlayers(repository) 51 | } 52 | 53 | @Test 54 | fun testFetchGameName() { 55 | sharedTestFetchGameName(repository) 56 | } 57 | 58 | @Test 59 | fun testChangeGameStage() { 60 | sharedTestChangeGameStage(repository) 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/table/command/repository/RedisTableEventRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.command.repository 2 | 3 | import com.flexpoker.config.ProfileNames 4 | import com.flexpoker.exception.FlexPokerException 5 | import com.flexpoker.table.command.events.TableEvent 6 | import org.springframework.context.annotation.Profile 7 | import org.springframework.data.redis.core.RedisTemplate 8 | import org.springframework.stereotype.Repository 9 | import java.util.UUID 10 | import javax.inject.Inject 11 | 12 | @Profile(ProfileNames.REDIS, ProfileNames.TABLE_COMMAND_REDIS) 13 | @Repository 14 | class RedisTableEventRepository @Inject constructor( 15 | private val redisTemplate: RedisTemplate, 16 | ) : TableEventRepository { 17 | 18 | companion object { 19 | private const val TABLE_EVENT_NAMESPACE = "table-event" 20 | } 21 | 22 | private fun redisKey(tableId: UUID) = "$TABLE_EVENT_NAMESPACE:$tableId" 23 | 24 | override fun fetchAll(id: UUID): List { 25 | return redisTemplate.opsForList().range(redisKey(id), 0, Long.MAX_VALUE)!! 26 | } 27 | 28 | override fun setEventVersionsAndSave(basedOnVersion: Int, events: List): List { 29 | if (events.isEmpty()) { 30 | return emptyList() 31 | } else { 32 | val aggregateId = events[0].aggregateId 33 | val existingEvents = fetchAll(aggregateId) 34 | if (existingEvents.size != basedOnVersion) { 35 | throw FlexPokerException("events to save are based on a different version of the aggregate") 36 | } 37 | for (i in events.indices) { 38 | events[i].version = basedOnVersion + i + 1 39 | } 40 | redisTemplate.opsForList().rightPushAll(redisKey(aggregateId), events) 41 | return events 42 | } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/query/handlers/GameCreatedEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.query.handlers 2 | 3 | import com.flexpoker.framework.event.EventHandler 4 | import com.flexpoker.framework.pushnotifier.PushNotificationPublisher 5 | import com.flexpoker.game.command.events.GameCreatedEvent 6 | import com.flexpoker.game.query.dto.GameInListDTO 7 | import com.flexpoker.game.command.GameStage 8 | import com.flexpoker.game.query.repository.GameListRepository 9 | import com.flexpoker.login.repository.LoginRepository 10 | import com.flexpoker.pushnotifications.GameListUpdatedPushNotification 11 | import org.springframework.stereotype.Component 12 | import javax.inject.Inject 13 | 14 | @Component 15 | class GameCreatedEventHandler @Inject constructor( 16 | private val loginRepository: LoginRepository, 17 | private val gameListRepository: GameListRepository, 18 | private val pushNotificationPublisher: PushNotificationPublisher 19 | ) : EventHandler { 20 | 21 | override fun handle(event: GameCreatedEvent) { 22 | handleGameListRepository(event) 23 | handlePushNotifications() 24 | } 25 | 26 | private fun handleGameListRepository(event: GameCreatedEvent) { 27 | val createdByUsername = loginRepository.fetchUsernameByAggregateId(event.createdByPlayerId) 28 | val gameInListDTO = GameInListDTO( 29 | event.aggregateId, 30 | event.gameName, GameStage.REGISTERING.toString(), 0, 31 | event.numberOfPlayers, event.numberOfPlayersPerTable, 32 | event.numberOfMinutesBetweenBlindLevels, 33 | event.numberOfSecondsForActionOnTimer, 34 | createdByUsername, event.time.toString() 35 | ) 36 | gameListRepository.saveNew(gameInListDTO) 37 | } 38 | 39 | private fun handlePushNotifications() { 40 | pushNotificationPublisher.publish(GameListUpdatedPushNotification) 41 | } 42 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/archtest/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.archtest 2 | 3 | import com.tngtech.archunit.base.DescribedPredicate 4 | import com.tngtech.archunit.core.domain.JavaClass 5 | import com.tngtech.archunit.core.domain.JavaClasses 6 | import com.tngtech.archunit.core.importer.ClassFileImporter 7 | import com.tngtech.archunit.core.importer.ImportOption 8 | import com.tngtech.archunit.lang.ArchCondition 9 | import com.tngtech.archunit.lang.ConditionEvents 10 | import com.tngtech.archunit.lang.SimpleConditionEvent 11 | 12 | val classesUnderTest: JavaClasses = ClassFileImporter() 13 | .withImportOption(ImportOption.DoNotIncludeTests()) 14 | .importPackages("com.flexpoker") 15 | 16 | fun nonInterfaceTopLevelClasses(regex: String): DescribedPredicate { 17 | return object : DescribedPredicate(regex) { 18 | override fun test(t: JavaClass?): Boolean { 19 | return if (t == null) false 20 | else (!t.isInterface 21 | && t.isTopLevelClass 22 | && t.name.contains(regex.toRegex())) 23 | } 24 | } 25 | } 26 | 27 | fun checkAnnotationContainsValue(annotationName: String, annotationValue: String): ArchCondition { 28 | return object : ArchCondition("$annotationName missing $annotationValue") { 29 | override fun check(item: JavaClass?, events: ConditionEvents?) { 30 | if (item != null && events != null) { 31 | val profileAnnotationValues = item.annotations 32 | .first { a -> (a.type as JavaClass).simpleName == annotationName } 33 | .get("value").get() as Array 34 | if (!profileAnnotationValues.contains(annotationValue)) { 35 | events.add(SimpleConditionEvent.violated(item, "${item.simpleName} violates rule")) 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/game/command/aggregate/tablebalancer/TableBalancerResumedTableTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.aggregate.tablebalancer 2 | 3 | import com.flexpoker.game.command.aggregate.createSingleBalancingEvent 4 | import com.flexpoker.game.command.events.TableResumedAfterBalancingEvent 5 | import com.flexpoker.test.util.UnitTestClass 6 | import org.junit.jupiter.api.Assertions.assertEquals 7 | import org.junit.jupiter.api.Test 8 | import java.util.UUID 9 | 10 | @UnitTestClass 11 | class TableBalancerResumedTableTest { 12 | 13 | @Test 14 | fun testOneBalancedTableThatIsPaused() { 15 | val subjectTableId = UUID.randomUUID() 16 | val tableToPlayersMap = TableBalancerTestUtils.createTableToPlayersMap(subjectTableId, 2) 17 | val event = createSingleBalancingEvent(UUID.randomUUID(), 2, subjectTableId, setOf(subjectTableId), 18 | tableToPlayersMap, TableBalancerTestUtils.createDefaultChipMapForSubjectTable(subjectTableId, tableToPlayersMap)) 19 | assertEquals(TableResumedAfterBalancingEvent::class.java, event.get().javaClass) 20 | assertEquals(subjectTableId, (event.get() as TableResumedAfterBalancingEvent).tableId) 21 | } 22 | 23 | @Test 24 | fun testTwoBalancedTablesOnePaused() { 25 | val subjectTableId = UUID.randomUUID() 26 | val tableToPlayersMap = TableBalancerTestUtils.createTableToPlayersMap(subjectTableId, 8, 8) 27 | val otherTableId = tableToPlayersMap.keys.first { it != subjectTableId } 28 | val event = createSingleBalancingEvent(UUID.randomUUID(), 9, subjectTableId, setOf(otherTableId), 29 | tableToPlayersMap, TableBalancerTestUtils.createDefaultChipMapForSubjectTable(subjectTableId, tableToPlayersMap)) 30 | assertEquals(TableResumedAfterBalancingEvent::class.java, event.get().javaClass) 31 | assertEquals(otherTableId, (event.get() as TableResumedAfterBalancingEvent).tableId) 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/flexpoker/game/command/repository/InMemoryGameEventRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.game.command.repository 2 | 3 | import com.flexpoker.config.ProfileNames 4 | import com.flexpoker.exception.FlexPokerException 5 | import com.flexpoker.game.command.events.GameCreatedEvent 6 | import com.flexpoker.game.command.events.GameEvent 7 | import org.springframework.context.annotation.Profile 8 | import org.springframework.stereotype.Repository 9 | import java.util.UUID 10 | 11 | @Profile(ProfileNames.DEFAULT, ProfileNames.TABLE_COMMAND_INMEMORY) 12 | @Repository 13 | class InMemoryGameEventRepository : GameEventRepository { 14 | 15 | private val gameEventMap: MutableMap> = HashMap() 16 | 17 | override fun fetchAll(id: UUID): List { 18 | return gameEventMap[id]!! 19 | } 20 | 21 | override fun setEventVersionsAndSave(basedOnVersion: Int, events: List): List { 22 | if (events.isEmpty()) { 23 | return emptyList() 24 | } else { 25 | val aggregateId = events[0].aggregateId 26 | if (!gameEventMap.containsKey(aggregateId)) { 27 | gameEventMap[aggregateId] = ArrayList() 28 | } 29 | val existingEvents: List = gameEventMap[aggregateId]!! 30 | if (existingEvents.size != basedOnVersion) { 31 | throw FlexPokerException("events to save are based on a different version of the aggregate") 32 | } 33 | for (i in events.indices) { 34 | events[i].version = basedOnVersion + i + 1 35 | } 36 | gameEventMap[aggregateId]!!.addAll(events) 37 | return events 38 | } 39 | } 40 | 41 | override fun fetchGameCreatedEvent(gameId: UUID): GameCreatedEvent { 42 | return fetchAll(gameId).first { it.javaClass === GameCreatedEvent::class.java } as GameCreatedEvent 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/main/webapp/resources/modules/signup/sign-up-step-2.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import SignUpStep2Form from "./sign-up-step-2-form" 3 | import SignUpStep2Success from "./sign-up-step-2-success" 4 | import { useSearchParams } from "react-router-dom" 5 | 6 | type View = 'none' | 'form' | 'success' 7 | 8 | const submitFormCallback = (setView: (x: View) => {}, setError, evt) => { 9 | evt.preventDefault() 10 | 11 | const data = { 12 | username: evt.target.elements.username.value, 13 | signUpCode: evt.target.elements.signUpCode.value, 14 | } 15 | 16 | const myInit: RequestInit = { 17 | method: 'POST', 18 | cache: 'no-cache', 19 | redirect: 'follow', 20 | credentials: 'same-origin', 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | body: JSON.stringify(data), 25 | } 26 | 27 | fetch('/sign-up-confirm', myInit) 28 | .then(resp => resp.json()) 29 | .then(data => { 30 | if (data.error) { 31 | setError(data.error) 32 | } else { 33 | setView('success') 34 | } 35 | }) 36 | } 37 | 38 | export default () => { 39 | const [view, setView] = useState('none') 40 | const [signUpCode, setSignUpCode] = useState(null) 41 | const [error, setError] = useState(null) 42 | const [searchParams, _] = useSearchParams() 43 | const username = searchParams.get('username') 44 | 45 | useEffect(() => { 46 | fetch(`/confirm-sign-up?username=${username}`) 47 | .then(resp => resp.json()) 48 | .then(data => { 49 | setSignUpCode(data.signUpCode) 50 | setView('form') 51 | }) 52 | }, []) 53 | 54 | switch (view as View) { 55 | case 'none': 56 | return null 57 | case 'form': 58 | return 59 | case 'success': 60 | return 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/kotlin/com/flexpoker/table/query/repository/impl/RedisTableRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.flexpoker.table.query.repository.impl 2 | 3 | import com.flexpoker.table.query.repository.TableRepository 4 | import com.flexpoker.test.util.RedisTestClass 5 | import com.flexpoker.test.util.redisContainer 6 | import org.junit.jupiter.api.RepeatedTest 7 | import org.junit.jupiter.api.Test 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection 10 | import org.testcontainers.junit.jupiter.Container 11 | 12 | @RedisTestClass 13 | class RedisTableRepositoryTest { 14 | 15 | @Autowired 16 | private lateinit var repository: TableRepository 17 | 18 | companion object { 19 | @Container 20 | @ServiceConnection 21 | val redisContainer = redisContainer() 22 | } 23 | 24 | @Test 25 | fun testFetchNonExistentTable() { 26 | sharedTestFetchNonExistentTable(repository) 27 | } 28 | 29 | @Test 30 | fun testFetchExistingTable() { 31 | sharedTestFetchExistingTable(repository) 32 | } 33 | 34 | @Test 35 | fun testSaveIncrementingVersions() { 36 | sharedTestSaveIncrementingVersions(repository) 37 | } 38 | 39 | @Test 40 | fun testSaveDecrementingVersions() { 41 | sharedTestSaveDecrementingVersions(repository) 42 | } 43 | 44 | @Test 45 | fun testSaveDuplicateVersionsDoNotOverwrite() { 46 | sharedTestSaveDuplicateVersionsDoNotOverwrite(repository) 47 | } 48 | 49 | @RepeatedTest(100) 50 | @Throws(InterruptedException::class) 51 | fun testSaveMultithreadVersionsWithTwoDifferentTables() { 52 | sharedTestSaveMultithreadVersionsWithTwoDifferentTables(repository) 53 | } 54 | 55 | @RepeatedTest(100) 56 | @Throws(InterruptedException::class) 57 | fun testFetchMultithreaded() { 58 | sharedTestFetchMultithreaded(repository) 59 | } 60 | 61 | } --------------------------------------------------------------------------------