Skip to content

Commit

Permalink
Fixed Base32 encoding and added more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Billzabob committed May 29, 2020
1 parent 0f54ebf commit cb6007d
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 11 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ libraryDependencies += "com.github.billzabob" %% "fordeckmacia" % "version"
```scala
import fordeckmacia._

val deckcode = "CEBAKAIECETSQNBWA4AQGBYMB4PCKKBXAIAQCBA4AEAQGGIAA"
val deckcode = "CIBAKAIECETSQNBWA4AQGBYMB4PCKKBXAIAQCBA4AEAQGGIA"

val deck = Deck.decode(deckcode)
// deck: scodec.Attempt[Deck] = Successful(
Expand Down Expand Up @@ -49,7 +49,7 @@ val cardCodes = deck.map(_.cards.map(_.code))
// )

val encoded = deck.flatMap(_.encode)
// encoded: scodec.Attempt[String] = Successful("CIBAKAIECETSQNBWA4AQGBYMB4PCKKBXAIAQCBA4AEAQGGIAA")
// encoded: scodec.Attempt[String] = Successful("CIBAKAIECETSQNBWA4AQGBYMB4PCKKBXAIAQCBA4AEAQGGIA")
```

<img src="https://github.com/Billzabob/ForDeckmacia/blob/master/src/main/resources/ForDeckmacia.png" height="300px"/>
5 changes: 4 additions & 1 deletion src/main/scala/fordeckmacia/Base32.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ object Base32 {
.mkString
)

val remaining = binaryString.length % 5
val padding = if (remaining == 0) 0 else 5 - remaining

binaryString
.padTo(binaryString.length + 5 - (binaryString.length % 5), "0")
.padTo(binaryString.length + padding, "0")
.mkString
.grouped(5)
.map(Integer.parseInt(_, 2))
Expand Down
8 changes: 8 additions & 0 deletions src/main/scala/fordeckmacia/Card.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fordeckmacia

import cats.data.NonEmptyList
import cats.implicits._
import scala.util.Try
import scodec.{Attempt, Codec, Err}
import scodec.codecs._

Expand All @@ -13,6 +14,13 @@ case class Card(set: Int, faction: Faction, cardNumber: Int) {

object Card {

def fromCode(code: String): Option[Card] =
for {
set <- Try(code.take(2).toInt).toOption
faction <- Faction.fromId(code.drop(2).take(2))
number <- Try(code.drop(4).take(3).toInt).toOption
} yield Card(set, faction, number)

def codec: Codec[List[Card]] = {
listOfN(vintL, factionCodec).xmapc(_.flatMap(_.toList))(_.groupByNel(card => (card.set, card.faction.int)).values.toList.sortBy(_.size))
}
Expand Down
10 changes: 5 additions & 5 deletions src/main/scala/fordeckmacia/Deck.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ case class Deck(cards: List[Card]) {

override def equals(a: Any) =
a match {
case Deck(otherCards) => cards.sortBy(_.code) == otherCards.sortBy(_.code)
case _ => false
case d: Deck => encode == d.encode
case _ => false
}

override def hashCode = cards.sortBy(_.code).hashCode
override def hashCode = encode.hashCode
}

object Deck {
Expand All @@ -36,9 +36,9 @@ object Deck {
Attempt.guard(checkVersion(prefix), unsupportedVersion(prefix)).as(Deck(duplicate(cardsOf3, 3) ::: duplicate(cardsOf2, 2) ::: cardsOf1))
}(deck => prefix ~ cardsOf(deck.cards, 3)(_.code) ~ cardsOf(deck.cards, 2)(_.code) ~ cardsOf(deck.cards, 1)(_.code))

def duplicate[A](list: List[A], count: Int): List[A] = list.flatMap(a => List.fill(count)(a))
private def duplicate[A](list: List[A], count: Int): List[A] = list.flatMap(a => List.fill(count)(a))

def cardsOf[A, B: cats.Order](list: List[A], count: Int)(f: A => B) =
private def cardsOf[A, B: cats.Order](list: List[A], count: Int)(f: A => B): List[A] =
list.groupByNel(f).values.filter(_.size == count).toList.map(_.head)

private def checkVersion(byte: Byte): Boolean = {
Expand Down
3 changes: 2 additions & 1 deletion src/main/scala/fordeckmacia/Faction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ object Faction {

val allFactions = List(Demacia, Freljord, Ionia, Noxus, PiltoverAndZaun, ShadowIsles, Bilgewater)

def fromInt(int: Int): Option[Faction] = allFactions.find(_.int == int)
def fromInt(int: Int): Option[Faction] = allFactions.find(_.int == int)
def fromId(id: String): Option[Faction] = allFactions.find(_.id == id)
}
41 changes: 41 additions & 0 deletions src/test/resources/deck1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
CIBQEAQDAMCAIAIEBAITINQGAEBQEDY6EUUC6AICAECB6JYA
01PZ008
01PZ008
01PZ008
01NX040
01NX040
01NX040
01NX015
01NX015
01NX015
01NX037
01NX037
01NX037
01NX030
01NX030
01NX030
01PZ054
01PZ054
01PZ054
02NX004
02NX004
02NX004
01PZ017
01PZ017
01PZ017
01NX047
01NX047
01NX047
01PZ052
01PZ052
01PZ052
01PZ039
01PZ039
02NX003
02NX003
02NX003
01PZ031
01PZ031
01NX002
01NX002
01NX002
41 changes: 41 additions & 0 deletions src/test/resources/deck2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
CIBQCAQEBABACBA3GQCQCBI5EEUDKNQDAEBAIBQCAECB6MAEAECQCKZRHAAQEAIFB4MQ
01SI053
01SI053
01SI053
02PZ008
02PZ008
02PZ008
01SI043
01SI043
01SI056
01SI056
01PZ048
01PZ048
01SI033
01SI033
01SI033
01PZ027
01PZ027
01PZ027
01SI049
01SI049
01SI040
01SI040
01SI040
01PZ052
01PZ052
01PZ052
01PZ031
01PZ031
02PZ006
02PZ006
01SI054
01SI054
01SI054
01SI029
01SI029
01SI029
01SI025
01SI001
01SI001
01SI015
44 changes: 42 additions & 2 deletions src/test/scala/fordeckmacia/DeckTest.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package fordeckmacia

import cats.implicits._
import org.scalacheck.Gen
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
import scala.io.Source
import scala.util.Random
import scodec.Attempt

class DeckTest extends AnyFlatSpec with Matchers with ScalaCheckDrivenPropertyChecks {
Expand Down Expand Up @@ -41,12 +44,36 @@ class DeckTest extends AnyFlatSpec with Matchers with ScalaCheckDrivenPropertyCh
}
}

it should "not depend on card order" in {
forAll(deckGenerator) { deck: Deck =>
val encoded = deck.encode
val encodedScrambled = Deck(Random.shuffle(deck.cards)).encode
encodedScrambled shouldBe encoded
}
}

it should "fail on unexpected version" in {
Deck.decode("D4AAAAA") should matchPattern { case Attempt.Failure(_) => }
}

it should "return None on invalid deck codes" in {
Deck.decode("CIAAAAI")
it should "fail on invalid deck codes" in {
Deck.decode("CIAAAAI") should matchPattern { case Attempt.Failure(_) => }
}

it should "fail on unknown factions" in {
Deck.decode("CIAAAAIBAB7QA") should matchPattern { case Attempt.Failure(_) => }
}

it should "fail on invalid Base32" in {
Deck.decode("CIAAAAA01") should matchPattern { case Attempt.Failure(_) => }
}

it should "work for deck1" in {
verifyDeck("deck1.txt")
}

it should "work for deck2" in {
verifyDeck("deck2.txt")
}

"decks" should "be equal if order is different" in {
Expand All @@ -59,4 +86,17 @@ class DeckTest extends AnyFlatSpec with Matchers with ScalaCheckDrivenPropertyCh
deck1.hashCode shouldBe deck2.hashCode
deck1 should not be card1
}

private def verifyDeck(resource: String) = {
val deck = readDeckFromResource(resource)
Deck.decode(deck.code).map(_.cards.map(_.code).sorted) shouldBe Attempt.successful(deck.cardCodes.sorted)
deck.cardCodes.traverse(Card.fromCode).map(Deck.apply).map(_.encode) shouldBe Some(Attempt.successful(deck.code))
}

case class DeckAndCode(code: String, cardCodes: List[String])

private def readDeckFromResource(resource: String): DeckAndCode = {
val lines = Source.fromResource(resource).getLines.toList
DeckAndCode(lines.head, lines.tail.filterNot(_.isEmpty))
}
}

0 comments on commit cb6007d

Please sign in to comment.