2026-02-15 23:17:10 +00:00
|
|
|
package phase10
|
|
|
|
|
|
2026-02-19 01:51:19 +00:00
|
|
|
import "core:io"
|
|
|
|
|
import "core:encoding/csv"
|
2026-02-18 21:52:31 +00:00
|
|
|
import "core:os"
|
|
|
|
|
import "core:strings"
|
2026-02-16 01:30:11 +00:00
|
|
|
import "core:strconv"
|
|
|
|
|
import "core:fmt"
|
|
|
|
|
import "core:slice"
|
|
|
|
|
|
|
|
|
|
// Struct to hold all information about a game
|
2026-02-15 23:17:10 +00:00
|
|
|
Game :: struct {
|
|
|
|
|
names: [dynamic]string,
|
|
|
|
|
scores: [dynamic][dynamic]int,
|
|
|
|
|
phases: [dynamic]int
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 01:30:11 +00:00
|
|
|
// Clean up memory allocated for a Game struct
|
2026-02-15 23:17:10 +00:00
|
|
|
deleteGameData :: proc(game: ^Game) {
|
2026-02-20 05:10:23 +00:00
|
|
|
for name in game.names {
|
|
|
|
|
delete(name)
|
|
|
|
|
}
|
2026-02-15 23:17:10 +00:00
|
|
|
for scorelist in game.scores {
|
|
|
|
|
delete(scorelist)
|
|
|
|
|
}
|
|
|
|
|
delete(game.names)
|
2026-02-20 05:10:23 +00:00
|
|
|
delete(game.scores)
|
2026-02-15 23:17:10 +00:00
|
|
|
delete(game.phases)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 01:30:11 +00:00
|
|
|
// Prompt user to enter scores for each player
|
|
|
|
|
addScores :: proc(game: ^Game) {
|
|
|
|
|
buf: [2048]byte
|
|
|
|
|
fmt.print(" ")
|
|
|
|
|
for name in game.names {
|
|
|
|
|
fmt.print(name, "")
|
|
|
|
|
}
|
|
|
|
|
fmt.println()
|
|
|
|
|
fmt.print("Scores: ")
|
|
|
|
|
newScores := getSpaceDelimetedItems(buf[:])
|
2026-02-18 21:50:34 +00:00
|
|
|
if len(newScores) != len(game.names) {
|
2026-02-18 21:52:31 +00:00
|
|
|
fmt.println("[ERROR] Invalid number of scores")
|
2026-02-18 21:50:34 +00:00
|
|
|
return
|
|
|
|
|
}
|
2026-02-16 01:30:11 +00:00
|
|
|
defer delete(newScores)
|
|
|
|
|
for score, index in newScores {
|
|
|
|
|
intScore, _ := strconv.parse_int(score)
|
|
|
|
|
append(&(game.scores[index]), intScore)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the status of each player's phase in a game based on scores each round
|
|
|
|
|
updatePhasesByScores :: proc(game: ^Game) {
|
2026-02-15 23:17:10 +00:00
|
|
|
for name, index in game.names {
|
|
|
|
|
phase: int = 1
|
|
|
|
|
for score in game.scores[index] {
|
|
|
|
|
if score < 50 {
|
|
|
|
|
phase += 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
game.phases[index] = phase
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 01:30:11 +00:00
|
|
|
// Add a player to the game
|
2026-02-15 23:17:10 +00:00
|
|
|
addPlayer :: proc(game: ^Game, name: string, score: int = 0, phase: int = 1) {
|
2026-02-20 05:10:23 +00:00
|
|
|
stringCopy: string = strings.clone(name)
|
|
|
|
|
append(&game.names, stringCopy)
|
2026-02-15 23:17:10 +00:00
|
|
|
append(&game.phases, phase)
|
2026-02-20 05:10:23 +00:00
|
|
|
// Calculate necessary score sheet to result in score and phase
|
2026-02-15 23:17:10 +00:00
|
|
|
scores: [dynamic]int
|
|
|
|
|
for i := 1; i < phase - 1; i += 1 {
|
|
|
|
|
append(&scores, 0)
|
|
|
|
|
}
|
|
|
|
|
if score >= 50 {
|
|
|
|
|
append(&scores, 0)
|
|
|
|
|
}
|
|
|
|
|
if (phase != 1) {
|
|
|
|
|
append(&scores, score)
|
|
|
|
|
}
|
|
|
|
|
append(&game.scores, scores)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 01:30:11 +00:00
|
|
|
// Get aggregate score of player by index
|
|
|
|
|
getScore :: proc(game: ^Game, playerIndex: int) -> int {
|
|
|
|
|
sum: int = 0
|
|
|
|
|
for score in game.scores[playerIndex] {
|
|
|
|
|
sum += score
|
|
|
|
|
}
|
|
|
|
|
return sum
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the index of the player who wins, or -1 if there is no winner
|
|
|
|
|
checkWinner :: proc(game: ^Game) -> int {
|
|
|
|
|
winners: [dynamic]int
|
|
|
|
|
defer delete(winners)
|
2026-02-15 23:17:10 +00:00
|
|
|
for player, index in game.names {
|
|
|
|
|
if game.phases[index] > 10 {
|
2026-02-16 01:30:11 +00:00
|
|
|
append(&winners, index)
|
2026-02-15 23:17:10 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-16 01:30:11 +00:00
|
|
|
|
|
|
|
|
if len(winners) > 0 {
|
|
|
|
|
// sort by lowest points
|
|
|
|
|
context.user_ptr = game
|
|
|
|
|
point_order :: proc(lhs, rhs: int) -> bool {
|
|
|
|
|
game: ^Game = (^Game)(context.user_ptr)
|
|
|
|
|
return getScore(game, lhs) < getScore(game, rhs)
|
|
|
|
|
}
|
|
|
|
|
slice.sort_by(winners[:], point_order)
|
|
|
|
|
return winners[0]
|
|
|
|
|
}
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 21:50:34 +00:00
|
|
|
// Print state of the game as a nice table
|
2026-02-16 01:30:11 +00:00
|
|
|
printGame :: proc(game: ^Game) {
|
|
|
|
|
fmt.printf("Name\tScore\tPhase\n")
|
|
|
|
|
fmt.printf("----\t-----\t-----\n")
|
|
|
|
|
for name, index in game.names {
|
2026-02-20 05:10:23 +00:00
|
|
|
fmt.printf(
|
|
|
|
|
"%v\t%v\t%v\n",
|
|
|
|
|
name,
|
|
|
|
|
getScore(game, index),
|
|
|
|
|
game.phases[index]
|
|
|
|
|
)
|
2026-02-16 01:30:11 +00:00
|
|
|
}
|
2026-02-15 23:17:10 +00:00
|
|
|
}
|
2026-02-18 21:52:31 +00:00
|
|
|
|
|
|
|
|
// Return a slice of space delimited strings from stdin
|
|
|
|
|
getSpaceDelimetedItems :: proc(backingBuffer: []byte) -> []string {
|
|
|
|
|
count, _ := os.read(os.stdin, backingBuffer)
|
|
|
|
|
response := string(backingBuffer[:count - 1]) // leave off the newline
|
|
|
|
|
strings.trim_space(response)
|
|
|
|
|
items, _ := strings.fields(response)
|
|
|
|
|
return items
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prompt user for names of all players
|
|
|
|
|
getNames :: proc(backingBuffer: []byte) -> []string {
|
|
|
|
|
fmt.print("Enter Names: ")
|
|
|
|
|
return getSpaceDelimetedItems(backingBuffer)
|
|
|
|
|
}
|
2026-02-19 01:51:19 +00:00
|
|
|
|
|
|
|
|
// export a game as a csv
|
|
|
|
|
exportGame :: proc(game: ^Game, filename: string) {
|
|
|
|
|
// Open handle to file
|
2026-02-20 04:38:25 +00:00
|
|
|
handle, err := os.open(
|
|
|
|
|
filename,
|
|
|
|
|
flags = os.O_CREATE | os.O_WRONLY | os.O_TRUNC,
|
|
|
|
|
mode = 0o644
|
|
|
|
|
)
|
2026-02-19 01:51:19 +00:00
|
|
|
if err != nil {
|
|
|
|
|
fmt.eprintln("Could not open file:", filename)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer os.close(handle)
|
|
|
|
|
|
|
|
|
|
// create stream from file handle
|
|
|
|
|
stream := os.stream_from_handle(handle)
|
|
|
|
|
defer io.destroy(stream)
|
|
|
|
|
|
|
|
|
|
// Create CSV Writer
|
|
|
|
|
w: csv.Writer
|
|
|
|
|
csv.writer_init(&w, stream)
|
|
|
|
|
|
2026-02-20 04:38:25 +00:00
|
|
|
// We will store all rows as slices of strings. We will be allocating
|
|
|
|
|
// memory for the strings, so we need to free that memory too.
|
2026-02-19 01:51:19 +00:00
|
|
|
records: [dynamic][]string
|
|
|
|
|
defer {
|
|
|
|
|
for record in records {
|
|
|
|
|
for col in record {
|
|
|
|
|
delete(col)
|
|
|
|
|
}
|
|
|
|
|
delete(record)
|
|
|
|
|
}
|
|
|
|
|
delete(records)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// First row is all the names, each subsequent row is the score for a
|
|
|
|
|
// particular round. It is important that we create a new array for each of
|
|
|
|
|
// these so we don't accidentally free the actual data in the game struct.
|
|
|
|
|
nameRow: [dynamic]string
|
|
|
|
|
for name in game.names {
|
|
|
|
|
s := strings.clone(name)
|
|
|
|
|
append(&nameRow, s)
|
|
|
|
|
}
|
|
|
|
|
append(&records, nameRow[:])
|
|
|
|
|
|
|
|
|
|
for i := 0 ; i < len(game.scores[0]); i += 1 {
|
|
|
|
|
scoreRowString: [dynamic]string
|
|
|
|
|
for playerScores in game.scores {
|
|
|
|
|
s := strings.builder_make()
|
|
|
|
|
strings.write_int(&s, playerScores[i])
|
|
|
|
|
scoreCol := strings.to_string(s)
|
|
|
|
|
append(&scoreRowString, scoreCol)
|
|
|
|
|
}
|
|
|
|
|
append(&records, scoreRowString[:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write the records to the file
|
|
|
|
|
csv.write_all(&w, records[:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// import an existing game from a csv file
|
2026-02-20 04:38:25 +00:00
|
|
|
importGame :: proc(game: ^Game, filename: string) {
|
|
|
|
|
r: csv.Reader
|
|
|
|
|
r.trim_leading_space = true
|
|
|
|
|
r.reuse_record = true
|
|
|
|
|
r.reuse_record_buffer = true
|
|
|
|
|
defer csv.reader_destroy(&r)
|
|
|
|
|
|
|
|
|
|
csv_data, success := os.read_entire_file(filename, context.allocator)
|
|
|
|
|
defer delete(csv_data)
|
|
|
|
|
|
|
|
|
|
if success == true {
|
|
|
|
|
csv.reader_init_with_string(&r, string(csv_data))
|
|
|
|
|
} else {
|
|
|
|
|
fmt.eprintfln("Unable to open file: %v", filename)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for r, i, err in csv.iterator_next(&r) {
|
2026-02-20 05:10:23 +00:00
|
|
|
if err != nil { /* Do something with error */ }
|
|
|
|
|
for f, j in r {
|
|
|
|
|
if i == 0 {
|
|
|
|
|
nameClone := strings.clone(f)
|
|
|
|
|
scores: [dynamic]int
|
|
|
|
|
append(&game.names, nameClone)
|
|
|
|
|
append(&game.scores, scores)
|
|
|
|
|
append(&game.phases, 1)
|
|
|
|
|
} else {
|
|
|
|
|
score, _ := strconv.parse_int(f)
|
|
|
|
|
append(&game.scores[j], score)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
updatePhasesByScores(game)
|
2026-02-19 01:51:19 +00:00
|
|
|
}
|