├── .gitignore ├── pokerhand-fp ├── .gitignore ├── .prettierrc ├── .babelrc ├── src │ ├── matchers │ │ ├── type.ts │ │ ├── composites │ │ │ ├── index.ts │ │ │ └── groups.ts │ │ ├── repeats │ │ │ ├── repeatMoreThan.ts │ │ │ ├── repeatEqual.ts │ │ │ └── index.ts │ │ ├── consecutive.ts │ │ └── rank.ts │ ├── foldUntil.ts │ ├── ranks │ │ ├── pair.ts │ │ ├── twoPairs.ts │ │ ├── fullHouse.ts │ │ ├── fourOfKind.ts │ │ ├── threeOfKind.ts │ │ ├── flush.ts │ │ ├── straight.ts │ │ ├── straightFlush.ts │ │ └── highCards.ts │ ├── duel │ │ ├── duel.ts │ │ ├── valueDueler.ts │ │ └── rankDueler.ts │ ├── group.ts │ └── card.ts ├── package.json ├── tsconfig.json ├── test │ └── ranks │ │ ├── rank.spec.ts │ │ ├── twoPairs.spec.ts │ │ ├── flush.spec.ts │ │ ├── fourOfKind.spec.ts │ │ ├── highCards.spec.ts │ │ ├── fullHouse.spec.ts │ │ ├── pair.spec.ts │ │ ├── straight.spec.ts │ │ ├── straightFlush.spec.ts │ │ └── threeOfKind.spec.ts └── jest.config.js ├── pokerhand-oop ├── .gitignore ├── .prettierrc ├── .babelrc ├── src │ ├── matchers │ │ ├── Matcher.ts │ │ ├── ConsecutiveMatcher.ts │ │ ├── composites │ │ │ ├── CompositeMatcher.ts │ │ │ ├── SuitMatcher.ts │ │ │ └── FaceMatcher.ts │ │ ├── repeats │ │ │ ├── RepeatMoreThanMatcher.ts │ │ │ └── RepeatEqualMatcher.ts │ │ └── Matchers.ts │ ├── Card.ts │ ├── ranks │ │ ├── Rank.ts │ │ └── Ranks.ts │ ├── Group.ts │ └── Hand.ts ├── package.json ├── tsconfig.json ├── test │ └── ranks │ │ ├── Flush.spec.ts │ │ ├── Straight.spec.ts │ │ ├── StraightFlush.spec.ts │ │ ├── HighCard.spec.ts │ │ ├── FullHouse.spec.ts │ │ ├── FourOfAKind.spec.ts │ │ ├── ThreeOfAKind.spec.ts │ │ ├── Pair.spec.ts │ │ └── TwoPairs.spec.ts └── jest.config.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /pokerhand-fp/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | .idea 4 | *.iml -------------------------------------------------------------------------------- /pokerhand-oop/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | .idea 4 | *.iml -------------------------------------------------------------------------------- /pokerhand-fp/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /pokerhand-oop/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /pokerhand-fp/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript" 5 | ] 6 | } -------------------------------------------------------------------------------- /pokerhand-oop/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript" 5 | ] 6 | } -------------------------------------------------------------------------------- /pokerhand-oop/src/matchers/Matcher.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | 3 | export interface Matcher { 4 | isMatches(t: T): boolean 5 | matched(t: T): Hand 6 | unmatched(t: T): Hand 7 | } 8 | -------------------------------------------------------------------------------- /pokerhand-fp/src/matchers/type.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/card' 2 | 3 | export type MatchingResult = { 4 | result: boolean 5 | matched: Hand 6 | unmatched: Hand 7 | } 8 | 9 | export type Matcher = (t: T) => MatchingResult -------------------------------------------------------------------------------- /pokerhand-fp/src/foldUntil.ts: -------------------------------------------------------------------------------- 1 | export const foldUntil = until => f => (vs, defaultValue?) => { 2 | const v = vs.slice(0).reduce((acc, n, i, arr) => { 3 | const v = f(n) 4 | if (until(v)) { 5 | arr.splice(i) 6 | return v 7 | } 8 | 9 | return undefined 10 | }, []) 11 | 12 | return v ? v : defaultValue 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pokerhand - OOP vs FP 2 | 3 | This kata is to demonstrate different ways of acheiving the same results using different pardigmns, (OOP vs FP). 4 | 5 | One paradigmn is not superior than the other as long as developer followed good design principles such as SOLID. 6 | 7 | # More about the kata 8 | 9 | http://codingdojo.org/kata/PokerHands/ 10 | -------------------------------------------------------------------------------- /pokerhand-fp/src/matchers/composites/index.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import * as _ from 'lodash/fp' 3 | 4 | export const matchers: (...matchers: Matcher[]) => Matcher = ( 5 | ...matchers 6 | ) => t => ({ 7 | result: matchers.every(m => m(t).result), 8 | matched: _.head(matchers)(t).matched, 9 | unmatched: _.head(matchers)(t).unmatched 10 | }) 11 | -------------------------------------------------------------------------------- /pokerhand-fp/src/ranks/pair.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import { rankMatcher } from '~/matchers/rank' 3 | import { double, single } from '~/matchers/repeats' 4 | import { Hand } from '~/card' 5 | import { faces } from '~/matchers/composites/groups' 6 | 7 | export const pair: Matcher = rankMatcher( 8 | 'Pair', 9 | 2, 10 | faces(double(1), single(3)) 11 | ) 12 | -------------------------------------------------------------------------------- /pokerhand-fp/src/ranks/twoPairs.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import { rankMatcher } from '~/matchers/rank' 3 | import { double, single } from '~/matchers/repeats' 4 | import { Hand } from '~/card' 5 | import { faces } from '~/matchers/composites/groups' 6 | 7 | export const twoPairs: Matcher = rankMatcher( 8 | 'Two Pairs', 9 | 3, 10 | faces(double(2), single(1)) 11 | ) 12 | -------------------------------------------------------------------------------- /pokerhand-fp/src/ranks/fullHouse.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import { rankMatcher } from '~/matchers/rank' 3 | import { double, triple } from '~/matchers/repeats' 4 | import { Hand } from '~/card' 5 | import { faces } from '~/matchers/composites/groups' 6 | 7 | export const fullHouse: Matcher = rankMatcher( 8 | 'Full House', 9 | 7, 10 | faces(triple(1), double(1)) 11 | ) 12 | -------------------------------------------------------------------------------- /pokerhand-fp/src/ranks/fourOfKind.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import { rankMatcher } from '~/matchers/rank' 3 | import { quadruple, single } from '~/matchers/repeats' 4 | import { faces } from '~/matchers/composites/groups' 5 | import { Hand } from '~/card' 6 | 7 | export const fourOfKind: Matcher = rankMatcher( 8 | 'Four of a Kind', 9 | 8, 10 | faces(quadruple(1), single(1)) 11 | ) 12 | -------------------------------------------------------------------------------- /pokerhand-fp/src/ranks/threeOfKind.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import { rankMatcher } from '~/matchers/rank' 3 | import { single, triple } from '~/matchers/repeats' 4 | import { Hand } from '~/card' 5 | import { faces } from '~/matchers/composites/groups' 6 | 7 | export const threeOfKind: Matcher = rankMatcher( 8 | 'Three of a Kind', 9 | 4, 10 | faces(triple(1), single(2)) 11 | ) 12 | -------------------------------------------------------------------------------- /pokerhand-fp/src/matchers/repeats/repeatMoreThan.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import { count, Group, values } from '~/group' 3 | 4 | export const repeatMoreThan: ( 5 | repeat: number 6 | ) => Matcher = matcher => group => ({ 7 | result: count(group) > matcher, 8 | matched: count(group) > matcher ? values(group) : [], 9 | unmatched: count(group) > matcher ? [] : values(group) 10 | }) 11 | -------------------------------------------------------------------------------- /pokerhand-fp/src/matchers/consecutive.ts: -------------------------------------------------------------------------------- 1 | import { Hand, isConsecutive, sortByValue } from '~/card' 2 | import { Matcher } from '~/matchers/type' 3 | 4 | export const consecutive: (expected: boolean) => Matcher = 5 | expected => (hand: Hand) => { 6 | const sorted = hand.sort(sortByValue) 7 | return { 8 | result: hand.length === 5 && isConsecutive(sorted) === expected, 9 | matched: sorted, 10 | unmatched: [] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pokerhand-oop/src/Card.ts: -------------------------------------------------------------------------------- 1 | export enum Face { 2 | _2 = 2, 3 | _3, 4 | _4, 5 | _5, 6 | _6, 7 | _7, 8 | _8, 9 | _9, 10 | _T, 11 | _J, 12 | _Q, 13 | _K, 14 | _A 15 | } 16 | 17 | export enum Suit { 18 | C = 'C', 19 | D = 'D', 20 | H = 'H', 21 | S = 'S' 22 | } 23 | 24 | export class Card { 25 | constructor(public readonly face: Face, public readonly suit: Suit) {} 26 | 27 | static comparable = (c1: Card, c2: Card) => c2.face - c1.face 28 | } 29 | -------------------------------------------------------------------------------- /pokerhand-fp/src/matchers/composites/groups.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import { Group, GroupBy, groupByFace, groupBySuit } from '~/group' 3 | import { Hand } from '~/card' 4 | import { matchers } from '~/matchers/composites/index' 5 | 6 | const groups: (g: GroupBy) => (...ms: Matcher[]) => Matcher = 7 | groupBy => (...groupMatchers) => hand => matchers(...groupMatchers)(groupBy(hand)) 8 | 9 | export const faces = groups(groupByFace) 10 | export const suits = groups(groupBySuit) 11 | -------------------------------------------------------------------------------- /pokerhand-fp/src/ranks/flush.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import { rankMatcher } from '~/matchers/rank' 3 | import { quintuple } from '~/matchers/repeats' 4 | import { Hand } from '~/card' 5 | import { suits } from '~/matchers/composites/groups' 6 | import { matchers } from '~/matchers/composites' 7 | import { consecutive } from '~/matchers/consecutive' 8 | 9 | export const flush: Matcher = rankMatcher( 10 | 'Flush', 11 | 6, 12 | matchers(suits(quintuple(1)), consecutive(false)) 13 | ) 14 | -------------------------------------------------------------------------------- /pokerhand-fp/src/ranks/straight.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import { consecutive } from '~/matchers/consecutive' 3 | import { rankMatcher } from '~/matchers/rank' 4 | import { matchers } from '~/matchers/composites' 5 | import { moreThanOne } from '~/matchers/repeats' 6 | import { Hand } from '~/card' 7 | import { suits } from '~/matchers/composites/groups' 8 | 9 | export const straight: Matcher = rankMatcher( 10 | 'Straight', 11 | 5, 12 | matchers(consecutive(true), suits(moreThanOne)) 13 | ) 14 | -------------------------------------------------------------------------------- /pokerhand-oop/src/matchers/ConsecutiveMatcher.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/Matcher' 2 | import { Hand } from '~/Hand' 3 | 4 | export class ConsecutiveMatcher implements Matcher { 5 | constructor(private expected: boolean = true) {} 6 | 7 | isMatches(hand: Hand): boolean { 8 | return hand.isConsecutive === this.expected && hand.size === 5 9 | } 10 | 11 | matched(hand: Hand): Hand { 12 | return hand 13 | } 14 | 15 | unmatched(hand: Hand): Hand { 16 | return new Hand([]) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pokerhand-fp/src/duel/duel.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/card' 2 | import { matchRank, rankDueler } from '~/duel/rankDueler' 3 | import { valueDueler } from '~/duel/valueDueler' 4 | import { foldUntil } from '~/foldUntil' 5 | 6 | const duelers = [ 7 | rankDueler, 8 | valueDueler(r => r.matched), 9 | valueDueler(r => r.unmatched) 10 | ] 11 | 12 | export const duel = (h1: Hand, h2: Hand) => { 13 | const r1 = matchRank(h1) 14 | const r2 = matchRank(h2) 15 | 16 | return foldUntil(v => v)(duel => duel(r1, r2))(duelers, 'Tie.') 17 | } 18 | -------------------------------------------------------------------------------- /pokerhand-fp/src/ranks/straightFlush.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import { matchers } from '~/matchers/composites' 3 | import { rankMatcher } from '~/matchers/rank' 4 | import { consecutive } from '~/matchers/consecutive' 5 | import { Hand } from '~/card' 6 | import { suits } from '~/matchers/composites/groups' 7 | import { quintuple } from '~/matchers/repeats' 8 | 9 | export const straightFlush: Matcher = rankMatcher( 10 | 'Straight Flush', 11 | 9, 12 | matchers(consecutive(true), suits(quintuple(1))) 13 | ) 14 | -------------------------------------------------------------------------------- /pokerhand-fp/src/ranks/highCards.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/type' 2 | import { rankMatcher } from '~/matchers/rank' 3 | import { moreThanOne, single } from '~/matchers/repeats' 4 | import { matchers } from '~/matchers/composites' 5 | import { Hand } from '~/card' 6 | import { faces, suits } from '~/matchers/composites/groups' 7 | import { consecutive } from '~/matchers/consecutive' 8 | 9 | export const highCard: Matcher = rankMatcher( 10 | 'High Card', 11 | 1, 12 | matchers(faces(single(5)), suits(moreThanOne), consecutive(false)) 13 | ) 14 | -------------------------------------------------------------------------------- /pokerhand-oop/src/matchers/composites/CompositeMatcher.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/Matcher' 2 | 3 | export class CompositeMatcher implements Matcher { 4 | constructor(private primary: Matcher, private secondaries: Matcher[]) {} 5 | 6 | isMatches(t: T) { 7 | return ( 8 | this.primary.isMatches(t) && this.secondaries.every(m => m.isMatches(t)) 9 | ) 10 | } 11 | 12 | matched(t: T) { 13 | return this.primary.matched(t) 14 | } 15 | 16 | unmatched(t: T) { 17 | return this.primary.unmatched(t) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pokerhand-fp/src/matchers/repeats/repeatEqual.ts: -------------------------------------------------------------------------------- 1 | import { 2 | count, 3 | filterBy, 4 | Group, 5 | values, 6 | valueSizeEq, 7 | valueSizeNotEq 8 | } from '~/group' 9 | import { Matcher } from '~/matchers/type' 10 | 11 | export type MatcherCreator = (matcher: number) => Matcher 12 | 13 | export const repeatEqual: ( 14 | repeat: number 15 | ) => MatcherCreator = repeat => counts => group => ({ 16 | result: count(filterBy(valueSizeEq(repeat), group)) === counts, 17 | matched: values(filterBy(valueSizeEq(repeat), group)), 18 | unmatched: values(filterBy(valueSizeNotEq(repeat), group)) 19 | }) 20 | 21 | 22 | -------------------------------------------------------------------------------- /pokerhand-oop/src/matchers/repeats/RepeatMoreThanMatcher.ts: -------------------------------------------------------------------------------- 1 | import { Group } from '~/Group' 2 | import { Hand } from '~/Hand' 3 | import { Matcher } from '~/matchers/Matcher' 4 | 5 | export class RepeatMoreThanMatcher implements Matcher { 6 | constructor(protected repeat: number) {} 7 | 8 | isMatches(group: Group): boolean { 9 | return group.countKeys() > this.repeat 10 | } 11 | 12 | matched(group: Group): Hand { 13 | return this.isMatches(group) ? group.values() : new Hand([]) 14 | } 15 | 16 | unmatched(group: Group): Hand { 17 | return this.isMatches(group) ? new Hand([]) : group.values() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pokerhand-fp/src/matchers/repeats/index.ts: -------------------------------------------------------------------------------- 1 | import { Group } from '~/group' 2 | import { Matcher } from '~/matchers/type' 3 | import { repeatMoreThan } from '~/matchers/repeats/repeatMoreThan' 4 | import { MatcherCreator, repeatEqual } from '~/matchers/repeats/repeatEqual' 5 | 6 | export const moreThanOne: Matcher = repeatMoreThan(1) 7 | export const single: MatcherCreator = repeatEqual(1) 8 | export const double: MatcherCreator = repeatEqual(2) 9 | export const triple: MatcherCreator = repeatEqual(3) 10 | export const quadruple: MatcherCreator = repeatEqual(4) 11 | export const quintuple: MatcherCreator = repeatEqual(5) 12 | -------------------------------------------------------------------------------- /pokerhand-fp/src/matchers/rank.ts: -------------------------------------------------------------------------------- 1 | import { Matcher, MatchingResult } from '~/matchers/type' 2 | import { Hand } from '~/card' 3 | 4 | export type Rank = 5 | | 'High Card' 6 | | 'Pair' 7 | | 'Two Pairs' 8 | | 'Three of a Kind' 9 | | 'Straight' 10 | | 'Flush' 11 | | 'Full House' 12 | | 'Four of a Kind' 13 | | 'Straight Flush' 14 | 15 | export type RankMatcherResult = { name: Rank; value: number } & MatchingResult 16 | 17 | export const rankMatcher: ( 18 | name: Rank, 19 | value: number, 20 | matcher: Matcher 21 | ) => (hand: Hand) => RankMatcherResult = 22 | (name, value, matcher) => hand => ({ 23 | name, 24 | value, 25 | ...matcher(hand) 26 | }) 27 | -------------------------------------------------------------------------------- /pokerhand-oop/src/ranks/Rank.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import { Matcher } from '~/matchers/Matcher' 3 | 4 | export class Rank { 5 | constructor( 6 | public readonly name: string, 7 | public readonly value: number, 8 | public readonly matcher: Matcher 9 | ) {} 10 | 11 | matches(hand: Hand): boolean { 12 | return this.matcher.isMatches(hand) 13 | } 14 | 15 | compareTo(r: Rank) { 16 | return this.value - r.value 17 | } 18 | 19 | duel(r: Rank) { 20 | const rankValue = this.compareTo(r) 21 | if (rankValue > 0) return `You win: ${this.name} > ${r.name}` 22 | else if (rankValue < 0) return `You lose: ${this.name} < ${r.name}` 23 | else return undefined 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pokerhand-fp/src/group.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash/fp' 2 | import { Hand, sortByValue } from '~/card' 3 | 4 | export type Group = { [x: string]: Hand } 5 | export type GroupBy = (h:Hand) => Group 6 | 7 | export const groupBySuit: GroupBy = _.memoize(_.groupBy(c => c.suit)) 8 | export const groupByFace: GroupBy = _.memoize(_.groupBy(c => c.face)) 9 | 10 | export const count = (g: Group) => Object.keys(g).length 11 | 12 | export const filterBy = _.pickBy 13 | 14 | export const values: (g: Group) => Hand = 15 | g => _.flatten(Object.values(g)).sort(sortByValue) 16 | 17 | export const valueSizeEq = (size: number) => (h: Hand) => size === h.length 18 | export const valueSizeNotEq = (size: number) => (h: Hand) => size !== h.length 19 | -------------------------------------------------------------------------------- /pokerhand-fp/src/duel/valueDueler.ts: -------------------------------------------------------------------------------- 1 | import { RankMatcherResult } from '~/matchers/rank' 2 | import { cardValue } from '~/card' 3 | import { foldUntil } from '~/foldUntil' 4 | import * as _ from 'lodash/fp' 5 | 6 | const win = (name, v1, v2) => `${name}: ${v1.face} over ${v2.face}` 7 | 8 | export const valueDueler = values => ( 9 | r1: RankMatcherResult, 10 | r2: RankMatcherResult 11 | ) => { 12 | const r1Values = values(r1) 13 | const r2Values = values(r2) 14 | 15 | return foldUntil(v => v !== undefined)(v => { 16 | if (cardValue(v[0]) > cardValue(v[1])) return win(r1.name, v[0], v[1]) 17 | else if (cardValue(v[0]) < cardValue(v[1])) return win(r2.name, v[1], v[0]) 18 | else return undefined 19 | })(_.zip(r1Values, r2Values)) 20 | } 21 | -------------------------------------------------------------------------------- /pokerhand-oop/src/matchers/repeats/RepeatEqualMatcher.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/Matcher' 2 | import { Hand } from '~/Hand' 3 | import { Group } from '~/Group' 4 | import { Card } from '~/Card' 5 | 6 | export class RepeatEqualMatcher implements Matcher { 7 | constructor(private predicate, private repeat: number) {} 8 | 9 | isMatches(group: Group): boolean { 10 | return group.filter(this.predicate).countKeys() === this.repeat 11 | } 12 | 13 | matched(group: Group): Hand { 14 | return group.filter(this.predicate).values() 15 | } 16 | 17 | unmatched(group: Group): Hand { 18 | return group.filter(this.notPredicate()).values() 19 | } 20 | 21 | private notPredicate() { 22 | return (cs: Card[]) => !this.predicate(cs) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pokerhand-fp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pokerhand", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "fmt": "prettier --write '{src,test}/**/*.{js,ts}'", 9 | "build": "babel src -d lib --extensions \".ts\"" 10 | }, 11 | "devDependencies": { 12 | "@babel/cli": "^7.6.0", 13 | "@babel/core": "^7.6.0", 14 | "@babel/preset-env": "^7.6.0", 15 | "@babel/preset-typescript": "^7.6.0", 16 | "@types/jest": "^24.0.18", 17 | "babel-jest": "^24.9.0", 18 | "jest": "^24.9.0", 19 | "prettier": "1.18.2" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "ISC", 24 | "dependencies": { 25 | "babel-runtime": "^6.26.0", 26 | "lodash": "^4.17.15" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pokerhand-oop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pokerhand", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "fmt": "prettier --write '{src,test}/**/*.{js,ts}'", 9 | "build": "babel src -d lib --extensions \".ts\"" 10 | }, 11 | "devDependencies": { 12 | "@babel/cli": "^7.6.0", 13 | "@babel/core": "^7.6.0", 14 | "@babel/preset-env": "^7.6.0", 15 | "@babel/preset-typescript": "^7.6.0", 16 | "@types/jest": "^24.0.18", 17 | "babel-jest": "^24.9.0", 18 | "jest": "^24.9.0", 19 | "prettier": "1.18.2" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "ISC", 24 | "dependencies": { 25 | "babel-runtime": "^6.26.0", 26 | "lodash": "^4.17.15" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pokerhand-oop/src/Group.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import * as _ from 'lodash' 3 | import { Card } from '~/Card' 4 | 5 | export class Group { 6 | private readonly group: Hand 7 | 8 | static groupByValue(hand: Hand): Group { 9 | return new Group(_.groupBy(hand.cards, c => c.face)) 10 | } 11 | 12 | static groupBySuit(hand: Hand): Group { 13 | return new Group(_.groupBy(hand.cards, c => c.suit)) 14 | } 15 | 16 | constructor(group) { 17 | this.group = group 18 | } 19 | 20 | countKeys() { 21 | return Object.keys(this.group).length 22 | } 23 | 24 | filter(f: (vs: Card[]) => boolean) { 25 | return new Group(_.pickBy(this.group, f)) 26 | } 27 | 28 | values(): Hand { 29 | return new Hand(_.flatten(Object.values(this.group))) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pokerhand-oop/src/matchers/composites/SuitMatcher.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/Matcher' 2 | import { Hand } from '~/Hand' 3 | import { CompositeMatcher } from '~/matchers/composites/CompositeMatcher' 4 | import { Group } from '~/Group' 5 | 6 | export class SuitMatcher implements Matcher { 7 | private composite: CompositeMatcher 8 | 9 | constructor(primary: Matcher, secondaries: Matcher[]) { 10 | this.composite = new CompositeMatcher(primary, secondaries) 11 | } 12 | 13 | isMatches(hand: Hand): boolean { 14 | return this.composite.isMatches(hand.groupBySuit()) 15 | } 16 | 17 | matched(hand: Hand): Hand { 18 | return this.composite.matched(hand.groupBySuit()) 19 | } 20 | 21 | unmatched(hand: Hand): Hand { 22 | return this.composite.unmatched(hand.groupBySuit()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pokerhand-oop/src/matchers/composites/FaceMatcher.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/Matcher' 2 | import { Group } from '~/Group' 3 | import { Hand } from '~/Hand' 4 | import { CompositeMatcher } from '~/matchers/composites/CompositeMatcher' 5 | 6 | export class FaceMatcher implements Matcher { 7 | private composite: CompositeMatcher 8 | 9 | constructor(primary: Matcher, secondaries: Matcher[]) { 10 | this.composite = new CompositeMatcher(primary, secondaries) 11 | } 12 | 13 | isMatches(hand: Hand): boolean { 14 | return this.composite.isMatches(hand.groupByValue()) 15 | } 16 | 17 | matched(hand: Hand): Hand { 18 | return this.composite.matched(hand.groupByValue()) 19 | } 20 | 21 | unmatched(hand: Hand): Hand { 22 | return this.composite.unmatched(hand.groupByValue()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pokerhand-fp/src/card.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash/fp' 2 | 3 | 4 | export type Suit = 'C' | 'D' | 'H' | 'S' 5 | 6 | export type Face = 7 | | '2' 8 | | '3' 9 | | '4' 10 | | '5' 11 | | '6' 12 | | '7' 13 | | '8' 14 | | '9' 15 | | 'T' 16 | | 'J' 17 | | 'Q' 18 | | 'K' 19 | | 'A' 20 | 21 | export type Card = { 22 | face: Face 23 | suit: Suit 24 | } 25 | 26 | export type Hand = Card[] 27 | 28 | export const cardValue = (c: Card) => '23456789TJQKA'.indexOf(c.face) + 2 29 | 30 | export const sortByValue = (a: Card, b: Card) => cardValue(b) - cardValue(a) 31 | 32 | export const isConsecutive = (h: Hand) => { 33 | const fold: (c: Card, h: Hand) => boolean = (c, h) => { 34 | if (h.length === 0) return true 35 | if (cardValue(c) - 1 === cardValue(h[0])) return fold(_.head(h), _.tail(h)) 36 | else return false 37 | } 38 | 39 | return fold(_.head(h), _.tail(h)) 40 | } 41 | -------------------------------------------------------------------------------- /pokerhand-fp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.js"], 3 | "exclude": ["node_modules"], 4 | "compilerOptions": { 5 | "target": "es5", 6 | "jsx": "preserve", 7 | "lib": ["es5", "dom", "es2015", "es2015.promise", "es2017"], 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "resolveJsonModule": true, 13 | "noImplicitAny": false, 14 | "strictFunctionTypes": false, 15 | "strictNullChecks": true, 16 | "removeComments": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "allowSyntheticDefaultImports": true, 19 | "downlevelIteration": true, 20 | "allowJs": true, 21 | "sourceMap": true, 22 | "strict": true, 23 | "baseUrl": ".", 24 | "types": [ 25 | "@types/node", 26 | "@types/jest" 27 | ], 28 | "paths": { 29 | "~/*": ["src/*"], 30 | "~~/*": ["test/*"], 31 | "@/*": ["./*"] 32 | }, 33 | "esModuleInterop": true, 34 | "noEmit": false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pokerhand-oop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.js"], 3 | "exclude": ["node_modules"], 4 | "compilerOptions": { 5 | "target": "es5", 6 | "jsx": "preserve", 7 | "lib": ["es5", "dom", "es2015", "es2015.promise", "es2017"], 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "resolveJsonModule": true, 13 | "noImplicitAny": false, 14 | "strictFunctionTypes": false, 15 | "strictNullChecks": true, 16 | "removeComments": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "allowSyntheticDefaultImports": true, 19 | "downlevelIteration": true, 20 | "allowJs": true, 21 | "sourceMap": true, 22 | "strict": true, 23 | "baseUrl": ".", 24 | "types": [ 25 | "@types/node", 26 | "@types/jest" 27 | ], 28 | "paths": { 29 | "~/*": ["src/*"], 30 | "~~/*": ["test/*"], 31 | "@/*": ["./*"] 32 | }, 33 | "esModuleInterop": true, 34 | "noEmit": false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pokerhand-fp/src/duel/rankDueler.ts: -------------------------------------------------------------------------------- 1 | import { RankMatcherResult } from '~/matchers/rank' 2 | import { highCard } from '~/ranks/highCards' 3 | import { pair } from '~/ranks/pair' 4 | import { twoPairs } from '~/ranks/twoPairs' 5 | import { threeOfKind } from '~/ranks/threeOfKind' 6 | import { straight } from '~/ranks/straight' 7 | import { flush } from '~/ranks/flush' 8 | import { fullHouse } from '~/ranks/fullHouse' 9 | import { straightFlush } from '~/ranks/straightFlush' 10 | import { Hand } from '~/card' 11 | import { fourOfKind } from '~/ranks/fourOfKind' 12 | 13 | const ranks = [ 14 | highCard, 15 | pair, 16 | twoPairs, 17 | threeOfKind, 18 | straight, 19 | flush, 20 | fullHouse, 21 | fourOfKind, 22 | straightFlush 23 | ] 24 | 25 | export const matchRank = (h: Hand) => ranks.map(m => m(h)).find(r => r.result) 26 | 27 | export const rankDueler = (r1: RankMatcherResult, r2: RankMatcherResult) => { 28 | if (r1.value > r2.value) return `${r1.name}: ${r1.matched[0].face}` 29 | else if (r1.value < r2.value) return `${r2.name}: ${r2.matched[0].face}` 30 | return undefined 31 | } 32 | -------------------------------------------------------------------------------- /pokerhand-oop/test/ranks/Flush.spec.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import { Card, Face, Suit } from '~/Card' 3 | 4 | describe('Flush', () => { 5 | const flush1 = new Hand([ 6 | new Card(Face._2, Suit.C), 7 | new Card(Face._7, Suit.C), 8 | new Card(Face._4, Suit.C), 9 | new Card(Face._A, Suit.C), 10 | new Card(Face._6, Suit.C) 11 | ]) 12 | 13 | it('matches as a flush', () => { 14 | expect(flush1.rank.name).toEqual('Flush') 15 | }) 16 | 17 | describe('duel', () => { 18 | const straight1 = new Hand([ 19 | new Card(Face._2, Suit.S), 20 | new Card(Face._3, Suit.C), 21 | new Card(Face._4, Suit.D), 22 | new Card(Face._5, Suit.C), 23 | new Card(Face._6, Suit.S) 24 | ]) 25 | 26 | const flush2 = new Hand([ 27 | new Card(Face._3, Suit.C), 28 | new Card(Face._7, Suit.C), 29 | new Card(Face._4, Suit.C), 30 | new Card(Face._A, Suit.C), 31 | new Card(Face._6, Suit.C) 32 | ]) 33 | 34 | it('flush > straight', () => { 35 | expect(flush1.duel(straight1)).toEqual('You win: Flush > Straight') 36 | }) 37 | 38 | it('flush vs flush: highest card of flush wins', () => { 39 | expect(flush2.duel(flush1)).toEqual('You win: Flush with 3 > 2') 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /pokerhand-fp/test/ranks/rank.spec.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/card' 2 | import { duel } from '~/duel/duel' 3 | 4 | describe('rank', () => { 5 | const highCard: Hand = [ 6 | { suit: 'S', face: '5' }, 7 | { suit: 'S', face: '7' }, 8 | { suit: 'S', face: '9' }, 9 | { suit: 'D', face: 'J' }, 10 | { suit: 'S', face: 'A' } 11 | ] 12 | 13 | const pair: Hand = [ 14 | { suit: 'D', face: '2' }, 15 | { suit: 'S', face: '7' }, 16 | { suit: 'S', face: '9' }, 17 | { suit: 'S', face: '2' }, 18 | { suit: 'S', face: '3' } 19 | ] 20 | 21 | describe('duel', () => { 22 | describe('uneven ranks', () => { 23 | it('double wins over high card', () => { 24 | expect(duel(pair, highCard)).toEqual('Pair: 2') 25 | }) 26 | }) 27 | 28 | describe('even ranks', () => { 29 | const pair2: Hand = [ 30 | { suit: 'D', face: '2' }, 31 | { suit: 'S', face: '7' }, 32 | { suit: 'S', face: '9' }, 33 | { suit: 'S', face: '2' }, 34 | { suit: 'S', face: 'A' } 35 | ] 36 | 37 | it('pair2 wins', () => { 38 | expect(duel(pair, pair2)).toEqual('Pair: A over 9') 39 | }) 40 | }) 41 | 42 | describe('ties', () => { 43 | it('ties', () => { 44 | expect(duel(pair, pair)).toEqual('Tie.') 45 | }) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /pokerhand-oop/test/ranks/Straight.spec.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import { Card, Face, Suit } from '~/Card' 3 | 4 | describe('Straight', () => { 5 | const straight1 = new Hand([ 6 | new Card(Face._2, Suit.S), 7 | new Card(Face._3, Suit.C), 8 | new Card(Face._4, Suit.D), 9 | new Card(Face._5, Suit.C), 10 | new Card(Face._6, Suit.S) 11 | ]) 12 | 13 | it('matches as a straight', () => { 14 | expect(straight1.rank.name).toEqual('Straight') 15 | }) 16 | 17 | describe('duel', () => { 18 | const threeOfAKind1 = new Hand([ 19 | new Card(Face._2, Suit.S), 20 | new Card(Face._2, Suit.C), 21 | new Card(Face._2, Suit.D), 22 | new Card(Face._4, Suit.C), 23 | new Card(Face._8, Suit.S) 24 | ]) 25 | 26 | const straight2 = new Hand([ 27 | new Card(Face._3, Suit.C), 28 | new Card(Face._4, Suit.D), 29 | new Card(Face._5, Suit.C), 30 | new Card(Face._6, Suit.S), 31 | new Card(Face._7, Suit.S) 32 | ]) 33 | 34 | it('straight > three of a kind', () => { 35 | expect(straight1.duel(threeOfAKind1)).toEqual( 36 | 'You win: Straight > Three of a Kind' 37 | ) 38 | }) 39 | 40 | it('straight vs straight: highest card of straight wins', () => { 41 | expect(straight2.duel(straight1)).toEqual('You win: Straight with 7 > 6') 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /pokerhand-fp/test/ranks/twoPairs.spec.ts: -------------------------------------------------------------------------------- 1 | import { twoPairs } from '~/ranks/twoPairs' 2 | import { Hand } from '~/card' 3 | 4 | describe('two pairs', () => { 5 | describe('matches', () => { 6 | const hand: Hand = [ 7 | { suit: 'D', face: '2' }, 8 | { suit: 'S', face: '2' }, 9 | { suit: 'S', face: '9' }, 10 | { suit: 'H', face: '9' }, 11 | { suit: 'S', face: '3' } 12 | ] 13 | 14 | it('returns true when there is 2 pairs', () => { 15 | expect(twoPairs(hand).result).toBeTruthy() 16 | }) 17 | 18 | it('shows matching pairs in the matched', () => { 19 | expect(twoPairs(hand).matched).toEqual([ 20 | { suit: 'S', face: '9' }, 21 | { suit: 'H', face: '9' }, 22 | { suit: 'D', face: '2' }, 23 | { suit: 'S', face: '2' } 24 | ]) 25 | }) 26 | 27 | it('shows unmatched single cards in the unmatched', () => { 28 | expect(twoPairs(hand).unmatched).toEqual([{ suit: 'S', face: '3' }]) 29 | }) 30 | }) 31 | 32 | describe('unmatched', () => { 33 | const hand: Hand = [ 34 | { suit: 'S', face: '2' }, 35 | { suit: 'D', face: '2' }, 36 | { suit: 'S', face: '9' }, 37 | { suit: 'D', face: '9' }, 38 | { suit: 'H', face: '9' } 39 | ] 40 | 41 | it('returns false when there is three of kind with a double', () => { 42 | expect(twoPairs([]).result).toBeFalsy() 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /pokerhand-fp/test/ranks/flush.spec.ts: -------------------------------------------------------------------------------- 1 | import { flush } from '~/ranks/flush' 2 | import { Hand } from '~/card' 3 | 4 | describe('flush', () => { 5 | describe('matches', () => { 6 | const hand: Hand = [ 7 | { suit: 'D', face: '2' }, 8 | { suit: 'D', face: '5' }, 9 | { suit: 'D', face: '7' }, 10 | { suit: 'D', face: '9' }, 11 | { suit: 'D', face: 'Q' } 12 | ] 13 | 14 | it('returns true when all cards share the same suit', () => { 15 | expect(flush(hand).result).toBeTruthy() 16 | }) 17 | 18 | it('shows matched as all cards that share the same suit', () => { 19 | expect(flush(hand).matched).toEqual([ 20 | { suit: 'D', face: 'Q' }, 21 | { suit: 'D', face: '9' }, 22 | { suit: 'D', face: '7' }, 23 | { suit: 'D', face: '5' }, 24 | { suit: 'D', face: '2' } 25 | ]) 26 | }) 27 | 28 | it('shows unmatched as none', () => { 29 | expect(flush(hand).unmatched).toEqual([]) 30 | }) 31 | }) 32 | 33 | describe('unmatched', () => { 34 | it('returns false when one card is not in a same suit', () => { 35 | const hand: Hand = [ 36 | { suit: 'D', face: '2' }, 37 | { suit: 'D', face: '5' }, 38 | { suit: 'D', face: '7' }, 39 | { suit: 'D', face: '9' }, 40 | { suit: 'S', face: 'Q' } 41 | ] 42 | 43 | expect(flush(hand).result).toBeFalsy() 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /pokerhand-fp/test/ranks/fourOfKind.spec.ts: -------------------------------------------------------------------------------- 1 | import { fourOfKind } from '~/ranks/fourOfKind' 2 | import { Hand } from '~/card' 3 | 4 | describe('four of kind', () => { 5 | describe('matches', () => { 6 | const hand: Hand = [ 7 | { suit: 'D', face: '2' }, 8 | { suit: 'S', face: '2' }, 9 | { suit: 'C', face: '2' }, 10 | { suit: 'H', face: '2' }, 11 | { suit: 'S', face: '3' } 12 | ] 13 | 14 | it('returns true when there is a four of kind', () => { 15 | expect(fourOfKind(hand).result).toBeTruthy() 16 | }) 17 | 18 | it('shows matched as all four of kind', () => { 19 | expect(fourOfKind(hand).matched).toEqual([ 20 | { suit: 'D', face: '2' }, 21 | { suit: 'S', face: '2' }, 22 | { suit: 'C', face: '2' }, 23 | { suit: 'H', face: '2' } 24 | ]) 25 | }) 26 | 27 | it('shows unmatched as single card', () => { 28 | expect(fourOfKind(hand).unmatched).toEqual([{ suit: 'S', face: '3' }]) 29 | }) 30 | }) 31 | 32 | describe('unmatched', () => { 33 | it('returns false when there is three of kind', () => { 34 | const hand: Hand = [ 35 | { suit: 'S', face: '2' }, 36 | { suit: 'D', face: '2' }, 37 | { suit: 'S', face: '9' }, 38 | { suit: 'D', face: '9' }, 39 | { suit: 'H', face: '9' } 40 | ] 41 | 42 | expect(fourOfKind(hand).result).toBeFalsy() 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /pokerhand-fp/test/ranks/highCards.spec.ts: -------------------------------------------------------------------------------- 1 | import { highCard } from '~/ranks/highCards' 2 | import { Hand } from '~/card' 3 | 4 | describe('highCard', () => { 5 | const hand: Hand = [ 6 | { suit: 'S', face: '5' }, 7 | { suit: 'D', face: '7' }, 8 | { suit: 'S', face: '9' }, 9 | { suit: 'S', face: 'J' }, 10 | { suit: 'S', face: 'A' } 11 | ] 12 | 13 | describe('matches', () => { 14 | it('returns true when there is at least one high card', () => { 15 | expect(highCard(hand).result).toBeTruthy() 16 | }) 17 | 18 | it('shows matched as all high cards sorted by value', () => { 19 | expect(highCard(hand).matched).toEqual([ 20 | { suit: 'S', face: 'A' }, 21 | { suit: 'S', face: 'J' }, 22 | { suit: 'S', face: '9' }, 23 | { suit: 'D', face: '7' }, 24 | { suit: 'S', face: '5' } 25 | ]) 26 | }) 27 | }) 28 | 29 | describe('unmatched', () => { 30 | it('returns false when there is no high card', () => { 31 | expect(highCard([]).result).toBeFalsy() 32 | }) 33 | 34 | it('returns false when it is flush', () => { 35 | const flush: Hand = [ 36 | { suit: 'D', face: '2' }, 37 | { suit: 'D', face: '5' }, 38 | { suit: 'D', face: '7' }, 39 | { suit: 'D', face: '9' }, 40 | { suit: 'D', face: 'Q' } 41 | ] 42 | 43 | expect(highCard(flush).result).toBeFalsy() 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /pokerhand-oop/test/ranks/StraightFlush.spec.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import { Card, Face, Suit } from '~/Card' 3 | 4 | describe('Straight Flush', () => { 5 | const straightFlush1 = new Hand([ 6 | new Card(Face._2, Suit.S), 7 | new Card(Face._3, Suit.S), 8 | new Card(Face._4, Suit.S), 9 | new Card(Face._5, Suit.S), 10 | new Card(Face._6, Suit.S) 11 | ]) 12 | 13 | it('matches as a straight flush', () => { 14 | expect(straightFlush1.rank.name).toEqual('Straight Flush') 15 | }) 16 | 17 | describe('duel', () => { 18 | const fourOfKind1 = new Hand([ 19 | new Card(Face._2, Suit.S), 20 | new Card(Face._2, Suit.C), 21 | new Card(Face._2, Suit.D), 22 | new Card(Face._2, Suit.H), 23 | new Card(Face._8, Suit.S) 24 | ]) 25 | 26 | const straightFlush2 = new Hand([ 27 | new Card(Face._3, Suit.S), 28 | new Card(Face._4, Suit.S), 29 | new Card(Face._5, Suit.S), 30 | new Card(Face._6, Suit.S), 31 | new Card(Face._7, Suit.S) 32 | ]) 33 | 34 | it('straight flush > four of kind', () => { 35 | expect(straightFlush1.duel(fourOfKind1)).toEqual( 36 | 'You win: Straight Flush > Four of a Kind' 37 | ) 38 | }) 39 | 40 | it('straight flush vs straight flush: highest card of straight flush wins', () => { 41 | expect(straightFlush2.duel(straightFlush1)).toEqual( 42 | 'You win: Straight Flush with 7 > 6' 43 | ) 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /pokerhand-fp/test/ranks/fullHouse.spec.ts: -------------------------------------------------------------------------------- 1 | import { fullHouse } from '~/ranks/fullHouse' 2 | import { Hand } from '~/card' 3 | 4 | describe('full house', () => { 5 | describe('matches', () => { 6 | const hand: Hand = [ 7 | { suit: 'S', face: '2' }, 8 | { suit: 'D', face: '2' }, 9 | { suit: 'S', face: '9' }, 10 | { suit: 'D', face: '9' }, 11 | { suit: 'H', face: '9' } 12 | ] 13 | 14 | it('returns true when there is three of kind with a double', () => { 15 | expect(fullHouse(hand).result).toBeTruthy() 16 | }) 17 | 18 | it('shows matched as three of kind cards', () => { 19 | expect(fullHouse(hand).matched).toEqual([ 20 | { suit: 'S', face: '9' }, 21 | { suit: 'D', face: '9' }, 22 | { suit: 'H', face: '9' } 23 | ]) 24 | }) 25 | 26 | it('shows unmatched as the double card', () => { 27 | expect(fullHouse(hand).unmatched).toEqual([ 28 | { suit: 'S', face: '2' }, 29 | { suit: 'D', face: '2' } 30 | ]) 31 | }) 32 | }) 33 | 34 | describe('unmatched', () => { 35 | it('returns false when there is only three of kind with no double', () => { 36 | const hand: Hand = [ 37 | { suit: 'S', face: '2' }, 38 | { suit: 'D', face: '3' }, 39 | { suit: 'S', face: '9' }, 40 | { suit: 'D', face: '9' }, 41 | { suit: 'H', face: '9' } 42 | ] 43 | 44 | expect(fullHouse(hand).result).toBeFalsy() 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /pokerhand-oop/test/ranks/HighCard.spec.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import { Card, Face, Suit } from '~/Card' 3 | 4 | describe('HighCard', () => { 5 | const h = new Hand([ 6 | new Card(Face._2, Suit.S), 7 | new Card(Face._7, Suit.C), 8 | new Card(Face._4, Suit.S), 9 | new Card(Face._6, Suit.S), 10 | new Card(Face._8, Suit.S) 11 | ]) 12 | 13 | it('matches as a high card', () => { 14 | expect(h.rank.name).toEqual('High Card') 15 | }) 16 | 17 | it('does not match straight', () => { 18 | const h = new Hand([ 19 | new Card(Face._2, Suit.S), 20 | new Card(Face._3, Suit.C), 21 | new Card(Face._4, Suit.S), 22 | new Card(Face._5, Suit.S), 23 | new Card(Face._6, Suit.S) 24 | ]) 25 | 26 | expect(h.rank.name).not.toEqual('High Card') 27 | }) 28 | 29 | it('does not match flush', () => { 30 | const h = new Hand([ 31 | new Card(Face._2, Suit.S), 32 | new Card(Face._7, Suit.S), 33 | new Card(Face._4, Suit.S), 34 | new Card(Face._6, Suit.S), 35 | new Card(Face._8, Suit.S) 36 | ]) 37 | 38 | expect(h.rank.name).not.toEqual('High Card') 39 | }) 40 | 41 | describe('duel', () => { 42 | const h2 = new Hand([ 43 | new Card(Face._2, Suit.S), 44 | new Card(Face._7, Suit.C), 45 | new Card(Face._5, Suit.S), 46 | new Card(Face._6, Suit.S), 47 | new Card(Face._8, Suit.S) 48 | ]) 49 | 50 | expect(h2.duel(h)).toEqual('You win: High Card with 5 > 4') 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /pokerhand-fp/test/ranks/pair.spec.ts: -------------------------------------------------------------------------------- 1 | import { pair } from '~/ranks/pair' 2 | import { Hand } from '~/card' 3 | 4 | describe('pair', () => { 5 | describe('matches', () => { 6 | const hand: Hand = [ 7 | { suit: 'D', face: '2' }, 8 | { suit: 'S', face: '7' }, 9 | { suit: 'S', face: '9' }, 10 | { suit: 'S', face: '2' }, 11 | { suit: 'S', face: '3' } 12 | ] 13 | 14 | it('returns true when there is at least one double', () => { 15 | expect(pair(hand).result).toBeTruthy() 16 | }) 17 | 18 | it('shows matched as the matching double', () => { 19 | expect(pair(hand).matched).toEqual([ 20 | { suit: 'D', face: '2' }, 21 | { suit: 'S', face: '2' } 22 | ]) 23 | }) 24 | 25 | it('shows unmatched as the single cards', () => { 26 | expect(pair(hand).unmatched).toEqual([ 27 | { suit: 'S', face: '9' }, 28 | { suit: 'S', face: '7' }, 29 | { suit: 'S', face: '3' } 30 | ]) 31 | }) 32 | }) 33 | 34 | describe('unmatched', () => { 35 | it('returns false when there is no double', () => { 36 | const hand: Hand = [ 37 | { suit: 'S', face: '2' }, 38 | { suit: 'S', face: '7' }, 39 | { suit: 'S', face: '9' }, 40 | { suit: 'S', face: 'A' }, 41 | { suit: 'S', face: '3' } 42 | ] 43 | expect(pair(hand).result).toBeFalsy() 44 | }) 45 | 46 | it('returns false when there is two pairs', () => { 47 | const hand: Hand = [ 48 | { suit: 'S', face: '2' }, 49 | { suit: 'D', face: '2' }, 50 | { suit: 'S', face: '9' }, 51 | { suit: 'D', face: '9' }, 52 | { suit: 'S', face: '3' } 53 | ] 54 | 55 | expect(pair(hand).result).toBeFalsy() 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /pokerhand-fp/test/ranks/straight.spec.ts: -------------------------------------------------------------------------------- 1 | import { straight } from '~/ranks/straight' 2 | import { Hand } from '~/card' 3 | 4 | describe('straight', () => { 5 | describe('matches', () => { 6 | const hand: Hand = [ 7 | { suit: 'D', face: '2' }, 8 | { suit: 'S', face: '3' }, 9 | { suit: 'S', face: '4' }, 10 | { suit: 'H', face: '5' }, 11 | { suit: 'S', face: '6' } 12 | ] 13 | 14 | it('returns true when all cards are in sequence', () => { 15 | expect(straight(hand).result).toBeTruthy() 16 | }) 17 | 18 | it('shows matched as all 5 cards', () => { 19 | expect(straight(hand).matched).toEqual([ 20 | { suit: 'S', face: '6' }, 21 | { suit: 'H', face: '5' }, 22 | { suit: 'S', face: '4' }, 23 | { suit: 'S', face: '3' }, 24 | { suit: 'D', face: '2' } 25 | ]) 26 | }) 27 | 28 | it('shows unmatched as none', () => { 29 | expect(straight(hand).unmatched).toEqual([]) 30 | }) 31 | }) 32 | 33 | describe('unmatched', () => { 34 | it('returns false when one card is not in sequence', () => { 35 | const hand: Hand = [ 36 | { suit: 'D', face: '2' }, 37 | { suit: 'S', face: '3' }, 38 | { suit: 'S', face: '4' }, 39 | { suit: 'H', face: '5' }, 40 | { suit: 'S', face: '7' } 41 | ] 42 | 43 | expect(straight(hand).result).toBeFalsy() 44 | }) 45 | 46 | it('returns false when hand is straight flush', () => { 47 | const hand: Hand = [ 48 | { suit: 'D', face: '2' }, 49 | { suit: 'D', face: '3' }, 50 | { suit: 'D', face: '4' }, 51 | { suit: 'D', face: '5' }, 52 | { suit: 'D', face: '6' } 53 | ] 54 | 55 | expect(straight(hand).result).toBeFalsy() 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /pokerhand-oop/test/ranks/FullHouse.spec.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import { Card, Face, Suit } from '~/Card' 3 | 4 | describe('Full House', () => { 5 | const fullHouse1 = new Hand([ 6 | new Card(Face._2, Suit.S), 7 | new Card(Face._2, Suit.C), 8 | new Card(Face._2, Suit.D), 9 | new Card(Face._4, Suit.C), 10 | new Card(Face._4, Suit.S) 11 | ]) 12 | 13 | it('matches as a full house', () => { 14 | expect(fullHouse1.rank.name).toEqual('Full House') 15 | }) 16 | 17 | describe('duel', () => { 18 | const flush1 = new Hand([ 19 | new Card(Face._2, Suit.C), 20 | new Card(Face._7, Suit.C), 21 | new Card(Face._4, Suit.C), 22 | new Card(Face._A, Suit.C), 23 | new Card(Face._6, Suit.C) 24 | ]) 25 | 26 | const fullHouse2 = new Hand([ 27 | new Card(Face._3, Suit.S), 28 | new Card(Face._3, Suit.C), 29 | new Card(Face._3, Suit.D), 30 | new Card(Face._4, Suit.C), 31 | new Card(Face._4, Suit.S) 32 | ]) 33 | 34 | const fullHouse3 = new Hand([ 35 | new Card(Face._3, Suit.S), 36 | new Card(Face._3, Suit.C), 37 | new Card(Face._3, Suit.D), 38 | new Card(Face._5, Suit.C), 39 | new Card(Face._5, Suit.S) 40 | ]) 41 | 42 | it('full house > flush', () => { 43 | expect(fullHouse1.duel(flush1)).toEqual('You win: Full House > Flush') 44 | }) 45 | 46 | it('full house vs full house: highest card of three of kinds wins', () => { 47 | expect(fullHouse2.duel(fullHouse1)).toEqual( 48 | 'You win: Full House with 3 > 2' 49 | ) 50 | }) 51 | 52 | it('full house vs full house: highest card of pair wins', () => { 53 | expect(fullHouse3.duel(fullHouse2)).toEqual( 54 | 'You win: Full House with 5 > 4' 55 | ) 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /pokerhand-fp/test/ranks/straightFlush.spec.ts: -------------------------------------------------------------------------------- 1 | import { straightFlush } from '~/ranks/straightFlush' 2 | import { Hand } from '~/card' 3 | 4 | describe('straightFlush flush', () => { 5 | describe('matches', () => { 6 | const hand: Hand = [ 7 | { suit: 'D', face: '2' }, 8 | { suit: 'D', face: '3' }, 9 | { suit: 'D', face: '4' }, 10 | { suit: 'D', face: '5' }, 11 | { suit: 'D', face: '6' } 12 | ] 13 | 14 | it('returns true when all cards are in sequence and share the same suit', () => { 15 | expect(straightFlush(hand).result).toBeTruthy() 16 | }) 17 | 18 | it('shows matched as all 5 cards', () => { 19 | expect(straightFlush(hand).matched).toEqual([ 20 | { suit: 'D', face: '6' }, 21 | { suit: 'D', face: '5' }, 22 | { suit: 'D', face: '4' }, 23 | { suit: 'D', face: '3' }, 24 | { suit: 'D', face: '2' } 25 | ]) 26 | }) 27 | 28 | it('shows unmatched as none', () => { 29 | expect(straightFlush(hand).unmatched).toEqual([]) 30 | }) 31 | }) 32 | 33 | describe('unmatched', () => { 34 | it('returns false when one card is not in sequence', () => { 35 | const hand: Hand = [ 36 | { suit: 'D', face: '2' }, 37 | { suit: 'S', face: '3' }, 38 | { suit: 'S', face: '4' }, 39 | { suit: 'H', face: '5' }, 40 | { suit: 'S', face: '7' } 41 | ] 42 | 43 | expect(straightFlush(hand).result).toBeFalsy() 44 | }) 45 | 46 | it('returns false when one card is not in the same suit', () => { 47 | const hand: Hand = [ 48 | { suit: 'D', face: '2' }, 49 | { suit: 'D', face: '3' }, 50 | { suit: 'D', face: '4' }, 51 | { suit: 'D', face: '5' }, 52 | { suit: 'S', face: '6' } 53 | ] 54 | 55 | expect(straightFlush(hand).result).toBeFalsy() 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /pokerhand-fp/test/ranks/threeOfKind.spec.ts: -------------------------------------------------------------------------------- 1 | import { threeOfKind } from '~/ranks/threeOfKind' 2 | import { Hand } from '~/card' 3 | 4 | describe('three of kind', () => { 5 | describe('matches', () => { 6 | const hand: Hand = [ 7 | { suit: 'D', face: '2' }, 8 | { suit: 'S', face: '2' }, 9 | { suit: 'S', face: '9' }, 10 | { suit: 'H', face: '2' }, 11 | { suit: 'S', face: '3' } 12 | ] 13 | 14 | it('returns true when there is at least one three of kind', () => { 15 | expect(threeOfKind(hand).result).toBeTruthy() 16 | }) 17 | 18 | it('shows matched as three of kind cards', () => { 19 | expect(threeOfKind(hand).matched).toEqual([ 20 | { suit: 'D', face: '2' }, 21 | { suit: 'S', face: '2' }, 22 | { suit: 'H', face: '2' } 23 | ]) 24 | }) 25 | 26 | it('shows unmatched as single cards sorted by value', () => { 27 | expect(threeOfKind(hand).unmatched).toEqual([ 28 | { suit: 'S', face: '9' }, 29 | { suit: 'S', face: '3' } 30 | ]) 31 | }) 32 | }) 33 | 34 | describe('unmatched', () => { 35 | it('returns false when there is no three of kind', () => { 36 | const hand: Hand = [ 37 | { suit: 'S', face: '2' }, 38 | { suit: 'S', face: '7' }, 39 | { suit: 'S', face: '9' }, 40 | { suit: 'S', face: 'A' }, 41 | { suit: 'S', face: '3' } 42 | ] 43 | 44 | expect(threeOfKind(hand).result).toBeFalsy() 45 | }) 46 | 47 | it('returns false when there is three of kind with a double', () => { 48 | const hand: Hand = [ 49 | { suit: 'S', face: '2' }, 50 | { suit: 'D', face: '2' }, 51 | { suit: 'S', face: '9' }, 52 | { suit: 'D', face: '9' }, 53 | { suit: 'H', face: '9' } 54 | ] 55 | 56 | expect(threeOfKind(hand).result).toBeFalsy() 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /pokerhand-oop/test/ranks/FourOfAKind.spec.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import { Card, Face, Suit } from '~/Card' 3 | 4 | describe('Four of a Kind', () => { 5 | const fourOfKind1 = new Hand([ 6 | new Card(Face._2, Suit.S), 7 | new Card(Face._2, Suit.C), 8 | new Card(Face._2, Suit.D), 9 | new Card(Face._2, Suit.H), 10 | new Card(Face._8, Suit.S) 11 | ]) 12 | 13 | it('matches as a four of a kind', () => { 14 | expect(fourOfKind1.rank.name).toEqual('Four of a Kind') 15 | }) 16 | 17 | describe('duel', () => { 18 | const fullHouse1 = new Hand([ 19 | new Card(Face._2, Suit.S), 20 | new Card(Face._2, Suit.C), 21 | new Card(Face._2, Suit.D), 22 | new Card(Face._4, Suit.C), 23 | new Card(Face._4, Suit.S) 24 | ]) 25 | 26 | const fourOfKind2 = new Hand([ 27 | new Card(Face._3, Suit.S), 28 | new Card(Face._3, Suit.C), 29 | new Card(Face._3, Suit.D), 30 | new Card(Face._3, Suit.H), 31 | new Card(Face._8, Suit.S) 32 | ]) 33 | 34 | const fourOfKind3 = new Hand([ 35 | new Card(Face._3, Suit.S), 36 | new Card(Face._3, Suit.C), 37 | new Card(Face._3, Suit.D), 38 | new Card(Face._3, Suit.H), 39 | new Card(Face._9, Suit.S) 40 | ]) 41 | 42 | it('four of kind > full house', () => { 43 | expect(fourOfKind1.duel(fullHouse1)).toEqual( 44 | 'You win: Four of a Kind > Full House' 45 | ) 46 | }) 47 | 48 | it('four of kind vs four of kind: highest card of four of kinds wins', () => { 49 | expect(fourOfKind2.duel(fourOfKind1)).toEqual( 50 | 'You win: Four of a Kind with 3 > 2' 51 | ) 52 | }) 53 | 54 | it('four of kind vs four of kind: highest card of pair wins', () => { 55 | expect(fourOfKind3.duel(fourOfKind2)).toEqual( 56 | 'You win: Four of a Kind with 9 > 8' 57 | ) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /pokerhand-oop/test/ranks/ThreeOfAKind.spec.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import { Card, Face, Suit } from '~/Card' 3 | 4 | describe('Three of a Kind', () => { 5 | const threeOfAKind1 = new Hand([ 6 | new Card(Face._2, Suit.S), 7 | new Card(Face._2, Suit.C), 8 | new Card(Face._2, Suit.D), 9 | new Card(Face._4, Suit.C), 10 | new Card(Face._8, Suit.S) 11 | ]) 12 | 13 | it('matches as a three of a kind', () => { 14 | expect(threeOfAKind1.rank.name).toEqual('Three of a Kind') 15 | }) 16 | 17 | describe('duel', () => { 18 | const twoPair1 = new Hand([ 19 | new Card(Face._2, Suit.S), 20 | new Card(Face._2, Suit.C), 21 | new Card(Face._4, Suit.S), 22 | new Card(Face._4, Suit.C), 23 | new Card(Face._8, Suit.S) 24 | ]) 25 | 26 | const threeOfAKind2 = new Hand([ 27 | new Card(Face._3, Suit.S), 28 | new Card(Face._3, Suit.C), 29 | new Card(Face._3, Suit.D), 30 | new Card(Face._4, Suit.C), 31 | new Card(Face._8, Suit.S) 32 | ]) 33 | 34 | const threeOfAKind3 = new Hand([ 35 | new Card(Face._3, Suit.S), 36 | new Card(Face._3, Suit.C), 37 | new Card(Face._3, Suit.D), 38 | new Card(Face._5, Suit.C), 39 | new Card(Face._8, Suit.S) 40 | ]) 41 | 42 | it('three of kind > two pairs', () => { 43 | expect(threeOfAKind1.duel(twoPair1)).toEqual( 44 | 'You win: Three of a Kind > Two Pairs' 45 | ) 46 | }) 47 | 48 | it('three of kind vs three of kind: highest card of three of a kind wins', () => { 49 | expect(threeOfAKind2.duel(threeOfAKind1)).toEqual( 50 | 'You win: Three of a Kind with 3 > 2' 51 | ) 52 | }) 53 | 54 | it('three of kind vs three of kind: same highest pair, highest high card wins', () => { 55 | expect(threeOfAKind3.duel(threeOfAKind2)).toEqual( 56 | 'You win: Three of a Kind with 5 > 4' 57 | ) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /pokerhand-oop/src/matchers/Matchers.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '~/matchers/Matcher' 2 | import { CompositeMatcher } from '~/matchers/composites/CompositeMatcher' 3 | import { FaceMatcher } from '~/matchers/composites/FaceMatcher' 4 | import { Group } from '~/Group' 5 | import { SuitMatcher } from '~/matchers/composites/SuitMatcher' 6 | import { RepeatMoreThanMatcher } from '~/matchers/repeats/RepeatMoreThanMatcher' 7 | import { ConsecutiveMatcher } from '~/matchers/ConsecutiveMatcher' 8 | import { RepeatEqualMatcher } from '~/matchers/repeats/RepeatEqualMatcher' 9 | import { Card } from '~/Card' 10 | import { Hand } from '~/Hand' 11 | 12 | const repeating = (n: number) => (cs: Card[]) => cs.length === n 13 | 14 | export class Matchers { 15 | static compose(a: Matcher, ...bs: Matcher[]) { 16 | return new CompositeMatcher(a, bs) 17 | } 18 | 19 | static faces(primary: Matcher, ...secondary: Matcher[]): Matcher { 20 | return new FaceMatcher(primary, secondary) 21 | } 22 | 23 | static suits(primary: Matcher, ...secondary: Matcher[]): Matcher { 24 | return new SuitMatcher(primary, secondary) 25 | } 26 | 27 | static consecutive() { 28 | return new ConsecutiveMatcher(true) 29 | } 30 | 31 | static notConsecutive() { 32 | return new ConsecutiveMatcher(false) 33 | } 34 | 35 | static moreThanOne() { 36 | return new RepeatMoreThanMatcher(1) 37 | } 38 | 39 | static single(count) { 40 | return new RepeatEqualMatcher(repeating(1), count) 41 | } 42 | 43 | static double(count) { 44 | return new RepeatEqualMatcher(repeating(2), count) 45 | } 46 | 47 | static triple(count) { 48 | return new RepeatEqualMatcher(repeating(3), count) 49 | } 50 | 51 | static quad(count) { 52 | return new RepeatEqualMatcher(repeating(4), count) 53 | } 54 | 55 | static quint(count) { 56 | return new RepeatEqualMatcher(repeating(5), count) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pokerhand-oop/src/ranks/Ranks.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import { Matchers } from '~/matchers/Matchers' 3 | import { Rank } from '~/ranks/Rank' 4 | 5 | export class Ranks { 6 | static create(hand: Hand): Rank { 7 | // @ts-ignore 8 | return registry.find(r => r.matches(hand)) 9 | } 10 | } 11 | 12 | const highCard = new Rank( 13 | 'High Card', 14 | 1, 15 | Matchers.compose( 16 | Matchers.faces(Matchers.single(5)), 17 | Matchers.suits(Matchers.moreThanOne()), 18 | Matchers.notConsecutive() 19 | ) 20 | ) 21 | 22 | const pair = new Rank( 23 | 'Pair', 24 | 2, 25 | Matchers.faces(Matchers.double(1), Matchers.single(3)) 26 | ) 27 | 28 | const twoPairs = new Rank( 29 | 'Two Pairs', 30 | 3, 31 | Matchers.faces(Matchers.double(2), Matchers.single(1)) 32 | ) 33 | 34 | const threeOfAKind = new Rank( 35 | 'Three of a Kind', 36 | 4, 37 | Matchers.faces(Matchers.triple(1), Matchers.single(2)) 38 | ) 39 | 40 | const straight = new Rank( 41 | 'Straight', 42 | 5, 43 | Matchers.compose( 44 | Matchers.consecutive(), 45 | Matchers.suits(Matchers.moreThanOne()) 46 | ) 47 | ) 48 | 49 | const flush = new Rank( 50 | 'Flush', 51 | 6, 52 | Matchers.compose( 53 | Matchers.suits(Matchers.quint(1)), 54 | Matchers.notConsecutive() 55 | ) 56 | ) 57 | 58 | const fullHouse = new Rank( 59 | 'Full House', 60 | 7, 61 | Matchers.faces(Matchers.triple(1), Matchers.double(1)) 62 | ) 63 | 64 | const fourOfAKind = new Rank( 65 | 'Four of a Kind', 66 | 8, 67 | Matchers.faces(Matchers.quad(1), Matchers.single(1)) 68 | ) 69 | 70 | const straightFlush = new Rank( 71 | 'Straight Flush', 72 | 9, 73 | Matchers.compose( 74 | Matchers.consecutive(), 75 | Matchers.suits(Matchers.quint(1)) 76 | ) 77 | ) 78 | 79 | export const registry = [ 80 | highCard, 81 | pair, 82 | twoPairs, 83 | threeOfAKind, 84 | flush, 85 | straight, 86 | fullHouse, 87 | fourOfAKind, 88 | straightFlush 89 | ] 90 | -------------------------------------------------------------------------------- /pokerhand-oop/test/ranks/Pair.spec.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import { Card, Face, Suit } from '~/Card' 3 | 4 | describe('Pair', () => { 5 | const pair1 = new Hand([ 6 | new Card(Face._2, Suit.S), 7 | new Card(Face._2, Suit.C), 8 | new Card(Face._4, Suit.S), 9 | new Card(Face._6, Suit.S), 10 | new Card(Face._8, Suit.S) 11 | ]) 12 | 13 | it('matches as a pair', () => { 14 | expect(pair1.rank.name).toEqual('Pair') 15 | }) 16 | 17 | describe('duel', () => { 18 | const highCard = new Hand([ 19 | new Card(Face._2, Suit.S), 20 | new Card(Face._7, Suit.C), 21 | new Card(Face._4, Suit.S), 22 | new Card(Face._6, Suit.S), 23 | new Card(Face._8, Suit.S) 24 | ]) 25 | 26 | const pair2 = new Hand([ 27 | new Card(Face._3, Suit.S), 28 | new Card(Face._3, Suit.C), 29 | new Card(Face._4, Suit.S), 30 | new Card(Face._6, Suit.S), 31 | new Card(Face._8, Suit.S) 32 | ]) 33 | 34 | const pair3 = new Hand([ 35 | new Card(Face._3, Suit.S), 36 | new Card(Face._3, Suit.C), 37 | new Card(Face._4, Suit.S), 38 | new Card(Face._6, Suit.S), 39 | new Card(Face._9, Suit.S) 40 | ]) 41 | 42 | it('pair > high Card', () => { 43 | expect(pair1.duel(highCard)).toEqual('You win: Pair > High Card') 44 | expect(highCard.duel(pair1)).toEqual('You lose: High Card < Pair') 45 | }) 46 | 47 | it('pair of 2 vs pair of 3: higher pair wins', () => { 48 | expect(pair2.duel(pair1)).toEqual('You win: Pair with 3 > 2') 49 | expect(pair1.duel(pair2)).toEqual('You lose: Pair with 2 < 3') 50 | }) 51 | 52 | it('pair of 3 vs pair of 3: higher high card wins', () => { 53 | expect(pair3.duel(pair2)).toEqual('You win: Pair with 9 > 8') 54 | expect(pair2.duel(pair3)).toEqual('You lose: Pair with 8 < 9') 55 | }) 56 | 57 | it('pair vs pair: Tie', () => { 58 | expect(pair1.duel(pair1)).toEqual('Tie') 59 | expect(pair2.duel(pair2)).toEqual('Tie') 60 | expect(pair3.duel(pair3)).toEqual('Tie') 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /pokerhand-oop/test/ranks/TwoPairs.spec.ts: -------------------------------------------------------------------------------- 1 | import { Hand } from '~/Hand' 2 | import { Card, Face, Suit } from '~/Card' 3 | 4 | describe('Two Pairs', () => { 5 | const twoPair1 = new Hand([ 6 | new Card(Face._2, Suit.S), 7 | new Card(Face._2, Suit.C), 8 | new Card(Face._4, Suit.S), 9 | new Card(Face._4, Suit.C), 10 | new Card(Face._8, Suit.S) 11 | ]) 12 | 13 | it('matches as two pairs', () => { 14 | expect(twoPair1.rank.name).toEqual('Two Pairs') 15 | }) 16 | 17 | describe('duel', () => { 18 | const pair = new Hand([ 19 | new Card(Face._2, Suit.S), 20 | new Card(Face._2, Suit.C), 21 | new Card(Face._4, Suit.S), 22 | new Card(Face._6, Suit.S), 23 | new Card(Face._8, Suit.S) 24 | ]) 25 | 26 | const twoPair2 = new Hand([ 27 | new Card(Face._2, Suit.S), 28 | new Card(Face._2, Suit.C), 29 | new Card(Face._5, Suit.S), 30 | new Card(Face._5, Suit.C), 31 | new Card(Face._8, Suit.S) 32 | ]) 33 | 34 | const twoPair3 = new Hand([ 35 | new Card(Face._3, Suit.S), 36 | new Card(Face._3, Suit.C), 37 | new Card(Face._5, Suit.S), 38 | new Card(Face._5, Suit.C), 39 | new Card(Face._8, Suit.S) 40 | ]) 41 | 42 | const twoPair4 = new Hand([ 43 | new Card(Face._3, Suit.S), 44 | new Card(Face._3, Suit.C), 45 | new Card(Face._5, Suit.S), 46 | new Card(Face._5, Suit.C), 47 | new Card(Face._9, Suit.S) 48 | ]) 49 | 50 | it('two pairs > pair', () => { 51 | expect(twoPair1.duel(pair)).toEqual('You win: Two Pairs > Pair') 52 | }) 53 | 54 | it('two pairs vs two pairs: highest two pairs wins', () => { 55 | expect(twoPair2.duel(twoPair1)).toEqual('You win: Two Pairs with 5 > 4') 56 | }) 57 | 58 | it('two pairs vs two pairs: highest two pairs wins', () => { 59 | expect(twoPair3.duel(twoPair2)).toEqual('You win: Two Pairs with 3 > 2') 60 | }) 61 | 62 | it('two pairs vs two pairs: same highest pair, highest high card wins', () => { 63 | expect(twoPair4.duel(twoPair3)).toEqual('You win: Two Pairs with 9 > 8') 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /pokerhand-oop/src/Hand.ts: -------------------------------------------------------------------------------- 1 | import { Card } from '~/Card' 2 | import { Group } from '~/Group' 3 | import { Rank } from '~/ranks/Rank' 4 | import { Ranks } from '~/ranks/Ranks' 5 | 6 | export class Hand { 7 | private readonly _cards: Card[] 8 | 9 | constructor(cards: Card[]) { 10 | this._cards = cards.sort(Card.comparable) 11 | } 12 | 13 | get rank(): Rank { 14 | // @ts-ignore 15 | return Ranks.create(this) 16 | } 17 | 18 | get matchedRank() { 19 | return this.rank.matcher.matched(this) 20 | } 21 | 22 | get unMatchedRank() { 23 | return this.rank.matcher.unmatched(this) 24 | } 25 | 26 | groupByValue(): Group { 27 | return Group.groupByValue(this) 28 | } 29 | 30 | groupBySuit(): Group { 31 | return Group.groupBySuit(this) 32 | } 33 | 34 | get isConsecutive(): boolean { 35 | for (let i = 0; i < this.cards.length - 1; i++) { 36 | if (this.cards[i].face - 1 !== this.cards[i + 1].face) return false 37 | } 38 | return true 39 | } 40 | 41 | get cards() { 42 | return this._cards 43 | } 44 | 45 | get size() { 46 | return this.cards.length 47 | } 48 | 49 | duel(h: Hand) { 50 | const plan = [ 51 | () => this.rank.duel(h.rank), 52 | () => this.matchedRank.duelHighCard(h.rank.name, h.matchedRank), 53 | () => this.unMatchedRank.duelHighCard(h.rank.name, h.unMatchedRank), 54 | 'Tie' 55 | ] 56 | 57 | return this.foldUntilFound(plan) 58 | } 59 | 60 | private duelHighCard(rankName: string, h: Hand) { 61 | for (let i = 0; i < this.size; i++) { 62 | if (this.cards[i].face > h.cards[i].face) { 63 | return `You win: ${rankName} with ${this.cards[i].face} > ${h.cards[i].face}` 64 | } else if (this.cards[i].face < h.cards[i].face) { 65 | return `You lose: ${rankName} with ${this.cards[i].face} < ${h.cards[i].face}` 66 | } 67 | } 68 | return undefined 69 | } 70 | 71 | private foldUntilFound(plan) { 72 | return plan.slice(0).reduce((acc, n, i, arr) => { 73 | // @ts-ignore 74 | const result = acc() 75 | if (result) { 76 | arr.splice(i) 77 | return result 78 | } 79 | return n 80 | }) 81 | } 82 | } 83 | 84 | [''] 85 | -------------------------------------------------------------------------------- /pokerhand-fp/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/k0/prspr16x7pn_vg_xwc7vwt9h0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names that allow to stub out resources with a single module 82 | moduleNameMapper: { 83 | '^~/(.*)$': '/src/$1', 84 | '^~~/(.*)$': '/test/$1', 85 | }, 86 | 87 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 88 | // modulePathIgnorePatterns: [], 89 | 90 | // Activates notifications for test results 91 | // notify: false, 92 | 93 | // An enum that specifies notification mode. Requires { notify: true } 94 | // notifyMode: "failure-change", 95 | 96 | // A preset that is used as a base for Jest's configuration 97 | // preset: null, 98 | 99 | // Run tests from one or more projects 100 | // projects: null, 101 | 102 | // Use this configuration option to add custom reporters to Jest 103 | // reporters: undefined, 104 | 105 | // Automatically reset mock state between every test 106 | // resetMocks: false, 107 | 108 | // Reset the module registry before running each individual test 109 | // resetModules: false, 110 | 111 | // A path to a custom resolver 112 | // resolver: null, 113 | 114 | // Automatically restore mock state between every test 115 | // restoreMocks: false, 116 | 117 | // The root directory that Jest should scan for tests and modules within 118 | // rootDir: null, 119 | 120 | // A list of paths to directories that Jest should use to search for files in 121 | // roots: [ 122 | // "" 123 | // ], 124 | 125 | // Allows you to use a custom runner instead of Jest's default test runner 126 | // runner: "jest-runner", 127 | 128 | // The paths to modules that run some code to configure or set up the testing environment before each test 129 | // setupFiles: [], 130 | 131 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 132 | // setupFilesAfterEnv: [], 133 | 134 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 135 | // snapshotSerializers: [], 136 | 137 | // The test environment that will be used for testing 138 | // testEnvironment: "jest-environment-jsdom", 139 | 140 | // Options that will be passed to the testEnvironment 141 | // testEnvironmentOptions: {}, 142 | 143 | // Adds a location field to test results 144 | // testLocationInResults: false, 145 | 146 | // The glob patterns Jest uses to detect test files 147 | // testMatch: [ 148 | // "**/__tests__/**/*.[jt]s?(x)", 149 | // "**/?(*.)+(spec|test).[tj]s?(x)" 150 | // ], 151 | 152 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 153 | // testPathIgnorePatterns: [ 154 | // "/node_modules/" 155 | // ], 156 | 157 | // The regexp pattern or array of patterns that Jest uses to detect test files 158 | // testRegex: [], 159 | 160 | // This option allows the use of a custom results processor 161 | // testResultsProcessor: null, 162 | 163 | // This option allows use of a custom test runner 164 | // testRunner: "jasmine2", 165 | 166 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 167 | // testURL: "http://localhost", 168 | 169 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 170 | // timers: "real", 171 | 172 | // A map from regular expressions to paths to transformers 173 | // transform: null, 174 | 175 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 176 | // transformIgnorePatterns: [ 177 | // "/node_modules/" 178 | // ], 179 | 180 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 181 | // unmockedModulePathPatterns: undefined, 182 | 183 | // Indicates whether each individual test should be reported during the run 184 | // verbose: null, 185 | 186 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 187 | // watchPathIgnorePatterns: [], 188 | 189 | // Whether to use watchman for file crawling 190 | // watchman: true, 191 | }; 192 | -------------------------------------------------------------------------------- /pokerhand-oop/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/k0/prspr16x7pn_vg_xwc7vwt9h0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before registry test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after registry test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in registry test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names that allow to stub out resources with a single module 82 | moduleNameMapper: { 83 | '^~/(.*)$': '/src/$1', 84 | '^~~/(.*)$': '/test/$1', 85 | }, 86 | 87 | // An array of regexp pattern strings, matched against registry module paths before considered 'visible' to the module loader 88 | // modulePathIgnorePatterns: [], 89 | 90 | // Activates notifications for test results 91 | // notify: false, 92 | 93 | // An enum that specifies notification mode. Requires { notify: true } 94 | // notifyMode: "failure-change", 95 | 96 | // A preset that is used as a base for Jest's configuration 97 | // preset: null, 98 | 99 | // Run tests from one or more projects 100 | // projects: null, 101 | 102 | // Use this configuration option to add custom reporters to Jest 103 | // reporters: undefined, 104 | 105 | // Automatically reset mock state between every test 106 | // resetMocks: false, 107 | 108 | // Reset the module registry before running each individual test 109 | // resetModules: false, 110 | 111 | // A path to a custom resolver 112 | // resolver: null, 113 | 114 | // Automatically restore mock state between every test 115 | // restoreMocks: false, 116 | 117 | // The root directory that Jest should scan for tests and modules within 118 | // rootDir: null, 119 | 120 | // A list of paths to directories that Jest should use to search for files in 121 | // roots: [ 122 | // "" 123 | // ], 124 | 125 | // Allows you to use a custom runner instead of Jest's default test runner 126 | // runner: "jest-runner", 127 | 128 | // The paths to modules that run some code to configure or set up the testing environment before each test 129 | // setupFiles: [], 130 | 131 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 132 | // setupFilesAfterEnv: [], 133 | 134 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 135 | // snapshotSerializers: [], 136 | 137 | // The test environment that will be used for testing 138 | // testEnvironment: "jest-environment-jsdom", 139 | 140 | // Options that will be passed to the testEnvironment 141 | // testEnvironmentOptions: {}, 142 | 143 | // Adds a location field to test results 144 | // testLocationInResults: false, 145 | 146 | // The glob patterns Jest uses to detect test files 147 | // testMatch: [ 148 | // "**/__tests__/**/*.[jt]s?(x)", 149 | // "**/?(*.)+(spec|test).[tj]s?(x)" 150 | // ], 151 | 152 | // An array of regexp pattern strings that are matched against registry test paths, matched tests are skipped 153 | // testPathIgnorePatterns: [ 154 | // "/node_modules/" 155 | // ], 156 | 157 | // The regexp pattern or array of patterns that Jest uses to detect test files 158 | // testRegex: [], 159 | 160 | // This option allows the use of a custom results processor 161 | // testResultsProcessor: null, 162 | 163 | // This option allows use of a custom test runner 164 | // testRunner: "jasmine2", 165 | 166 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 167 | // testURL: "http://localhost", 168 | 169 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 170 | // timers: "real", 171 | 172 | // A map from regular expressions to paths to transformers 173 | // transform: null, 174 | 175 | // An array of regexp pattern strings that are matched against registry source file paths, matched files will skip transformation 176 | // transformIgnorePatterns: [ 177 | // "/node_modules/" 178 | // ], 179 | 180 | // An array of regexp pattern strings that are matched against registry modules before the module loader will automatically return a mock for them 181 | // unmockedModulePathPatterns: undefined, 182 | 183 | // Indicates whether each individual test should be reported during the run 184 | // verbose: null, 185 | 186 | // An array of regexp patterns that are matched against registry source file paths before re-running tests in watch mode 187 | // watchPathIgnorePatterns: [], 188 | 189 | // Whether to use watchman for file crawling 190 | // watchman: true, 191 | }; 192 | --------------------------------------------------------------------------------