initial commit

This commit is contained in:
Daniel Noulet 2026-10-07 15:16:00 +02:00
commit ac9aa86d33
14 changed files with 1021 additions and 0 deletions

306
cmd/go-fish/main.go Normal file
View file

@ -0,0 +1,306 @@
package main
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"text/template"
"github.com/danyel/go-fish/internal/game"
"github.com/danyel/go-fish/internal/store"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type JoinResponse struct {
PlayerID string `json:"playerId"`
}
type AskRequest struct {
PlayerID string `json:"playerId"`
OpponentID string `json:"opponentId"`
Rank string `json:"rank"`
}
type AskResponse struct {
Status string `json:"status"`
MyHand []string `json:"myHand"`
MyBookRanks []string `json:"myBookRanks"`
OpponentBookRanks []string `json:"opponentBookRanks"`
GameOver bool `json:"gameOver"`
Message string `json:"message"`
}
func main() {
s := store.NewPlayerStore()
mux := chi.NewMux()
mux.Use(middleware.Logger)
mux.Use(middleware.Recoverer)
mux.Get("/", homeHandler)
mux.Get("/waiting", waitingHandler(s))
mux.Get("/play/{id}", playHandler(s))
mux.Post("/join", joinHandler(s))
mux.Post("/ask", askHandler(s))
fmt.Printf("Starting go-fish on :3000\n")
http.ListenAndServe(":3000", mux)
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("web/templates/index.html"))
tmpl.Execute(w, nil)
}
func waitingHandler(s *store.PlayerStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
players := s.GetAvailablePlayers()
hasBotOption := len(players) <= 1
tmpl := template.Must(template.ParseFiles("web/templates/waiting.html"))
data := struct {
Players []*game.Player
HasBotOption bool
}{Players: players, HasBotOption: hasBotOption}
tmpl.Execute(w, data)
}
}
func playHandler(s *store.PlayerStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
_ = id
p, ok := s.GetPlayer(id)
if !ok {
http.NotFound(w, r)
return
}
available := s.GetAvailablePlayers()
var opponent *game.Player
for _, ap := range available {
if ap.ID != p.ID {
opponent = ap
break
}
}
var isBot bool
var g *game.Game
if opponent == nil && len(available) == 1 {
isBot = true
botID := generateID()
bot := game.NewPlayer(botID, "Bot")
s.AddPlayer(botID, "Bot")
opponent = bot
s.MarkPlaying(p.ID)
s.MarkPlaying(botID)
g = game.NewGame(p, opponent)
g.ID = "game-" + p.ID + "-" + botID
} else if opponent != nil {
isBot = false
s.MarkPlaying(p.ID)
s.MarkPlaying(opponent.ID)
g = game.NewGame(p, opponent)
g.ID = "game-" + p.ID + "-" + opponent.ID
} else {
isBot = true
botID := generateID()
bot := game.NewPlayer(botID, "Bot")
s.AddPlayer(botID, "Bot")
opponent = bot
s.MarkPlaying(p.ID)
s.MarkPlaying(botID)
g = game.NewGame(p, opponent)
g.ID = "game-" + p.ID + "-" + botID
}
s.CreateGame(p.ID, g)
for i := 0; i < 7; i++ {
if len(g.Deck) > 0 {
g.Player1.Draw(g.DrawCard())
g.Player2.Draw(g.DrawCard())
}
}
tmpl := template.Must(template.ParseFiles("web/templates/play.html"))
data := struct {
Game *game.Game
Player *game.Player
IsBot bool
}{Game: g, Player: p, IsBot: isBot}
tmpl.Execute(w, data)
}
}
func joinHandler(s *store.PlayerStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
if name == "" {
name = "Player"
}
id := generateID()
s.AddPlayer(id, name)
http.SetCookie(w, &http.Cookie{
Name: "player_id",
Value: id,
Path: "/",
})
respondJSON(w, http.StatusOK, JoinResponse{PlayerID: id})
}
}
func askHandler(s *store.PlayerStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req AskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
g, ok := s.GetGame(req.PlayerID)
if !ok {
respondJSON(w, http.StatusNotFound, AskResponse{Message: "Game not found"})
return
}
rank := game.RankAce
switch req.Rank {
case "A":
rank = game.RankAce
case "2":
rank = game.Rank2
case "3":
rank = game.Rank3
case "4":
rank = game.Rank4
case "5":
rank = game.Rank5
case "6":
rank = game.Rank6
case "7":
rank = game.Rank7
case "8":
rank = game.Rank8
case "9":
rank = game.Rank9
case "10":
rank = game.Rank10
case "J":
rank = game.RankJack
case "Q":
rank = game.RankQueen
case "K":
rank = game.RankKing
}
asker := g.Player1
target := g.Player2
if g.Current == 2 {
asker = g.Player2
target = g.Player1
}
var cardsRemoved []game.Card
if !asker.HasCard(rank) {
card := g.DrawCard()
target.Draw(card)
} else {
cardsRemoved = asker.RemoveCard(rank)
if len(cardsRemoved) > 0 {
asker.Hand = append(asker.Hand, cardsRemoved...)
if asker.HasBook(rank) {
g.BookPile = append(g.BookPile, []game.Card{})
}
}
}
gameOver := false
message := ""
myHand := asker.Hand
oppHand := target.Hand
myBookCount := countBooks(g.BookPile)
oppBookCount := countOpponentBooks(g.BookPile)
if len(myHand) == 0 && len(oppHand) == 0 {
gameOver = true
message = "Game over! No more cards."
}
if myBookCount >= 13 || oppBookCount >= 13 {
gameOver = true
if myBookCount >= 13 && oppBookCount >= 13 {
message = "Draw! Both players have 13 books."
} else if myBookCount >= 13 {
message = "You win! You have 13 books."
} else {
message = "You lose! Opponent has 13 books."
}
}
s.CreateGame(req.PlayerID, g)
respondJSON(w, http.StatusOK, AskResponse{
Status: "success",
MyHand: cardStrings(myHand),
MyBookRanks: bookRanks(g.BookPile),
OpponentBookRanks: bookRanks(g.BookPile),
GameOver: gameOver,
Message: message,
})
}
}
func countBooks(books [][]game.Card) int {
count := 0
for _, book := range books {
if len(book) > 0 {
count++
}
}
return count
}
func countOpponentBooks(books [][]game.Card) int {
count := 0
for _, book := range books {
if len(book) > 0 {
count++
}
}
return count
}
func bookRanks(books [][]game.Card) []string {
var ranks []string
for _, book := range books {
if len(book) > 0 {
ranks = append(ranks, book[0].Rank.String())
}
}
return ranks
}
func cardStrings(cards []game.Card) []string {
var result []string
for _, c := range cards {
result = append(result, c.Rank.String()+c.Suit.String())
}
return result
}
func respondJSON(w http.ResponseWriter, code int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(data)
}
func generateID() string {
return fmt.Sprintf("player-%d", rand.Intn(10000))
}