Skip to content

Commit

Permalink
Изменение соглашений: stdout<->stderr, коды ничьи и поражения
Browse files Browse the repository at this point in the history
Добавлена возможность сохранения раундов в папку
  • Loading branch information
RobolabGs2 committed Oct 14, 2020
1 parent fff6fba commit ffe55e8
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 71 deletions.
63 changes: 40 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
# botctl
CLI утилита для проведения соревнований между ботами в рамках курса CS253. Интеллектуальные системы

# Требования к боту
+ Первым аргументом командной строки принимает каким цветом он играет: `0` или `1`
+ `stdin` принимает ходы соперника
+ `stdout` пишет свой ход
+ `stderr` пишет всё остальное - логи, информацию для человека, состояние доски
+ `exit code`: победа-`0`, поражение-`1`, ничья-`2`

# Использование
```
Usage:
botctl [flags] path/to/mybot1.exe path/to/mybot2.exe
-r int
Количество раундов (default 1)
-v int
0 - логи ботов не выводятся, 1 - выводятся логи первого, 2 - обоих ботов (default 1)
```

Порядок ботов влияет на вывод: в `stdout` будет писаться `stderr` первого бота, в `stderr` - второго.

По окончанию всех раундов будет выведен суммарный счёт. При смене раунда цвета меняются.
# botctl
CLI утилита для проведения соревнований между ботами в рамках курса CS253. Интеллектуальные системы

# Требования к боту
+ Последним аргументом командной строки принимает цвет (очередь хода), которым он играет: `0` или `1`
+ `stdin`(`cin`) принимает ходы соперника
+ `stderr`(`cerr`) пишет свой ход (один на строке)
+ `stdout`(`cout`) пишет всё остальное - логи, информацию для человека, состояние доски
+ `exit code`: победа-`0`, поражение-`3`, ничья-`4` (соответственно, должен понимать, когда игра окончилась)

# Использование
```
Usage:
botctl [flags] 'path/to/mybot1.exe [addition args]' 'path/to/mybot2.exe [addition args]'
-o1 string
Куда перенаправить stdout первого бота
'-' = stdout, '+' - stderr, '<dirname>' - будет сохранено в папку, файл на раунд, пусто - игнорировать (default "+")
-o2 string
Куда перенаправить stdout второго бота
'-' = stdout, '+' - stderr, '<dirname>' - будет сохранено в папку, файл на раунд, пусто - игнорировать
-r int
Количество раундов (default 1)
```

По окончанию всех раундов будет выведен суммарный счёт (1 балл за победу, 0.5 за ничью, 0 за поражение).
При смене раунда цвета меняются.
```
Раунд reversi_bot_v2.exe reversi_bot_v3.exe
0 Поражение, ходил первым Победа, ходил вторым
1 Поражение, ходил вторым Победа, ходил первым
2 Победа, ходил первым Поражение, ходил вторым
3 Поражение, ходил вторым Победа, ходил первым
4 Поражение, ходил первым Победа, ходил вторым
5 Победа, ходил вторым Поражение, ходил первым
6 Поражение, ходил первым Победа, ходил вторым
7 Ничья, ходил вторым Ничья, ходил первым
8 Победа, ходил первым Поражение, ходил вторым
9 Поражение, ходил вторым Победа, ходил первым
Итого 3.5 6.5
```
58 changes: 44 additions & 14 deletions bot.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"fmt"
"os"
"os/exec"
Expand All @@ -10,6 +11,7 @@ import (

type Bot struct {
Name string
File string
Cmd string
Number int
TotalScore float32
Expand All @@ -29,18 +31,44 @@ func (b BotCmd) String() string {
return b.Name
}

func (b BotCmd) Finish() (GameResult, string, error) {
score, err := SummarizeGame(b.Wait())
if err != nil {
return 0, "", err
}
switch score {
case Win:
b.TotalScore++
case Draw:
b.TotalScore += 0.5
func (b BotCmd) WaitChan() chan error {
errs := make(chan error)
go func() {
err := b.Cmd.Wait()
select {
case errs <- err:
break
default:
break
}
close(errs)
}()
return errs
}

type BotGameResult struct {
Error error
GameResult GameResult
Bot *Bot
}

func (b BotCmd) Finish(ctx context.Context) (GameResult, string, error) {
select {
case <-ctx.Done():
return 0, "", ctx.Err()
case res := <-b.WaitChan():
score, err := SummarizeGame(res)
if err != nil {
return 0, "", b.error(err)
}
switch score {
case Win:
b.TotalScore++
case Draw:
b.TotalScore += 0.5
}
return score, fmt.Sprintf("%s, ходил %s", score, b.Order), nil
}
return score, fmt.Sprintf("%s, ходил %s", score, b.Order), nil
}

func (b Bot) AppendArgs(args []string, order TurnOrder) []string {
Expand All @@ -49,24 +77,26 @@ func (b Bot) AppendArgs(args []string, order TurnOrder) []string {
}

func NewBot(number int, cmd string) (*Bot, error) {
filename := filepath.Base(strings.Split(cmd, " ")[0])
bot := Bot{
Name: strings.Split(cmd, " ")[0],
Name: strings.TrimSuffix(filename, filepath.Ext(filename)),
File: filename,
Cmd: filepath.Clean(cmd),
Number: number,
}
return &bot, bot.checkFile()
}

func (b Bot) checkFile() error {
if _, err := os.Stat(b.Name); err != nil {
if _, err := os.Stat(b.File); err != nil {
return b.error(err)
}
return nil
}

func (b Bot) error(err error) error {
if err != nil {
return fmt.Errorf("проблемы с ботом %d.%s:%w", b.Number, b.Name, err)
return fmt.Errorf("проблемы с ботом %d: %s: %w", b.Number, b.Name, err)
}
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions gameresult.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ func SummarizeGame(err error) (GameResult, error) {
}
if exit := new(exec.ExitError); errors.As(err, &exit) {
switch exit.ExitCode() {
case 1:
case 3:
return Lose, nil
case 2:
case 4:
return Draw, nil
}
}
Expand Down
57 changes: 37 additions & 20 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
)

func main() {
rounds := flag.Int("r", 1, "Количество раундов")
bot1Output := flag.String("o", "-", "stderr первого бота ('-' = stdout, '+' - stderr, '<filename>' - будет сохранено в файл, пусто - /dev/null)")
bot1Output := LogCollectorFlag("o1", "+", "первого")
bot2Output := LogCollectorFlag("o2", "", "второго")
flag.Parse()
if flag.NArg() != 2 {
_, _ = fmt.Fprintln(os.Stderr, "Usage:\n botctl [flags] 'path/to/mybot1.exe [addition args]' 'path/to/mybot2.exe [addition args]'")
_, _ = fmt.Fprintln(
os.Stderr,
"Usage:\n botctl [flags] 'path/to/mybot1.exe [addition args]' 'path/to/mybot2.exe [addition args]'")
flag.PrintDefaults()
return
}
Expand All @@ -21,29 +26,41 @@ func main() {
bot2, err := NewBot(2, flag.Arg(1))
logAndExitOnError(err)
settings := TournamentSettings{
Bot1: bot1,
Bot2: bot2,
TotalWriter: os.Stdout,
ProcessWriter: os.Stderr,
Bot1Writer: nil,
Bot2Writer: nil,
Bot1: bot1,
Bot2: bot2,
TotalWriter: os.Stdout,
ProcessWriter: os.Stderr,
Bot1LogCollectorFabric: GetLogCollectorFabric(*bot1Output, bot1),
Bot2LogCollectorFabric: GetLogCollectorFabric(*bot2Output, bot2),
}
switch *bot1Output {
err = settings.Init().Run(*rounds)
if err != nil {
log.Println("Что-то пошло не так:", err)
}
}

func LogCollectorFlag(name, value, numeric string) *string {
return flag.String(name, value, fmt.Sprint(`Куда перенаправить stdout `, numeric, ` бота
'-' = stdout, '+' - stderr, '<dirname>' - будет сохранено в папку, файл на раунд, пусто - игнорировать`))
}

func GetLogCollectorFabric(botOutputOption string, bot *Bot) LogCollectorFabric {
switch botOutputOption {
case "":
break
return IgnoreLogs
case "-":
settings.Bot1Writer = os.Stdout
return LogsRedirectTotWriter(os.Stdout)
case "+":
settings.Bot1Writer = os.Stderr
return LogsRedirectTotWriter(os.Stderr)
default:
file, err := os.Create(*bot1Output)
logAndExitOnError(err)
defer file.Close()
settings.Bot1Writer = file
}
err = settings.Init().Run(*rounds)
if err != nil {
log.Println("Что-то пошло не так:", err)
logAndExitOnError(os.MkdirAll(botOutputOption, 0666))
return func(round int) io.Writer {
file, err := os.Create(filepath.Join(botOutputOption, fmt.Sprintf("round_%02d_bot_%d_%s.txt", round, bot.Number, bot.Name)))
if err != nil {
panic(fmt.Errorf("%s: %w", bot.Name, err))
}
return file
}
}
}

Expand Down
44 changes: 32 additions & 12 deletions tournament.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
package main

import (
"context"
"fmt"
"io"
"log"
"text/tabwriter"
)

type TournamentSettings struct {
Bot1, Bot2 *Bot
TotalWriter io.Writer
ProcessWriter io.Writer
Bot1Writer, Bot2Writer io.Writer
Bot1, Bot2 *Bot
TotalWriter io.Writer
ProcessWriter io.Writer
Bot1LogCollectorFabric, Bot2LogCollectorFabric LogCollectorFabric
}

type LogCollectorFabric func(round int) io.Writer

func IgnoreLogs(_ int) io.Writer {
return nil
}

func LogsRedirectTotWriter(writer io.Writer) func(_ int) io.Writer {
return func(_ int) io.Writer {
return writer
}
}

type Tournament struct {
Expand All @@ -23,13 +36,13 @@ type Tournament struct {
func (s TournamentSettings) Init() *Tournament {
return &Tournament{
s,
tabwriter.NewWriter(s.TotalWriter, 0, 4, 0, '\t', 0),
tabwriter.NewWriter(s.TotalWriter, 0, 4, 4, ' ', 0),
log.New(s.ProcessWriter, "botctl: ", log.Ltime|log.Lmsgprefix),
}
}

func (t *Tournament) Run(rounds int) error {
fmt.Fprintf(t.summary, "Раунд\t%s\t%s\n", t.Bot1.Name, t.Bot2.Name)
fmt.Fprintf(t.summary, "Раунд\t%s\t%s\n", t.Bot1.Cmd, t.Bot2.Cmd)
for i := 0; i < rounds; i++ {
t.logger.Println("Раунд", i)
res, err := t.Round(i)
Expand All @@ -45,10 +58,16 @@ func (t *Tournament) Run(rounds int) error {
func (t *Tournament) Round(i int) (string, error) {
bot1TurnOrder := TurnOrder(i%2 == 0)
bot1, bot2 := t.Bot1.MakeCmd(bot1TurnOrder), t.Bot2.MakeCmd(bot1TurnOrder.Opponent())
bot1.Stdout, _ = bot2.StdinPipe()
bot2.Stdout, _ = bot1.StdinPipe()
bot1.Stderr = t.Bot1Writer
bot2.Stderr = t.Bot2Writer
bot1.Stderr, _ = bot2.StdinPipe()
bot2.Stderr, _ = bot1.StdinPipe()
bot1.Stdout = t.Bot1LogCollectorFabric(i)
if closer, ok := bot1.Stdout.(io.WriteCloser); ok {
defer closer.Close()
}
bot2.Stdout = t.Bot2LogCollectorFabric(i)
if closer, ok := bot2.Stdout.(io.WriteCloser); ok {
defer closer.Close()
}
if err := bot1.Start(); err != nil {
return "", err
}
Expand All @@ -57,8 +76,9 @@ func (t *Tournament) Round(i int) (string, error) {
return "", err
}
defer bot2.Process.Kill()
score1, score1Desc, err1 := bot1.Finish()
score2, score2Desc, err2 := bot2.Finish()
// TODO
score1, score1Desc, err1 := bot1.Finish(context.Background())
score2, score2Desc, err2 := bot2.Finish(context.Background())
if err1 != nil {
return "", err1
}
Expand Down

0 comments on commit ffe55e8

Please sign in to comment.