This commit is contained in:
2026-09-17 22:02:53 +03:00
commit f30cb0788a
8 changed files with 720 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
package main
import "base:runtime"
import c "core:c/libc"
import "core:bufio"
import "core:encoding/json"
import "core:fmt"
import "core:os"
import "core:strings"
import curl "vendor:curl"
API_URL :: "http://localhost:8080/v1/chat/completions"
REQUEST :: `{
"messages": [
{
"role": "system",
"content": "Write one short line spoken by the named NPC. Realize BEAT as dialogue. No narration, labels, explanation, player dialogue, or thinking."
},
{
"role": "user",
"content": "NPC: Mira, firm innkeeper.\nBEAT: Refuse lodging; the fixed price is eight silver."
},
{
"role": "assistant",
"content": "{\"dialogue\":\"Eight silver first, or find another roof.\"}"
},
{
"role": "user",
"content": "NPC: Osric, cowardly toll keeper.\nBEAT: Frightened by Duke Harland; waive the toll and permit passage."
},
{
"role": "assistant",
"content": "{\"dialogue\":\"No toll for His Grace's people. Go through, quickly.\"}"
},
{
"role": "user",
"content": "NPC: Captain Ronald, gruff, corrupt gate guard.\nSCENE: Night at Oakhaven's closed gate.\nBEAT: $BEAT"
}
],
"temperature": 0.3,
"top_p": 0.9,
"top_k": 40,
"max_tokens": 180,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "npc_line",
"strict": true,
"schema": {
"type": "object",
"properties": {
"dialogue": {
"type": "string",
"minLength": 1,
"maxLength": 140
}
},
"required": ["dialogue"],
"additionalProperties": false
}
}
}
}`
Message :: struct {
content: string,
}
Choice :: struct {
message: Message,
}
Chat_Response :: struct {
choices: []Choice,
}
NPC_Line :: struct {
dialogue: string,
}
write_response :: proc "c" (
data: [^]byte,
size, count: c.size_t,
user_data: rawptr,
) -> c.size_t {
context = runtime.default_context()
n := int(size * count)
response := cast(^[dynamic]byte)user_data
append(response, ..data[:n])
return c.size_t(n)
}
request_dialogue :: proc(headers: ^curl.slist, beat: string) -> (string, bool) {
body, allocated := strings.replace_all(REQUEST, "$BEAT", beat)
defer if allocated {
delete(body)
}
body_cstr := strings.clone_to_cstring(body)
defer delete(body_cstr)
response := make([dynamic]byte, 0, 1024)
defer delete(response)
handle := curl.easy_init()
if handle == nil {
return "", false
}
defer curl.easy_cleanup(handle)
curl.easy_setopt(handle, .URL, API_URL)
curl.easy_setopt(handle, .HTTPHEADER, headers)
curl.easy_setopt(handle, .POSTFIELDS, body_cstr)
curl.easy_setopt(handle, .POSTFIELDSIZE, c.long(len(body)))
curl.easy_setopt(handle, .WRITEFUNCTION, write_response)
curl.easy_setopt(handle, .WRITEDATA, &response)
curl.easy_setopt(handle, .TIMEOUT, c.long(30))
result := curl.easy_perform(handle)
if result != .E_OK {
fmt.eprintfln("curl: %s", curl.easy_strerror(result))
return "", false
}
chat: Chat_Response
if json.unmarshal(response[:], &chat) != nil || len(chat.choices) == 0 {
fmt.eprintfln("server: %s", string(response[:]))
return "", false
}
defer delete(chat.choices)
defer delete(chat.choices[0].message.content)
line: NPC_Line
if json.unmarshal_string(chat.choices[0].message.content, &line) != nil {
return "", false
}
return line.dialogue, true
}
print_menu :: proc() {
fmt.println("\n1. Demand passage")
fmt.println("2. Offer 20 gold")
fmt.println("3. Pay 50 gold")
fmt.println("4. Invoke Lady Marwyn")
fmt.print("> ")
}
main :: proc() {
if curl.global_init(c.long(curl.GLOBAL_DEFAULT)) != .E_OK {
fmt.eprintln("Could not initialize libcurl")
return
}
defer curl.global_cleanup()
headers := curl.slist_append(nil, "Content-Type: application/json")
if headers == nil {
fmt.eprintln("Could not create HTTP headers")
return
}
defer curl.slist_free_all(headers)
reader: bufio.Reader
bufio.reader_init(&reader, os.to_reader(os.stdin))
defer bufio.reader_destroy(&reader)
fmt.println("Oakhaven, after dark. Captain Ronald blocks the gate.")
if line, ok := request_dialogue(headers, "Keep the gate closed; demand exactly fifty gold before allowing passage."); ok {
defer delete(line)
fmt.printfln("\nRonald: %s", line)
} else {
fmt.eprintln("LLM request failed. Is llama-server running on port 8080?")
return
}
actions := [4]string{
"Open the gate. I don't have time for this.",
"Take twenty gold and let me through.",
"Fine. Here's your fifty gold.",
"Lady Marwyn will hear how you delayed her envoy.",
}
beats := [4]string{
"Keep the gate closed; demand exactly fifty gold before allowing passage.",
"Keep the gate closed; reject twenty gold and insist on exactly fifty gold.",
"Accept exactly fifty gold, open the gate, and permit passage.",
"Permit passage without payment. Claim a misunderstanding because Ronald fears Lady Marwyn.",
}
for {
print_menu()
input, err := bufio.reader_read_string(&reader, '\n')
if err != nil && len(input) == 0 {
return
}
defer delete(input)
if len(input) == 0 || input[0] < '1' || input[0] > '4' {
fmt.println("Choose 1-4.")
continue
}
choice := int(input[0] - '1')
fmt.printfln("\nYou: %s", actions[choice])
line, ok := request_dialogue(headers, beats[choice])
if !ok {
fmt.eprintln("LLM request failed")
continue
}
fmt.printfln("\nRonald: %s", line)
delete(line)
if choice == 2 || choice == 3 {
fmt.println("\nThe gate opens.")
return
}
}
}