再びトーナメント表作成 ⑧ 第一段階完成

先日から、トーナメント表作成の仕組みを焼き直ししている。
infoment.hatenablog.com
今回も、前回の続きから。

前回の結果を踏まえ、最終的にこの形に落ち着いた。

Option Explicit

Public PlayerRange As Range
Public PlayerDict As Object
Public InversFlag As Boolean
Public StartNumber As Long
Dim Wf As WorksheetFunction
Dim NetPalyerNumber As Long
Dim TournamentSize As Long
Dim PlayerCount As Long

' 名前のセル罫線要否。
Public BordersAroundName As Boolean
' 勝ち残りの名前セル要否。
Public IntermediateName As Boolean

Private Sub Class_Initialize()
    Set Wf = WorksheetFunction
End Sub

Public Sub init(Optional player_range As Range, _
                Optional invers_flag As Boolean = False, _
                Optional start_number As Long = 1, _
                Optional borders_around_name As Boolean = False, _
                Optional intermediate_name As Boolean = False)
    
    Dim arr As Variant
    Dim i As Long
        If Not player_range Is Nothing Then
            Set PlayerRange = player_range
        Else
            Set PlayerRange = Selection
        End If
        InversFlag = invers_flag
    
    Set PlayerDict = CreateObject("Scripting.Dictionary")
    Dim r As Range
        For Each r In PlayerRange
            If r.Value <> vbNullString Then
                NetPalyerNumber = NetPalyerNumber + 1
                PlayerDict(NetPalyerNumber) = r.Value
            End If
        Next

        ' 一回戦の人の配置を、前大会の何位からランダムに入れ替えるか。
        StartNumber = start_number

        ' 選手の数から求めるトーナメントサイズ(決勝までの段数)。
        TournamentSize = Wf.Ceiling_Math(Wf.Log10(NetPalyerNumber), Wf.Log10(2)) / Wf.Log10(2)

        ' 一回戦の人数。
        PlayerCount = 2 ^ TournamentSize
        
        ' 名前周辺の罫線要否。
        BordersAroundName = borders_around_name
        
        ' 勝ち上がり選手名の箱要否。
        IntermediateName = intermediate_name

End Sub

Private Property Get TournamentOrderArray() As Variant
    
    ' 作業用配列。最終的に戻り値となる。
    Static arr As Variant
    If Not IsEmpty(arr) Then
        TournamentOrderArray = arr
        Exit Property
    End If
    
    ReDim arr(1 To 1)
    ' 初期値。選手が一人のとき。
        arr(1) = 1
        If PlayerCount = 1 Then
            TournamentOrderArray = arr
            Exit Property
        End If
    
    Dim i As Long
    Dim j As Long
    
    ' 選手の数に併せて大きくなるトーナメントの仮格納用配列。
    Dim TempArray As Variant
        For i = 1 To TournamentSize
            ReDim TempArray(1 To UBound(arr) * 2)
            For j = 1 To UBound(arr)
                ' 元の配列内の数値を、上から奇数で、下から偶数でセット。
                TempArray(j) = arr(j) * 2 - 1
                TempArray(UBound(TempArray) + 1 - j) = arr(j) * 2
            Next
            arr = TempArray
        Next
        
        ' 上位半数の相手を改めてセット。
        ' 例えば32人のトーナメントでは、1と32が、2と31が、3と30が・・・nと33-nが
        ' ペアになるようにする。※優先的に前回上位者を一回戦シードとするため。
        For i = 1 To PlayerCount
            If arr(i) > PlayerCount / 2 Then
                ' 4人一組で評価するとき、2番目は1番目と、3番目は4番目と
                ' ペアになることから、4で割った余りで処理を変更する。
                Select Case i Mod 4
                    Case 2: arr(i) = PlayerCount - arr(i - 1) + 1
                    Case 3: arr(i) = PlayerCount - arr(i + 1) + 1
                End Select
            End If
        Next
        TempArray = arr
        
        ' 配列の最後を1位にしたい場合用。配列の順序をひっくり返す。
        If InversFlag Then
            For i = 1 To UBound(arr)
                arr(i) = TempArray(UBound(TempArray) - i + 1)
            Next
        End If
        
        TournamentOrderArray = arr

End Property

Private Property Get RandomSortDict() As Object

    Static Dict As Object
        If Not Dict Is Nothing Then
            Set RandomSortDict = Dict
            Exit Property
        End If

    ' 作業用配列。
    Dim TempArray As Variant
    ReDim TempArray(1 To NetPalyerNumber)
    ' 戻り値用配列。
    Dim arr As Variant
    ReDim arr(1 To NetPalyerNumber)

    Dim i As Long
        For i = 1 To NetPalyerNumber
            ' ソートしない範囲。
            If i < StartNumber Then
                TempArray(i) = i
            
            ' ソート範囲。iMax以上の桁でソートする。
            ' このときtotalの桁数範囲には、元の数字が保存されている。
            Else
                TempArray(i) = CLng(WorksheetFunction.RandBetween(1, 100) * _
                              (10 ^ Len(CStr(PlayerCount))) + i)
            End If
        Next

        ' 配列ソート。
        TempArray = SortArray(TempArray)
        
        ' 保存された元の数字を取り出す。
        ' SortArray関数を使用すると、配列が0始まりになるので注意。
        For i = NetPalyerNumber To 1 Step -1
            arr(i) = Right(TempArray(i - 1), CLng(Len(CStr(NetPalyerNumber)))) * 1
        Next

    Set Dict = CreateObject("Scripting.Dictionary")
        For i = 1 To NetPalyerNumber
            Dict(i) = arr(i)
        Next
        
        If NetPalyerNumber < PlayerCount Then
            For i = NetPalyerNumber + 1 To PlayerCount
                Dict(i) = i
            Next
        End If
        
    Set RandomSortDict = Dict

End Property

Public Function SortArray(ByVal source_array As Variant, _
                       Optional sort_order As Excel.XlSortOrder = xlAscending) As Variant
                       
    Dim aryList As Object
    Set aryList = CreateObject("System.Collections.ArrayList")
    Dim s As Variant
    
        For Each s In source_array
            Call aryList.Add(s)
        Next
    
        Select Case sort_order
            Case xlAscending
                ' 昇順でソート。
                Call aryList.Sort
            Case xlDescending
                ' 昇順でソートののち、降順へ反転。
                Call aryList.Sort
                Call aryList.Reverse
        End Select
    
        SortArray = aryList.ToArray
        
End Function

Public Property Get TournamentSortedOrderArray()

    Dim i As Long
    Dim arr() As Variant
    ReDim arr(1 To PlayerCount)
        For i = 1 To PlayerCount
            arr(i) = PlayerDict(RandomSortDict(TournamentOrderArray(i)))
        Next
        TournamentSortedOrderArray = arr
        
End Property

Public Sub CreateTournament()

    Sheets.Add After:=Sheets(Sheets.Count)
       
    Dim i As Long
    Dim j As Long
    
        ' 1回戦~準決勝戦まで。
        For j = 1 To TournamentSize
            ' j回戦の一人一人について。
            For i = 1 To (2 ^ TournamentSize) / (2 ^ (j - 1))
                With Cells((2 * i - 1) * 2 ^ (j - 1), 3 * j - 2)
                    Select Case j
                        ' 横の罫線だけ、先に描画しておく。
                        ' ※セル結合により、人名入力セル内は罫線が無くなる。
                        Case 1
                            .Resize(, 2).Borders(xlEdgeBottom).Weight = xlThin
                        Case Else
                            .Resize(, 3).Offset(, -1).Borders(xlEdgeBottom).Weight = xlThin
                    End Select
                    .Resize(2).Merge
                    .Resize(2).Borders.Weight = xlThin
                End With
            Next
        Next

        ' 決勝戦。
        With Cells(2 ^ TournamentSize, 3 * j - 2)
            .Resize(, 2).Offset(, -1).Borders(xlEdgeBottom).Weight = xlThin
            .Resize(2).Merge
            .Resize(2).Borders.Weight = xlThin
        End With
        
        ' 縦の線。
        For j = 1 To TournamentSize
            For i = 1 To 2 ^ (TournamentSize - j)
                Cells((2 ^ (j + 1)) * i - (3 * 2 ^ (j - 1) - 1), 3 * j - 1).Resize(2 ^ j).Borders(xlEdgeRight).Weight = xlThin
            Next
        Next
        
        ' 名前のセット
    Dim arr As Variant
        arr = TournamentSortedOrderArray
        
        For i = 1 To UBound(arr)
            Cells(i * 2 - 1, 1) = arr(i)
        Next
        
    ' シードを確認。
    Dim PlayerName(2) As String
        If TournamentSize >= 2 Then
            For i = 1 To 2 ^ TournamentSize / 2
            
                ' 一回戦の対戦同士、セル4つを一組で考える。
                With Cells(4 * i - 3, 1)
                    
                    ' それぞれのセット順を配列に格納。
                    PlayerName(0) = .Item(1) & .Item(3)
                    PlayerName(1) = .Item(1)
                    PlayerName(2) = .Item(3)
                    
                    ' 両方ともに空白はあり得ない(その場合、トーナメントの山が一つ小さくなる)。
                    ' したがって、両者順位の積が0ならば、それは一回戦が不戦勝を意味する(=シード)。
                    If PlayerName(1) = vbNullString Or PlayerName(2) = vbNullString Then
                        
                        ' セルの結合しなおし。および、罫線の引き直し。
                        With .Resize(4)
                            .UnMerge
                            .Resize(, 2).Borders.LineStyle = xlNone
                            .Value = vbNullString
                            .Item(2) = PlayerName(0)
                            .Borders.LineStyle = xlNone
                            .Item(2).Resize(2).Merge
                            .Item(2).Resize(2).Borders.Weight = xlThin
                            .Item(2).Resize(2, 2).Borders(xlInsideHorizontal).Weight = xlThin
                        End With
                    End If
                End With
            Next
        End If

End Sub

Private Sub AdjustFormat()
    Dim i As Long
 
        ' トーナメント表の名前以外の幅を調整。
        For i = 0 To TournamentSize
            Columns("B:C").Offset(, 3 * i).ColumnWidth = 3
        Next

        ' 名前セルの罫線設定。
        If Not BordersAroundName Then
            Columns(2).Borders(xlEdgeLeft).LineStyle = xlNone
            For i = 0 To TournamentSize
                Columns(3 * i + 1).Borders.LineStyle = xlNone
            Next
        End If

        ' 勝ち残り名前セルの設定。
        If Not IntermediateName Then
            For i = 0 To TournamentSize - 2
                Cells(1, i + 3).Resize(, 2).EntireColumn.Delete
            Next
        End If
        
        Columns(1).EntireColumn.AutoFit
        
        ' 空白セルの高さ設定。
        For i = 1 To ActiveSheet.UsedRange.Rows.Count
            If Cells(i, 1).MergeCells = False Then
                Cells(i, 1).RowHeight = Cells(i, 1).RowHeight / 4
            End If
        Next
End Sub

Private Sub Class_Terminate()
    Call AdjustFormat
End Sub

ここまで作りこんでおくと、実際のトーナメント作成はこれだけで済む。

Sub Test()
    With New VBAProject.Tournament
        ' 名簿の範囲指定。
        .init Sheets("sheet1").ListObjects(1).ListColumns(2).DataBodyRange
        ' トーナメント作成。
        .CreateTournament
    End With
End Sub

前回との大きな違いはやはり、人数の上限が無くなったことか。
5000人で試してみたところ、問題なく作成できた。

やれやれ、やっと形になった。と思ったら、先生から新たなリクエストが。

  1. 同じ道場の子は、1回戦で当たらないようにしたい。
  2. 形と組手で、同じ対戦相手と当たらないようにしたい。

トライしましょう。

ということで、今のところ第一弾完成。次回は、1回戦で同門対決にならない
仕掛けに挑戦です。

参考まで。