type-challenges / type-challenges

Collection of TypeScript type challenges with online judge

Home Page:https://tsch.js.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Classic 13 Coins Problem

teamchong opened this issue · comments

Please follow the template and fill the info. A PR will be auto-generated and always reflect on your changes.

Detailed solution/guide is not required, but please be sure the challenge is solvable.

Info

Basic info of your challenge questions,

difficulty: extreme
title: Classic 13 Coins Problem
tags: application, game, puzzle # separate by comma

Question

You are given 13 coins. All of them weigh the same except for one, which is either heavier or lighter.

You also have a balance scale that allows you to compare the weights of coins on its two sides.

Your task is to determine which coin is the one with a different weight and whether it is heavier or lighter using no more than 3 weighings.

You will use the Weigh1, Weigh2, and Weigh3 constants to represent each weighing step. Each constant contains the setup for the respective weighing step based on the outcome of the previous weighing.

Additionally, the WeighingOutcome type will represent the result of each weighing, indicating which side is heavier or if both sides are equal.

The test cases will call Weigh1, Weigh2, and Weigh3 to simulate the weighing steps and verify the solution.

The printFakeCoin function demonstrates how to use these constants in real JS runtime, performing real-time type validation on function parameters based on complex logic.

For more insight into balance puzzles, you can refer to this Wikipedia article.

Template

This is the template for challengers to start the coding. Basically, you just need to change the name of your generic/function and leave to implementation any.

type Coins = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13

// Defines weighing setup to systematically compare two groups of coins
type WeighingSetup1 = {Left: readonly Coins[], Right: readonly Coins[]}
type WeighingSetup2 = {HeavierOnLeft: WeighingSetup1, HeavierOnRight: WeighingSetup1, EqualWeight: WeighingSetup1}
type WeighingSetup3 = {HeavierOnLeft: WeighingSetup2, HeavierOnRight: WeighingSetup2, EqualWeight: WeighingSetup2}

/**
 * Below is the templates to get start
 */

const Weigh1 = {
  Left: [1, 2, 3, 4, 5, 6], Right: [7, 8, 9, 10, 11, 12]
} as const satisfies WeighingSetup1
/**
 * Weigh1: Initial weighing setup
 * 
 *         Left Side                         Right Side
 *        1  2  3  4  5  6               7  8  9  10  11  12
 * [===========================]    [===========================]
 *              [O]                              [O]
 *                               [O]
 * 
 * This const is used to explain how the first weighing setup works.
 * Coins 1 to 6 are placed on the left side of the scale,
 * and coins 7 to 12 are placed on the right side of the scale.
 * Coin 13 is not included in this weighing.
 **/

const Weigh2 = {
  HeavierOnLeft: {Left: [1, 2, 3], Right: [4, 5, 6]},
  HeavierOnRight: {Left: [7, 8, 9], Right: [10, 11, 12]},
  EqualWeight: {Left: [1], Right: [13]},
} as const satisfies WeighingSetup2
/**
 * Weigh2: Determines the second weighing setup based on the outcome of the first weighing
 *
 * If the left side is heavier:
 *    { Left: [1, 2, 3], Right: [4, 5, 6] }
 * If the right side is heavier:
 *    { Left: [7, 8, 9], Right: [10, 11, 12] }
 * If both sides are equal:
 *    { Left: [1], Right: [13] }
 */

const Weigh3 = {
  HeavierOnLeft: {
    HeavierOnLeft: {Left: [1], Right: [2]},
    HeavierOnRight: {Left: [4], Right: [5]},
    EqualWeight: {Left: [], Right: []}
  },
  HeavierOnRight: {
    HeavierOnLeft: {Left: [7], Right: [8]},
    HeavierOnRight: {Left: [10], Right: [11]},
    EqualWeight: {Left: [], Right: []}
  },
  EqualWeight: {
    HeavierOnLeft: {Left: [], Right: []},
    HeavierOnRight: {Left: [], Right: []},
    EqualWeight: {Left: [], Right: []}
  }
} as const satisfies WeighingSetup3
/**
 * Weigh3: Determines the third weighing setup based on the outcome of the first and second weighings
 *
 * If Outcome1 indicates the left side is heavier:
 *    - If Outcome2 also indicates the left side is heavier:
 *        { Left: [1], Right: [2] }
 *    - If Outcome2 indicates the right side is heavier:
 *        { Left: [4], Right: [5] }
 *    - Otherwise:
 *        { Left: [], Right: [] }
 *
 * If Outcome1 indicates the right side is heavier:
 *    - If Outcome2 indicates the left side is heavier:
 *        { Left: [7], Right: [8] }
 *    - If Outcome2 also indicates the right side is heavier:
 *        { Left: [10], Right: [11] }
 *    - Otherwise:
 *        { Left: [], Right: [] }
 *
 * If Outcome1 indicates both sides are equal:
 *    - If Outcome2 indicates the left side is heavier:
 *        { Left: [], Right: [] }
 *    - If Outcome2 indicates the right side is heavier:
 *        { Left: [], Right: [] }
 *    - Otherwise:
 *        { Left: [], Right: [] }
 */

/**
 * Instructions to Solve the Puzzle:
 *
 * 1. Check value of GameOutcome1, GameOutcome2, GameOutcome3 to find out why it is not solved
 *
 * 2. Why It Is Not Working:
 *    - The above template assumes the fake coin is heavier than the real coin. This assumption might not always hold true.
 *
 * 3. Guide to Revise All Three Weigh Types:
 *    - `Weigh1`, `Weigh2`, `Weigh3`: Ensure that the weighing setup is designed so that no matter which coin is fake, it can be identified.
 *    - Coins not appearing in previous weighing setups can also be used.
 */
printFakeCoin(Weigh1, Weigh2, Weigh3)

/* Mouseover `GameOutcome1` to view Outcome 1 */
export type GameOutcome1 = PrintOutcome1<typeof Weigh1, typeof Weigh2, typeof Weigh3>
/* Mouseover `GameOutcome2` to view Outcome 2 */
export type GameOutcome2 = PrintOutcome2<typeof Weigh1, typeof Weigh2, typeof Weigh3>
/* Mouseover `GameOutcome3` to view Outcome 3 */
export type GameOutcome3 = PrintOutcome3<typeof Weigh1, typeof Weigh2, typeof Weigh3>

Test Cases

Provide some test cases for your challenge, you can use some utils from @type-challenges/utils for asserting.

import type { Equal, Expect } from '@type-challenges/utils'

type UnidentifiedFakeCoins = FindUnidentifiedFakeCoins<typeof Weigh1, typeof Weigh2, typeof Weigh3>
type UnknownFakeCoinStatuses = FindUnknownFakeCoinStatuses<typeof Weigh1, typeof Weigh2, typeof Weigh3>

type cases = [
  Expect<Equal<UnidentifiedFakeCoins, never>>,
  Expect<Equal<HasMoreCoinsThan<UnknownFakeCoinStatuses, 1>, false>>,
]

declare function printFakeCoin<
  Setup1 extends WeighingSetup1, Setup2 extends WeighingSetup2, Setup3 extends WeighingSetup3,
  UnidentifiedFakeCoins extends [Coins, FakeCoinStatuses] = FindUnidentifiedFakeCoins<Setup1, Setup2, Setup3>,
  UnknownFakeCoinStatuses extends Coins = FindUnknownFakeCoinStatuses<Setup1, Setup2, Setup3>,
  Unidentified extends Coins = [UnidentifiedFakeCoins] extends [never] ? HasMoreCoinsThan<UnknownFakeCoinStatuses, 1> extends false ? never : UnknownFakeCoinStatuses : UnidentifiedFakeCoins[0]
  >(
    s1: Setup1, s2: Setup2,
    s3: [Unidentified] extends [never] ? Setup3 : {error: `Unidentified: ${Unidentified}`}
  ): void

// Defines possible outcomes to determine the weight relationship between two groups of coins
type WeighingOutcome = 'HeavierOnLeft' | 'HeavierOnRight' | 'EqualWeight'
type WeighingResult = {Heavier: Coins, Lighter: Coins, Equal: Coins}

type FakeCoinStatuses = 'Heavier' | 'Lighter'

type DetermineOutcome<Setup extends WeighingSetup1, FakeCoin extends Coins, FakeCoinStatus extends FakeCoinStatuses>
  = [Setup] extends [never] ? never
  : IsCoinsValid<Setup['Left'], Setup['Right']> extends false ? never
  : HasMoreCoinsThan<Setup['Left'][number], Setup['Right'][number]> extends true ? 'HeavierOnLeft'
  : HasMoreCoinsThan<Setup['Right'][number], Setup['Left'][number]> extends true ? 'HeavierOnRight'
  : [FakeCoin, FakeCoinStatus] extends [Setup['Left'][number], 'Heavier'] | [Setup['Right'][number], 'Lighter'] ? 'HeavierOnLeft'
  : [FakeCoin, FakeCoinStatus] extends [Setup['Right'][number], 'Heavier'] | [Setup['Left'][number], 'Lighter'] ? 'HeavierOnRight'
  : 'EqualWeight'

type DetermineResult<Setup extends WeighingSetup1, FakeCoin extends Coins, FakeCoinStatus extends FakeCoinStatuses>
  = [Setup] extends [never] ? never
  : IsCoinsValid<Setup['Left'], Setup['Right']> extends false ? never
  : HasMoreCoinsThan<Setup['Left'][number], Setup['Right'][number]> extends true ? {Heavier: Setup['Left'][number], Lighter: Setup['Right'][number], Equal: never}
  : HasMoreCoinsThan<Setup['Right'][number], Setup['Left'][number]> extends true ? {Heavier: Setup['Right'][number], Lighter: Setup['Left'][number], Equal: never}
  : [FakeCoin, FakeCoinStatus] extends [Setup['Left'][number], 'Heavier'] | [Setup['Right'][number], 'Lighter'] ? {Heavier: Setup['Left'][number], Lighter: Setup['Right'][number], Equal: never}
  : [FakeCoin, FakeCoinStatus] extends [Setup['Right'][number], 'Heavier'] | [Setup['Left'][number], 'Lighter'] ? {Heavier: Setup['Right'][number], Lighter: Setup['Left'][number], Equal: never}
  : {Heavier: never, Lighter: never, Equal: Setup['Left'][number] | Setup['Right'][number]}

type IsCoinsValid<Left extends readonly Coins[], Right extends readonly Coins[]>
  = number extends Left | Right ? false
  : [ keyof {[I in keyof Left & `${number}` as keyof {[J in Exclude<keyof Left & `${number}`, I> as Left[I] extends Left[J] ? J : never]: J}]: I} | 
      keyof {[I in keyof Right & `${number}` as keyof {[J in Exclude<keyof Right & `${number}`, I> as Right[I] extends Right[J] ? J : never]: J}]: I}
  ] extends [never]
    ? [Left[number] & Right[number]] extends [never] ? true
    : false
  : false

type HasMoreCoinsThan<A extends Coins, B extends Coins>
  = [A, B] extends [never, never] ? false
  : [A] extends [never] ? false
  : [B] extends [never] ? true
  : true extends {[X in A]: {[Y in B]: HasMoreCoinsThan<Exclude<A, X>, Exclude<B, Y>>}[B]}[A] ? true : false

type OneCoinOrNever<LeftOrRight extends Coins, U extends Coins = LeftOrRight>
  = [U] extends [never] ? never
  : [U extends U ? Exclude<LeftOrRight, U> : never] extends [never] ? U
  : never

type FindUnidentifiedFakeCoins<Setup1 extends WeighingSetup1, Setup2 extends WeighingSetup2, Setup3 extends WeighingSetup3>
  = {[FakeCoin in Coins]: {[FakeCoinStatus in FakeCoinStatuses]
      : DetermineOutcome<Setup1, FakeCoin, FakeCoinStatus> extends infer Outcome1 extends WeighingOutcome
        ? DetermineOutcome<Setup2[Outcome1], FakeCoin, FakeCoinStatus> extends infer Outcome2 extends WeighingOutcome
          ? DetermineFakeCoin<DetermineResult<Setup1, FakeCoin, FakeCoinStatus>
            | DetermineResult<Setup2[Outcome1], FakeCoin, FakeCoinStatus>
            | DetermineResult<Setup3[Outcome1][Outcome2], FakeCoin, FakeCoinStatus>
            > extends FakeCoin ? never
          : [FakeCoin, FakeCoinStatus]
        : [FakeCoin, FakeCoinStatus]
      : [FakeCoin, FakeCoinStatus]
    }[FakeCoinStatuses]}[Coins]

type FindUnknownFakeCoinStatuses<Setup1 extends WeighingSetup1, Setup2 extends WeighingSetup2, Setup3 extends WeighingSetup3>
  = {[FakeCoin in Coins]: {[FakeCoinStatus in FakeCoinStatuses]
      : DetermineOutcome<Setup1, FakeCoin, FakeCoinStatus> extends infer Outcome1 extends WeighingOutcome
        ? DetermineOutcome<Setup2[Outcome1], FakeCoin, FakeCoinStatus> extends infer Outcome2 extends WeighingOutcome
          ? DetermineFakeCoin<DetermineResult<Setup1, FakeCoin, FakeCoinStatus>
            | DetermineResult<Setup2[Outcome1], FakeCoin, FakeCoinStatus>
            | DetermineResult<Setup3[Outcome1][Outcome2], FakeCoin, FakeCoinStatus>
            > extends FakeCoin
            ? UnknownFakeCoinStatus<FakeCoin, DetermineResult<Setup1, FakeCoin, FakeCoinStatus>
              | DetermineResult<Setup2[Outcome1], FakeCoin, FakeCoinStatus>
              | DetermineResult<Setup3[Outcome1][Outcome2], FakeCoin, FakeCoinStatus>
              >
          : FakeCoin
        : FakeCoin
      : FakeCoin
    }[FakeCoinStatuses]}[Coins]

type ExtractOne<U> = (U extends U ? (_: Promise<U>) => U : U) extends ((_: infer I extends Promise<U>) => U) ? Awaited<I> : never

type PrintOutcome1<Setup1 extends WeighingSetup1, Setup2 extends WeighingSetup2, Setup3 extends WeighingSetup3>
  = ExtractOne<FindUnidentifiedFakeCoins<Setup1, Setup2, Setup3>> extends [infer FakeCoin extends Coins, infer FakeCoinStatus extends FakeCoinStatuses]
    ? PrintOutcome<Setup1['Left'][number], Setup1['Right'][number], FakeCoin, FakeCoinStatus, `# 1st Weight Outcome, fake coin is ${FakeCoin} and it's ${FakeCoinStatus}`> : 'You solved it!'

type PrintOutcome2<Setup1 extends WeighingSetup1, Setup2 extends WeighingSetup2, Setup3 extends WeighingSetup3>
  = ExtractOne<FindUnidentifiedFakeCoins<Setup1, Setup2, Setup3>> extends [infer FakeCoin extends Coins, infer FakeCoinStatus extends FakeCoinStatuses]
    ? DetermineOutcome<Setup1, FakeCoin, FakeCoinStatus> extends infer Outcome1 extends WeighingOutcome
      ? PrintOutcome<Setup2[Outcome1]['Left'][number], Setup2[Outcome1]['Right'][number], FakeCoin, FakeCoinStatus, `# 2nd Weight Outcome, fake coin is ${FakeCoin} and it's ${FakeCoinStatus}`> : 'You solved it!'
    : 'Incorrect weigh setup 1'

type PrintOutcome3<Setup1 extends WeighingSetup1, Setup2 extends WeighingSetup2, Setup3 extends WeighingSetup3>
  = ExtractOne<FindUnidentifiedFakeCoins<Setup1, Setup2, Setup3>> extends [infer FakeCoin extends Coins, infer FakeCoinStatus extends FakeCoinStatuses]
    ? DetermineOutcome<Setup1, FakeCoin, FakeCoinStatus> extends infer Outcome1 extends WeighingOutcome
      ? DetermineOutcome<Setup2[Outcome1], FakeCoin, FakeCoinStatus> extends infer Outcome2 extends WeighingOutcome
        ? PrintOutcome<Setup3[Outcome1][Outcome2]['Left'][number], Setup3[Outcome1][Outcome2]['Right'][number], FakeCoin, FakeCoinStatus, `# 3rd Weight Outcome, fake coin is ${FakeCoin} and it's ${FakeCoinStatus}`> : 'You solved it!'
      : 'Incorrect weigh setup 2'
    : 'Incorrect weigh setup 1'

type PrintOutcome<Left extends Coins, Right extends Coins, FakeCoin extends Coins, FakeCoinStatus extends FakeCoinStatuses, Message extends string>
  = true extends (FakeCoin extends Left ? FakeCoinStatus extends 'Lighter' ? true : false : false) | (FakeCoin extends Right ? FakeCoinStatus extends 'Heavier' ? true : false : false)
    ? Merge<{[P in PrintMessageFixedWidth<Message>]: 1} & {[P in
`${PrintCoinsFixedWidth<Left>}                                 `]: 1} & {[P in
`[===========================]    ${PrintCoinsFixedWidth<Right>}`]: 1} & {
'               [O]               [===========================]': 1
'                                             [O]              ': 1
'                              [O]                             ': 1}>
  : true extends (FakeCoin extends Left ? FakeCoinStatus extends 'Heavier' ? true : false : false) | (FakeCoin extends Right ? FakeCoinStatus extends 'Lighter' ? true : false : false)
    ? Merge<{[P in PrintMessageFixedWidth<Message>]: 1} & {[P in
`                                 ${PrintCoinsFixedWidth<Right>}`]: 1} & {[P in
`${PrintCoinsFixedWidth<Left>}    [===========================]`]: 1} & {
'[===========================]                [O]              ': 1
'               [O]                                            ': 1
'                              [O]                             ': 1}>
  : Merge<{[P in PrintMessageFixedWidth<Message>]: 1} & {[P in
`${PrintCoinsFixedWidth<Left>}    ${PrintCoinsFixedWidth<Right>}`]: 1} & {
'[===========================]    [===========================]': 1
'             [O]                              [O]             ': 1
'                              [O]                             ': 1}>

type PrintMessageFixedWidth<Message extends string>
  = Message extends `${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}` ? Message
  : PrintMessageFixedWidth<`${Message} `>
type PrintCoinsFixedWidth<C extends Coins, Output extends string = PrintCoins<C>, PadLeft = true>
  = Output extends `${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}` ? Output
  : PadLeft extends true ? PrintCoinsFixedWidth<C, ` ${Output}`, false> : PrintCoinsFixedWidth<C, `${Output} `, true>
type PrintCoins<C extends Coins, C1 extends Coins = ExtractOne<C>> = [C1] extends [never] ? '' : `${C1} ${PrintCoins<Exclude<C, C1>>}`

type Merge<O> = {[K in keyof O]: O[K]}

/*******************************************
 *             Spoiler Alert
 *******************************************
 */
type DetermineFakeCoin<Results extends WeighingResult, O extends Results = Results>
  = (O extends O
      ? O extends {Heavier: Coins, Lighter: Coins}
        ? O['Heavier'] extends IdentifiedRealCoin<Results> ? OneCoinOrNever<Exclude<O['Lighter'], IdentifiedRealCoin<Results>>>
        : O['Lighter'] extends IdentifiedRealCoin<Results> ? OneCoinOrNever<Exclude<O['Heavier'], IdentifiedRealCoin<Results>>>
        : never
      : never
    : never) extends infer FakeCoin extends Coins
      ? [FakeCoin] extends [never] ? Exclude<Coins, IdentifiedRealCoin<Results>>
      : FakeCoin
    : Exclude<Coins, IdentifiedRealCoin<Results>>

type IdentifiedRealCoin<Results extends WeighingResult, C extends Coins = Coins>
  = C extends C
    ? C extends Results['Heavier'] & Results['Lighter'] ? C
    : C extends Results['Equal'] ? C
    : never
  : never

type UnknownFakeCoinStatus<FakeCoin extends Coins, Reaults extends WeighingResult, O extends Reaults = Reaults>
  = FakeCoin extends (O extends O ? O['Heavier'] | O['Lighter'] | O['Equal'] : never) ? never : FakeCoin

#33474 - Pull Request updated.

2024-07-14T02:12:35.718Z Preview in Playground