25春ハッカソン 15班
作ったもの
数字と演算子(+
,-
,*
,/
)を組み合わせて 10 を作るオンライン対戦型カードゲーム「hasTEN」を作りました。「急ぐ」といった意味の英単語の "hasten" と 10 (TEN) を組み合わせています。
リポジトリ
https://github.com/traP-jp/h25s_15
このゲームでは、プレイヤーは1対1で対戦します。プレイヤーは15秒間の自分のターンに行動することができます。行動は3種類あります。
- 場に出ている札を手札に加える。(場に出ている札は補充される)
- 手札から数字と演算子のカードを組み合わせて答えが 10 になる式を作り提出する
- 手札からアイテムカードを使う
2 の式の提出で得点が得られます。得点は式に使った数字の数によって決まり、数字2枚なら1点ですが、3枚なら5点、4枚なら10点と、長い式ほど得点が大きくなります。時間内であれば1ターンの間に何回でも行動することができます。これを2人で20ターン繰り返し、点数が高かった方が勝ちです。
対戦画面
結果画面
このゲームでは、10が作れる組み合わせを素早く見極めて数字と演算子をバランスよく取ることが求められます。数字のカードは1ケタの数字としてのみ使える(1と2のカードで12のようにはできない)ため、数字4枚のカードでは演算子3枚を合わせて7枚の並べ方を時間内に考える必要があり、長い式を作るのが難しくなっています。また、自分の手札の数には制限があるため、手札のやりくりの戦略も同時に考えなくてはなりません。
また、アイテムも重要な役割があります。アイテムは4種類存在しますが、そのうち特に強力なのは「自分の手札の上限を増やすアイテム」と「相手の手札をリセットするアイテム」です。自分の手札の上限は初めは10枚なので数字5枚(20点)の式が上限ですが、11枚にすれば数字6枚(30点)の式が作れるため、リードを得ることができます。「相手の手札をリセットするアイテム」は、相手が多くの手札を持っているときに役立ちます。自分のターンの間、相手は今ある手札でどのように10を作れるかを考えているので、自分のターンの終わり際にこのアイテムを使うことで相手の思考を無駄にすることができます。さらに、このカードの影響で、自分のターンの間に長い式を作ってカードを使い切ることが重要になります。
このように、10を組み立てる数学・算数パズル的な要素とカードゲームの戦略性、さらにリアルタイム通信と時間制限によるハラハラが組み合わさって、とても盛り上がるゲームになりました。
技術的概要
クライアントサイド
クライアントでは、フレームワークは Vue 、言語は TypeScript 、バンドラには Vite を使いました。この構成は、新入生向けの「Webエンジニアになろう講習会」と同じになるように意識されています。また、ページルーティングには Vue Router を使っています。
ゲーム画面のコンポーネント構成は以下のようになっています。
コンポーネント構成
各 UI コンポーネントは単一責任の原則に従うことを目指して、自分が関心のある UI と、そのための小さなロジックだけを持つようにしました。これらを、全体のロジックを持っているページコンポーネントから import して、組み合わせて使用しています。
また、今回はオンライン対戦ゲームだったため、サーバ側チームが状態のソースのほとんどをサーバサイドで持つ設計をしてくれました。これにより、クライアントサイドの実装は状態の更新を要求をすることと、更新時に通知される新しい状態に同期することのみでよくなり、複雑で非同期な状態更新をサーバサイドに任せることができました。
内部的には、通知されるゲームの状態を正しく持つため、 GameInfo
というクラスで一元管理しています。この GameInfo
のオブジェクトを ref 化して、 WebSocket を listen するための useGameEvent
Composable を使って更新します。ページコンポーネントは以下のように、GameInfo
のオブジェクトから導出される状態とコールバックを、より小さな UI コンポーネントに配るということをすればよいです。
<script setup lang="ts">
/* importなど */
const gameState = ref(new GameInfo(gameId))
const myPlayer = computed(() => gameState.value.players[gameState.value.myPlayerId])
useGameEvent(gameWsUrl, (event) => {
gameState.value.onEvent(event)
})
function useCard(cardId: string) { /* アイテムなら使う、数字なら式に加える */ }
</script>
<template>
<div class="my-hand-container">
<HandCards card-size="medium">
<GameCard
v-for="handCard in myPlayer.cards"
size="medium"
@click="() => useCard(handCard.id)"
>
<GameCardContent :card="handCard" />
</GameCard>
</HandCards>
</div>
</template>
開発を進めるにあたって、できるだけタスクを分割することを意識しました。事前に Figma を使って精度の高めなデザインが作成できていたので、当日は必要そうなコンポーネント単位で GitHub 上に Issue を立てて各メンバーにアサインする、 Issue ドリブン開発を実施しました。これによって自分が今やることが明確になり、またデザインの答え合わせも比較的簡単にできるので、開発を速く回せたのではないかと思っています。反面、レビュワー側のコンテキストスイッチングが多発してしまったのは大変でした。
サーバーサイド
サーバーアプリは、Go 言語で記述され、Web サーバーフレームワークは labstack/echo
、DB操作には jmoiron/sqlx
を使いました。この構成は、新入生向けの「Webエンジニアになろう講習会」と同じです。
また、クライアントにカードの状態変化などのイベントを通知する必要があったため、WebSocket を使っていますが、WebSocket のフレームワークとして olahol/melody
を使いました。 Go の WebSocket のライブラリとしては gorilla/websocket
が有名だと思いますが、 今回使用した melody は gorilla/websocket
を内部で使用して、より高レベルな API を提供しています。例えば、特定の条件を満たすクライアントのみにイベントを通知したい場合は以下のように書くことができます。
var m *melody.Melody
// ...
err := m.BroadcastBinaryFilter([]byte("hello world"), func(s *Session) bool {
val, ok := s.Get("key")
if !ok {
return false
}
valStr, ok := val.(string)
return ok && valStr == "val"
})
今回はゲームアプリということでそれぞれのゲームに参加しているユーザーごとに通知を飛ばす必要があり、この機能がとても便利でした。
ユーザーが提出した式がちゃんと 10 になるかの判定もサーバー側で行っています。数字が 1 ケタだけなので構文解析自体は簡単ですが、あまり時間が無かったので、alecthomas/participle
を使ってパースしました。構造体にタグをつけるといい感じにパースして構文木を作ってくれます。構文木を作った後はこれを計算して 10 になるかを確かめる必要がありますが、 単純に計算すると浮動小数点の誤差が発生して、本来 10 になるはずの式が 10 でなくなってしまう可能性があります。そのため、標準ライブラリの math/big.Rat
を使って有理数での計算を行いました。
パッケージ構成としては、技術的関心ではなく機能ごとにトップレベルのパッケージを区切る、 package by feature の考え方を取り入れてみました。ikura-hamu が以前から試してみたいと思っていました。基本的に機能ごとに分担して作業を進めていくので、1 つの作業が 1 パッケージ内で完結するのはかなり頭の負荷が少ないし、エディタの補完も関係ないコードが出てこないので楽に開発できたと思います。一方で、複数の機能にまたがるような実装が発生してしまったため、機能を適切に分ける能力が必要になると思いました。
感想
@ikura-hamu
バックエンドと全体のリーダー的なことをしました。バックエンドが重実装だったため当日はチーム全体のことはあまり見れなかったのが反省点ですが、気づいたらとてもかっこいい画面ができていてすごいと思いました。完成後はいろんな人に遊んでもらえて盛り上がったので嬉しかったです。楽しく開発ができたのでよかったです。
@irinoirino
25Bなので初めての春ハッカソンでした。HTMLやCSS、JSは昔少し勉強したことがありましたが、チーム開発やバックエンドを含めた開発は初めてで、GitやAPIを実際に使うのも初めてでした。最初はチーム開発の進め方やプログラムの書き方がわからず苦労しましたが、先輩のサポートもあり、開発に参加できたと思います。この2日間大変でしたが、達成感も大きく、充実していて大きく成長できたと思います。今までぼんやりとしていたチーム開発のイメージがはっきりとし、学ぶことも明確になったとても良い機会でした。
@mizufuku
春ハッカソンを通じて、Gitの使い方を実際に体験し、しっかりと身につけることができたことは大きな収穫でした。これまではGit講習会で基礎知識は学んでいたものの、実践の機会がなかったため、理解が不十分であることを痛感していました。しかし、実際にプロジェクトの中で活用することで、知識が実践的な力に変わりました。また、春ハッカソンを目標にGo言語の学習を進められたことも大きな収穫でした。最後に、開発を進める中で設計書の存在に助けられ、非常に便利だと感じました。今後は自分で設計ができるようになることを目標に、さらに励んでいきたいと思います。
@Ponjuice
春ハッカソンでは、フロントエンドを担当しました。
これまで競技プログラミングには取り組んでいたもののフロントはちょっとした個人サイトを作ったことがあるくらいの経験しかなかったので、少し不安な気持ちでの参加でした。
ハッカソン当日は自分の知識不足で新入生からの質問にうまく答えられなかったりと反省点もありますが、ちゃんと楽しいゲームの形にできてよかったです。
完成後、想像以上にたくさんの人が楽しそうに遊んでくれているのを見て、すごく嬉しかったです。
@tufusa
フロントエンドのデザインと実装をしました。traPに入って初めて参加したハッカソンで、Vue や Figma に不慣れなまま始まってしまい不安でした。当日は手が回りきらず、指示が雑になってしまったり、25Bのメンバーの吸収が早くレビューが追い付かなくなってしまったりしたのが反省点です。なんとかしてくれたメンバーの皆さんには頭が上がりません。特に、当初作れる想定をしていなかったランキング表示を作るときに「デザインはなんかかっこいい感じにして」と言ったことが印象深いです。ごめんなさい。
自分のタスクとしては、状態管理をバックエンドチームが巻き取ってくれたり、 GameInfo
の設計やコールバックの導出を Ponjuice さんがやってくれたりしたので、作ってもらった UI コンポーネントを組み上げてデザインを整えることが主でした。そのため、CSS を書いていていたら気付いた時にはめっちゃ面白いゲームが完成していて凄かったです。最初にプレイしたとき、ゲームバランスがすごく良くて驚いたのを覚えています。
とても楽しく開発でき、また多くの学びを得ることができました!
@web
本来バックエンドを担当する予定でしたが、当日体調を崩してしまい結局参加できませんでした...
前日のアイデア出しだけ参加して終わってしまったので、次の機会にはオンサイトでしっかりとコード書いたりしてみたいです!
@ysaga
僕はフロントエンドを担当こそしていましたが、VueはもちろんTypeScriptもGitもわからず、当日の朝までなろう講習会の資料をふむふむしていました。結局わかりませんでした。1日目の朝タスクを振られた時、一挙手一投足何もわからずに絶望したのを今でもよく覚えています。あらゆることを先輩に質問しましたが、しばらくは言われたことを理解する間もなくとりあえず手だけを動かしていたので、まさにオフチョベットしたテフをマブガットしてリットしているような心持ちでした。先輩方、2日間無数の質問に対応してくださりありがとうございました。
言うなれば補助輪が10個くらいついた自転車を漕いでいたわけですが、それでも前に進んだ時はとても嬉しかったです。チームメンバーと過ごす中で、協働する際に自分も貢献できるようになりたいなと思ったハッカソンでした、参加できて良かったです。