|
| 1 | +package gsoc |
| 2 | +package contributors |
| 3 | + |
| 4 | +import cats.effect.* |
| 5 | +import cats.effect.std.Random |
| 6 | +import fs2.concurrent.* |
| 7 | +import fs2.dom.HtmlElement |
| 8 | +import calico.html.io.{*, given} |
| 9 | +import calico.syntax.* |
| 10 | + |
| 11 | +import org.scalajs.dom |
| 12 | +import scala.concurrent.duration.DurationInt |
| 13 | + |
| 14 | +val nvshinde: Contributor = Contributor("nvshinde"): |
| 15 | + import CatShooter.* |
| 16 | + for |
| 17 | + rng <- Random.scalaUtilRandom[IO].toResource |
| 18 | + mousePos <- randCol(rng).toResource |
| 19 | + ref <- SignallingRef[IO].of(init(mousePos)).toResource |
| 20 | + node <- div( |
| 21 | + tabIndex := 0, |
| 22 | + styleAttr := "outline:none;", |
| 23 | + p( |
| 24 | + "I am", |
| 25 | + span( |
| 26 | + styleAttr := "color:mediumseablue;font-weight:bold", |
| 27 | + "@nvshinde" |
| 28 | + ), |
| 29 | + " on Github. I agree to follow the Typelevel CoC and GSoC AI policy." |
| 30 | + ), |
| 31 | + p( |
| 32 | + styleAttr := "font-size:13px;color:#888;margin:4px 0;", |
| 33 | + "Play the Mice invaders game below. Use arrow keys to move left and right and space to shoot!" |
| 34 | + ), |
| 35 | + onKeyDown --> (_.foreach { e => |
| 36 | + e.key match |
| 37 | + case "ArrowLeft" => e.preventDefault *> ref.update(moveLeft) |
| 38 | + case "ArrowRight" => e.preventDefault *> ref.update(moveRight) |
| 39 | + case " " => e.preventDefault *> shootLaser(ref, rng) |
| 40 | + case _ => IO.unit |
| 41 | + }), |
| 42 | + ref.map(renderSpace), |
| 43 | + ref.map { s => |
| 44 | + p( |
| 45 | + styleAttr := "margin-top:8px;font-size:14px;", |
| 46 | + s"Score: ${s.score}" |
| 47 | + ) |
| 48 | + } |
| 49 | + ) |
| 50 | + yield node |
| 51 | + |
| 52 | +private object CatShooter { |
| 53 | + val Size = 20 |
| 54 | + val CellPx = 20 |
| 55 | + |
| 56 | + final case class State( |
| 57 | + cat: Int, |
| 58 | + mouse: Int, |
| 59 | + laserRow: Option[Int], |
| 60 | + score: Int |
| 61 | + ) |
| 62 | + |
| 63 | + def randCol(rng: Random[IO]): IO[Int] = |
| 64 | + rng.betweenInt(0, Size) |
| 65 | + |
| 66 | + def init(mouse: Int): State = State( |
| 67 | + cat = Size / 2, |
| 68 | + mouse = mouse, |
| 69 | + laserRow = None, |
| 70 | + score = 0 |
| 71 | + ) |
| 72 | + |
| 73 | + def moveLeft(s: State): State = |
| 74 | + if s.cat > 0 then s.copy(cat = s.cat - 1) else s |
| 75 | + |
| 76 | + def moveRight(s: State): State = |
| 77 | + if s.cat < Size - 1 then s.copy(cat = s.cat + 1) else s |
| 78 | + |
| 79 | + def shootLaser( |
| 80 | + ref: SignallingRef[IO, State], |
| 81 | + rng: Random[IO] |
| 82 | + ): IO[Unit] = |
| 83 | + fs2 |
| 84 | + .Stream |
| 85 | + .range(1, Size) |
| 86 | + .evalMap { row => ref.update(_.copy(laserRow = Some(row))) } |
| 87 | + .metered(20.millis) |
| 88 | + .compile |
| 89 | + .drain >> |
| 90 | + ref |
| 91 | + .modify { s => |
| 92 | + if s.cat == s.mouse then |
| 93 | + val updatedScore = s.copy(score = s.score + 1, laserRow = None) |
| 94 | + (updatedScore, true) |
| 95 | + else (s.copy(laserRow = None), false) |
| 96 | + } |
| 97 | + .flatMap { |
| 98 | + case true => |
| 99 | + randCol(rng).flatMap(newCol => ref.update(_.copy(mouse = newCol))) |
| 100 | + case false => IO.unit |
| 101 | + } |
| 102 | + |
| 103 | + def renderSpace(s: State): Resource[IO, HtmlElement[IO]] = |
| 104 | + div( |
| 105 | + styleAttr := s"display:grid;grid-template-columns:repeat($Size, ${CellPx}px); background:#111;width:${Size * CellPx}px;height:${Size * CellPx}px" |
| 106 | + ).flatMap { gridEl => |
| 107 | + Resource |
| 108 | + .eval(IO { |
| 109 | + val raw = gridEl.asInstanceOf[dom.HTMLElement] |
| 110 | + |
| 111 | + raw.innerHTML = "" |
| 112 | + for |
| 113 | + y <- 0 until Size |
| 114 | + x <- 0 until Size |
| 115 | + do |
| 116 | + val cell = dom.document.createElement("div") |
| 117 | + cell.setAttribute( |
| 118 | + "style", |
| 119 | + s"width:${CellPx}px;height:${CellPx}px;display:flex;align-items:center;justify-content:center;color:white;font-weight:bold;font-size:20px" |
| 120 | + ) |
| 121 | + |
| 122 | + val content = |
| 123 | + if y == 0 && x == s.cat then "C" |
| 124 | + else if y == Size - 1 && x == s.mouse then "M" |
| 125 | + else if s.laserRow.contains(y) && x == s.cat then "*" |
| 126 | + else "" |
| 127 | + |
| 128 | + cell.textContent = content |
| 129 | + raw.appendChild(cell) |
| 130 | + }) |
| 131 | + .map(_ => gridEl) |
| 132 | + } |
| 133 | +} |
0 commit comments